mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-04 00:02:04 +00:00
4e8a76261b01ac1a50c7494afe82e486ea3c6d92
95
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bf5149bea0 |
fix(editor): stop losing edits, share buffers, add file strip, quick open and go to line (#984)
* fix(host): save local files atomically via a temp file and rename LocalHost::write_file truncated the target in place, so a crash, a full disk or a killed process mid-save destroyed the user's file. It now writes a hidden sibling temp file, syncs it, keeps the old file's mode and renames it over the target, removing the temp file on any error. It still writes in place where a rename would change something visible: a non-regular target (symlink, directory, FIFO), a read-only file, and on Unix a hard-linked file or one owned by another user, or when the temp file cannot be created (e.g. a read-only directory). * feat(editor): add editor_text for encodings, line endings, indentation and EditorConfig A pure module the code editor will use when loading and saving files: decode detects BOMs, binary files, UTF-8, GB18030 and a lossless Windows-1252 fallback and normalises CRLF; encode restores the exact bytes and names the first unrepresentable character; detect_indent infers tabs or a 2/4/8 space width with language defaults; and editorconfig_for resolves .editorconfig sections with save-time rules. * feat(editor): share buffers across tabs, guard unsaved work, add a file strip - One buffer per file per window; tabs list which buffers they show. The same file open in two tabs is no longer two diverging copies. - Closing a tab, its last pane, the window, or quitting asks about unsaved files (Save / Cancel / Discard) instead of dropping them. Bulk closes skip tabs with unsaved files; a tab that vanishes any other way hands its unsaved buffers to the tab in front. - File tree rename/delete now retarget or flag the open buffer, so a save no longer recreates the old path. - Saves check the file's mtime first and ask before overwriting a change made elsewhere; this is the only detection SFTP buffers get. - Dirty is a comparison with the saved text, so undoing back clears it. - Reloads replace only the changed span as an ordinary edit, keeping undo. - Load/save go through editor_text: encoding, BOM and CRLF round-trip, indentation is detected, .editorconfig is honoured. - Header shows a strip of open files; New File, Save As (native panel locally, a path bar remotely), Go to Line (Ctrl+G), and the status bar shows indentation, encoding and a clickable line ending. - Open files are remembered per tab across restarts. * feat(search): quick open a file by name from a Files tab Search Everywhere gains a Files tab that finds any file in the active tab's project by fuzzy name and opens it in the built-in editor, with `name:line[:col]` jumping to that spot. The list comes from one walk of the project through the host (Host::search with an empty query), so it works the same on local, SSH and WSL workspaces and skips what the tree hides: dotfiles, .git and gitignored paths. The walk is capped at 50k entries / 20k directories, kept between openings and revalidated in the background each time the search opens. Files join the All tab once a query finds them. Go to File... is bound to Cmd+O on macOS (Cmd+P is already Search Everywhere) and ships unbound elsewhere, where every obvious chord is taken or owed to the shell. * chore(editor): allowlist the editor session file, tidy lints * fix(editor): keep restored file order, drop stale close waits, carry files through tab merges - Background arrivals (restore, merge, rescue) append to the strip in order instead of inserting beside the active file, which reversed them. - A cancelled Save As, a dismissed path bar, or a dropped buffer cancels any close that was waiting on that save. - Merging a tab into another carries its open files along. - A shell exiting closes its tab without a prompt it could not honour; unsaved buffers move to the tab in front. - Tabs rebuilt under the same id (server restart) restore their files. * fix(host): only fall back to an in-place write when the rename is refused On Windows every failure of the atomic save fell back to fs::write, including a failure while staging the temp file. A full disk would then truncate the original in place, the very loss the temp file prevents. Staging errors now return as-is; only a refused rename (a file held open elsewhere) takes the in-place path. |
||
|
|
fd2c4f7d4e |
feat(panel): Search and GitHub tabs in the right panel (#978)
* feat(panel): add Search and GitHub tabs to the right panel The right panel grows from three tabs to five. Five word labels do not fit the panel's 280px resting width, so the tab row now draws a glyph per tab and names it in a tooltip. Both new panes are placeholders here; the content search and the GitHub issues/PR browser land on top of this. * feat(panel): find in files in the right panel's Search tab The Search tab replaces its placeholder with a content search over the active tab's project -- the same roots the Files tab shows -- on the host that project lives on. Hits arrive as you type (debounced, with a generation counter so a stale answer never lands), grouped by file with a count, each line excerpted with its matches highlighted. Clicking a hit opens the built-in editor at that line and column; Enter searches again. Match-case, whole-word and regex toggles sit at the end of the field, and the tab focuses its field whenever it is brought forward. Host::search_content is new on the Host trait, implemented once in host::content_search (ignore walk + regex) and run by LocalHost directly and by tty7-server over a new SearchContent control request. The walk honours .gitignore with or without a repository, skips dot-entries, binary files and files over 1 MB, and reports a capped search as truncated. The request is gated on a new `content-search` hello feature, so a server that predates it is never sent it; the panel says the server needs updating instead of showing no results. Conformance cases cover local and the stdio server alike. * feat(panel): browse GitHub issues and pull requests in the right panel The GitHub tab follows the focused pane's repository: its root is resolved the way the Source Control tab does, its remotes are read through the Host (the tree may be on another machine), and the github.com remote is bound, upstream over origin in a fork, with a menu to pick another. The list switches between issues and pull requests, open and closed, 50 rows a page with Load more; rows carry a state glyph distinct by shape, labels (click one to filter by it) and relative times. A row opens the detail in place: title, state, author, labels, description and comments as Markdown, and for a pull request its branches, size and changed files. A file opens in the diff overlay through a new supplied-patch DiffSource, so GitHub's patch renders exactly like a local one without a git probe. Read-only, and sign-in reuses the GitHub CLI: GH_TOKEN, GITHUB_TOKEN, then `gh auth token`, found on PATH or at the Homebrew locations a Finder launch cannot see. Signed out, public repositories still work; 401, 403, 404 and rate limits are told apart and explained. Requests go out from this machine over the installer's ureq stack and proxy settings, on threads of their own, cached per repository with background revalidation. Remote images in issue text become links instead of loading, and non-web link targets are disarmed. The Info tab gains a GitHub row that opens the branch on the remote it tracks, or the repository for a branch never pushed. * docs: list ShowRightPanelGitHub with the other panel actions * feat(panel): one-line GitHub rows, a pill for the current tab - GitHub list rows are one line: state glyph, #number, title. Labels and the age of the last update appear on hover, from state rather than a group_hover display switch, which gpui cannot paint. - The current right panel tab sits on the sidebar's selected fill; ink alone could not tell five same-weight glyphs apart. - The GitHub glyph is a 1.8px outline like the other tab icons, not the filled mark. - The detail byline names both times (opened / updated) so it no longer reads as disagreeing with the list's update age. * feat(github): show screenshots pasted into issues Images GitHub hosts itself (github.com/user-attachments, a repo's /assets, *.githubusercontent.com) now render in issue and PR text, each in a paragraph of its own so the text view draws it at its size rather than at line height. Images from any other host stay links, so opening an issue still tells no third party that you read it. gpui held a null HTTP client, so no remote image could load; the app now installs the update check's reqwest client (same user agent and proxy) at launch. * fix(github): load private-repo screenshots, give inline code a neutral fill - Pasted attachments (github.com/user-attachments/assets/<uuid>) want a browser session on a private repository, which an API token is not. The detail and comment requests now ask for the full media type, and each attachment is swapped for the signed private-user-images URL the rendered body_html carries for the same uuid. - Inline code in rendered Markdown (the GitHub tab and the editor's preview) sits on a faint neutral fill instead of the theme accent, which is also the selection colour. Needs gpui-component 6af19d91 for TextViewStyle::inline_code_background. * style(panel): tidy the GitHub and Search tabs' top rows - GitHub drops its heading row on macOS. It existed only to hold the refresh tile, and no other tab has one; refresh now sits with the repository's other actions, in the repo row and a detail's header. - Search's Aa / ab / .* toggles are muted while off instead of body ink. - Search's idle note puts the folder on its own line, spelled ~/…, so the narrow column no longer breaks the path at a slash. * feat(panel): order the right panel's tabs Info, Files, Search, Changes, GitHub Info stays first as the default and the pane's overview; after it come two pairs, the project's files (Files, Search) and its version control from local to remote (Changes, GitHub), where Changes and GitHub were split by the file tabs before. The palette, the Keybindings list and the docs follow the same order. * style(panel): drop the change count from the Changes tab Beside one glyph of five, the number read as a badge on that tab alone, and the Changes tab already leads with the same count under its own heading. right_panel_tabs no longer needs the row's width, which it only measured to decide whether the count fit. * style(icons): fit the GitHub glyph to the other tab icons' size The Lucide mark filled its whole 24px box, edge to edge, where tty7's own icons keep about 3.5px clear, so at 15px it drew a size larger than the four tabs beside it. Scale it to 0.9 about the centre, and raise the stroke to 2.0 so it still renders at the others' 1.8. * style(icons): a simpler GitHub glyph Drop the Lucide mark's tail and redraw the head and legs on tty7's own grid: the same ~15px live area and 1.8 stroke as the other tab icons, no scale transform. The legs keep it reading as the Octocat; a head alone read as any cat. * test(github): find gh on PATH in the blank-variable token test The test placed gh only at /opt/homebrew/bin/gh, which gh_candidates never offers on Windows, so the Windows CI job panicked at unwrap. Put gh on a PATH directory spelled with the platform's exe name instead. * fix(github): close image and link bypasses in the issue Markdown sanitiser Checked against markdown-rs (the parser TextView uses), several inputs got past the line-based rewrite: - is_github_hosted cut the host only at `/`, so `https://evil.io?.githubusercontent.com/x.png` (and `#`, `\`, `/`) counted as GitHub-hosted and was fetched from evil.io. The host now ends at the first of `/?#\` and may hold only DNS characters. - `<img src>` values were written into `` unescaped, so a `)` in the value closed the image and opened a second one from any host. Written destinations are now percent-encoded. - `<image>` (which the HTML parser reads as `<img>`) passed as an ordinary tag and loaded its src. - A kept link target was copied without scanning; when the parser ended the link elsewhere (open title, unbalanced paren) a `` inside it came alive. Markup characters in it are now encoded. - `file:///...` and similar character references passed is_safe_target and decoded to a `file:` link. References are decoded before judging. The rewrite still cannot see every construct the way the parser does (code spans inside tag attributes, fences the parser rejects, multi-line link definitions), so the detail view now also checks the parsed tree: a block containing a non-GitHub image, an unsafe link or definition, or raw `<img>` is drawn as its plain source instead. * fix(github): hide gh's console, bound Retry-After, and reject URL authorities with ?#\ - run gh through proc::output_within with hide_console, so a Windows GUI launch does not flash a console window and stdout is drained while gh runs. - saturating_add a hostile Retry-After instead of overflowing i64. - parse_github_url no longer accepts `https://evil.io#@github.com/o/r`. * fix(search): no panic on an unbounded time budget, and read files through the size cap ContentLimits arrive off the wire on a server; Instant + u64::MAX ms panicked. A file that grew between the size check and the read was read whole; it is now read through a take() at the cap. * fix(panel): keep Load more on an empty filtered page, and drop another host's hits - /issues pages filtered to one kind can come back empty while later pages hold matches; the GitHub list said "No issues" and hid Load more. It now reads on through up to five such pages and keeps Load more offered. - While a new search runs, the previous hits stay on screen; if they came from another host, a click opened their path on the active host. They are now kept only when the host is the same. |
||
|
|
572bfc014b |
feat(ui): v4 redesign, including a rebuilt settings window (#973)
* feat(ui): restyle the right panel after the v4 design - Tab row: 12.5/16rem word tabs 22px in and 18px apart, the current one in body ink at medium weight; no hover pill, no underline bar, no hairline under the row. - Info: Session, Processes and Ports are spaced 16px apart with no rules; 28px medium muted headings, 28px Session rows on a 76px label floor with values in body ink, 26px process rows with a tree elbow for children, and an explicit empty line for Ports. - Files: the search sits in a 28px filled well; tree rows are 26px with a disclosure chevron column, ignored entries dim their icon instead of going italic, and a folder's change dot is 5px. - docs/design-system.md updated to match. * feat(switcher): restyle the workspace switcher after the v4 design - Card: 112px from the top, 12px corners, 48px search row with an esc keycap, 420px body split 340px / preview, 40px footer. - Workspace rows are 52px: a 26px initial disc carrying the link state as a ringed dot (live green, faint when offline, amber while connecting, red on failure), a medium name with its stable number, a machine · path · time line, and the tab count over the state word. - Preview rows are 44px with the sidebar's 18px brand disc, an all-muted branch · diff line, a Current label and a 5px dot that blinks with the sidebar while an agent is working. - Footer: ghost New workspace button and keycap hints for navigate, open and new window; the unused click-for-new-window string is dropped. * feat(scm): restyle the Changes tab after the v4 design - Pinned block keeps 8/10/14 rhythm; branch name medium, 26px sync tile. - Commit message box rests at 56px with a 7px radius. - Split commit control: inverted neutral fill when committable, faint fill otherwise; 6px radius and an inset 0.5px seam. - Change groups sit 16px apart under 22px sentence-case medium headers; file names take width first and directories right-align, eliding from the start. - History: 32px header, 26px rows inset with rounded hover, 1px lines and 7px beads (HEAD filled, others hollow), neutral inks on a single-lane page, age column always shown, faint HEAD pill. * feat(ui): restyle the rail and palette after the v4 design - Default Light/Dark take warm neutrals (#fcfcfb/#1c1c1e, #18181a/#ececed); Git added/modified seeds follow v4 green and amber. - The left rail gets its own tinted fill again (Neutrals.rail, 3% toward the ink) with its own surface ladder; the right panel keeps the content fill. Captions and hairlines are floored on the rail too. - Title bar is 48px; the bar over the terminal centres the active tab's title in caption ink when tabs live in the rail. - Rail header: 26px new-tab and collapse tiles, then the workspace chip and search field (28px, 7px radius). - Groups sit 16px apart under a 22px caption heading with 'branch · +a −d' in tabular numerals. - Rows are 30px (42px with a branch line), 16px avatars, medium weight when current, branch cut from the front, and a trailing 5px status dot (blinks while working, hollow while waiting, unread count as a pill). - docs/design-system.md updated. * docs(design-system): note the commit button's inverted neutral fill * fix(panel): align the right panel's insets with the v4 design - Rows pad 8px inside lists inset 12px, so text sits on a 20px column in every tab and hover fills start 12px in with a 6px radius. Headings, empty states and the Ports line move to the same column. - Tab labels 18px apart; the panel row's chrome tiles are 26px, 4px apart, 12px from the edge. Default panel width 280. - Info: label column floor keeps values at x=88; Ports add tile 22px. - Changes: 8px top gap on macOS, pinned block on 14px edges with the branch at 22, 12px sync glyph, 8px group chevron, 10px status cell, 1px between rows. - History: compact gutter for single-lane pages, filtered rows on the text column, 10px row gap, 24px age floor, header on 20px insets, 4/12 padding when expanded (heights re-counted in commits). - Files: search well at 12px with an 11px glyph, 10px before the tree, 16px indent step, 16px bottom padding. * feat(diff): restyle the diff overlay and commit detail after the v4 design Carry the v4 language into the diff overlay and the commit detail view: 0.5px hairlines at 8% ink, 26px row pills with a 6px radius, the rem type ladder from right_panel.rs, neutral chips instead of accent washes, the shared git_badge for status letters, and tabular figures on counts. Layout, spacing, type and colour only; no behaviour or i18n changes. * feat(ui): restyle the dialogs, notices and home page after the v4 design - New ui::dialog module holds the shared modal chrome, taken from the workspace switcher: a 12px card, a 48px title row with an esc keycap, 18px insets, a 40px hairline footer, 28px borderless field wells on the faint fill, 11.5px medium muted labels, and 18px keycaps. - Buttons: the primary is the inverted neutral fill, the Commit button's paint, instead of the accent. Secondary buttons are transparent with the surface's hover rung. Override on a changed host key stays the one red button. A disabled button sinks to the faint fill and drops its click handler. - SSH sheet: host and fingerprint lines sit in a mono detail well, and keyboard-interactive prompts become field labels. Banners match the sheet's width and card shape. - Worktree prompt: moves to the same card, with the path preview hung off the Name field. - Notice pill: severity moves from a tinted edge to a 6px leading dot. - Home: shortcut rows are 28px with a hover fill and keycap chords, and the remote strip's action uses the secondary button. * fix(panel): start Info and Changes flush under the tab row Their first line is text centred in a 28px row, so the extra 8px step put it visibly lower than the Files tab's search well. Only Files keeps it. * fix(scm): put the commit detail on the right panel's 20px text column * feat(palette): restyle the command palette after the v4 design - Card: the switcher's 12px corner, 112px drop from the top (shorter windows still scale it up), 600px max width. - Search row keeps the list's own field; an esc keycap sits in its trailing corner while the field is empty. - Rows are 32px with an 8px corner, 8px list inset and 10px padding. The keyboard row takes the popover's neutral selected step and a medium title instead of the accent wash, via a palette row element in place of ListItem. - Section headings: 28px, 11.5/16rem medium caption ink, on the rows' text column. Shortcuts are per-key 18px faint keycaps from ui::dialog. - New 40px footer with the switcher's keycap hints (navigate, open). - Empty state: headline in body ink, hint in caption ink. * feat(ui): carry the v4 chrome into panes, the file viewer and SFTP - theme: additive helpers for a device-pixel hairline, tabular figures and an inverted neutral button variant. - Pane splits rest as a device-pixel hairline in the divider tone; hover and drag keep the accent at 1px like the other resize edges. - File viewer header: medium file name, 5px unsaved dot, 26px/6px close tile with its glyph on the content inset, divider hairline under it. Status bar: divider hairline, caption size, tabular line/column. - SFTP browser: file-tree rows (26px, 6px corner, 16px caption glyphs), breadcrumb and notes on the 20px text column, a borderless edit well, inverted OK button, and ink-on-track transfer progress. - Forward rows line up with the process and port rows (text at 20px, 6px corner); Add and Reconnect use the inverted neutral fill. * docs(design-system): note the v4 palette, pane, viewer and SFTP chrome * feat(settings): restyle the settings page after the v4 design - Nav: the rail's tinted fill and surface ladder behind a divider hairline; a 28px filled search well; 28px rows in 7px pills, the current one on the selected rung at medium weight instead of the accent; match counts in muted ink; the modified-only filter toggles like a nav row. - Pages: the title sits in the 48px title-bar band at 16/16rem; group headings are 11.5/16rem medium muted on a 28px line; sections are split by a 0.5px divider with 16px either side. - Rows: labels in body ink at regular weight, descriptions at 12/16rem muted, 28px floor with 8px padding; a search hit wears the faint neutral fill rather than the accent tint. - Controls: text fields and dropdowns are 28px filled pills with no outline; buttons, segmented tracks and steppers are 26px with a 6px radius on the same fill. The one primary action per view (save theme draft, connect, install update) is the inverted neutral fill of the commit button. Switches and sliders keep the accent. - SSH: host list header with 26px tiles and a filled search, 22px group headings, 42px two-line host rows; the form's labels are a muted, right-aligned column level with 28px fields; disclosure headers use a chevron on a 28px band. - Theme cards are filled and unoutlined, taking the selected rung while open; the theme panel keeps the content fill with a divider edge and its title in the title-bar band. Keycaps are filled with no outline and shortcut rows are divided by 0.5px hairlines. - right_panel::SECTION_GAP is now shared; docs/design-system.md updated. * feat(ui): spell tab titles out in full in the rail and title bar The rail's rows and the centred title have room to spare, so they take the whole label from a new full_tab_label rather than tab_label's three-segment cut; only the width they have decides what gets elided. * fix(settings): even out the page rhythm and line up the columns - Nav header: drop the min_h(ROW_H)/min_h(0) pair on the heading, which measured ~46pt taller than it painted and opened a hole under the search. - Page titles sit under the title-bar band, level with the nav heading, instead of jammed against the window's top edge. - Headings get a 22pt group-header row and hug their rows; rules keep more air, so a heading reads as its rows' rather than floating between. - SSH: the host list gives width before the nav, so the nav no longer narrows on that page; its header, search well and detail title run level with the nav's; the empty note starts on the host-title column. - Window & Tabs no longer opens on a stray rule. - Integrations: status leads the buttons on one line, in the meta ink. - Terminal: the shell footnote stays close to its rows. * fix(ui): stop eliding branches that fit, and seat the SCM branch on the text column - elide_tail_clusters returned "…" plus the whole string when nothing needed cutting, so the rail's group header printed …feat/v4-redesign with room to spare. Return the text as-is when it fits. - The group header only reserves the chevron's width when it draws one. - The Changes tab's branch name no longer stacks a small button's padding on the row gap; it starts on the file names' column. * fix(ui): keep a tab's name in place when an inline rename starts gpui-component's Input keeps 12px of inner padding even with appearance(false), so the name jumped sideways as the rail row, the group header and the top-strip chip swapped their label for the field. Claude-Session: https://claude.ai/code/session_01Q9vsQSxAZjkwT7nRAiFF1J * revert(settings): restore the page rhythm from before |
||
|
|
d843b82286 | chore(release): v26.9.3 | ||
|
|
592058acc7 |
feat(fonts): make macOS stroke thickening configurable (#720)
Add a macOS-only `font_thicken` key (default true) and a Settings row under Appearance > Terminal text. When off, AppleFontSmoothing is pinned to 0 in this process's NSArgumentDomain before gpui's text system first reads it, so glyphs render at the face's own weight. The volatile domain is in-memory only: nothing is persisted and no other app is affected. gpui caches the preference in a OnceLock, so a change applies after a restart; no gpui fork change is needed. |
||
|
|
6aa83b4bd9 | chore(release): v26.9.2 | ||
|
|
6649cccbc7 |
Merge pull request #822 from l0ng-ai/fix/notification-poll
fix(notify): stop polling Notification Center from the UI thread |
||
|
|
990873f762 |
fix(notify): scope notification identifiers to the process
Include the pid in the `tty7-pane-<pid>-<leaf>-<seq>` identifier so a banner left over from a previous run (or a concurrent instance) is ignored instead of revealing an unrelated pane. Also drop the dead `com.apple.Terminal` fallback in the delegate installer (mac-notification-sys completes its Once even on failure, so the second `set_application` never ran), collapse the macOS cfg arms so test builds compile the production path, and correct stale comments. Claude-Session: https://claude.ai/code/session_01VuYUPiDEhQX6aQ4WQZbEGn |
||
|
|
59dbe83913 |
feat(macos): add default terminal integration (#818)
* feat(macos): add default terminal integration * fix(macos): route external opens through the layout pull Five holes in the LaunchServices path, all on the way from a URL to a tab. The `ssh:` arm handed the raw URL back to `parse_quick_connect`, which reads a bare `user@host:port` typed into Quick Connect. Everything a URL carries past the authority landed in the wrong field: `ssh://h:2200/` parsed its port as `2200/` and was dropped on the floor, `ssh://h/srv` became the host `h/srv`, and the percent escapes `url` was added for were never decoded. Read the authority off the parsed URL instead. `x-man-page://3/printf` is Apple's sectioned form, and taking the host as the page name ran `man 3`, which asks the user what page they wanted. Section and page are now both carried. A window that is pulling its layout is one `Adopt::IfEmpty` will not adopt into, so a tab inserted while the pull is out comes back as the whole workspace — the failure `then_open` already exists to avoid. Both the script/man path and the SSH path inserted straight into a freshly restored window, so `then_open` becomes a list of parked requests and carries a command or an SSH link as well as a folder. A cold `ssh://` link also went through `open_at` directly, claiming a fresh workspace and leaving the restored one detached and unannounced; it takes the shared restore now. `new_tab_running` wrote the command whether or not a tab opened, so a failed spawn typed a script path and a newline into whatever pane was focused before — a shell mid-line, or an agent. Left alone deliberately: an `ssh://` link still connects without a confirmation, which is a product call rather than a defect. Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
c9ec23d090 |
fix(notify): stop polling Notification Center from the UI thread
macOS notifications went through mac-notification-sys with wait_for_click so a click could reveal the pane. That crate notices a click by parking the sending thread and adding, per outstanding notification, a repeating 0.5 s timer on the main run loop that calls deliveredNotifications — a synchronous XPC round trip. A banner nobody clicks stays in Notification Center, so its timer never goes away. Sampled with nine outstanding: a fifth of the UI thread inside that XPC, every window juddering, one more timer per agent turn. Drive NSUserNotificationCenter directly with a delegate of our own: the click arrives through didActivateNotification, the pane rides in the identifier, and nothing runs on the main thread until the user clicks. notify-rust's show is no longer called on macOS, since it is that crate and would replace the delegate; only set_application stays, to name a bare binary. Claude-Session: https://claude.ai/code/session_01VuYUPiDEhQX6aQ4WQZbEGn |
||
|
|
cb710c4d79 |
deps: bump async_zip from 0.0.18 to 0.0.19 (#764)
Also bump the version requirement in Cargo.toml, which dependabot left at 0.0.18 and which made every --locked job fail. |
||
|
|
081e191bb0 |
perf(diff-overlay): draw the patch as a virtualised row list (#799)
The overlay built its whole patch as a nested element tree on every frame: a card per file, a header per hunk, six elements per line. gpui notifies the view on each scroll wheel event, so a few hundred lines of diff rebuilt tens of thousands of elements tens of times a second, and the window stalled. Flatten the tree into one row per line in a new `diff_list` module and draw it with `gpui::list`, which builds only the rows on screen. The rows are rebuilt only when what they are built from changes, so scrolling no longer re-splits hunks or re-clones every line, and a change to one file splices just the rows it touched rather than resetting the list and losing the scroll position. The key that decides a rebuild takes the snapshot each frame was asked about even when it matched only by contents. A probe that finds nothing new still lands a fresh `Arc` over an equal snapshot; a key left pointing at the old one would go on walking the whole patch to prove the two equal, once per wheel event, which is the cost the key exists to avoid. A list counts a row it has not laid out yet as zero tall, which left the scrollbar reading an 800-line patch as one viewport: its thumb filled the track, and a drag from top to bottom travelled 248px and stopped. The rows below the fold are counted at the 19px both views already give a line of a patch, through `ListState::with_size_hint` — added to the gpui fork for this, `Cargo.lock` following its `tty7` branch to `ece710e3`. A card cannot survive that flattening — its rows are separate items now — so the frame it drew is gone, and with it the grey header bars and hunk bands that made the overlay the one view in the app still speaking gpui-component's default container language. The rows take the source control panel's own measurements instead: 26px, 10px inset, 5px radius, colour only under the pointer. The title bar's view switch loses its border for the same reason. |
||
|
|
20b73adb2a | chore(release): v26.9.1 | ||
|
|
37be703d5b | chore(release): v26.9.0 | ||
|
|
46759b8a01 |
fix(input-bar): read column widths from unicode-width, not a hand-rolled table (#704)
* 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. |
||
|
|
b2d73ec68b |
feat(update): update an all-users Windows install through one UAC prompt (#562)
* 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> |
||
|
|
71c6783fb4 | chore(release): v26.8.3 | ||
|
|
44f0683d0a |
fix(sidebar): cut labels on grapheme clusters, not on chars (#450)
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.
|
||
|
|
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> |
||
|
|
e46bcbcf56 |
chore(terminal): drop the client-side command-mark store (#404)
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 |
||
|
|
b4e7add65d | chore(release): v26.8.2 | ||
|
|
51c35aac1f |
fix(terminal): resize ConPTY panes with conhost's semantics, in stream order (#415)
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. |
||
|
|
a7de7db2c4 |
feat(windows,macos): clickable toasts, richer context, and i18n (#373)
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> |
||
|
|
e1531cdea6 |
revert(windows): drop the taskbar status dot (#377)
The per-window taskbar overlay badge (#355, for #199) is removed, and with it the in-flight follow-up that was making its green "finished a turn" state reachable: the feature is not wanted. Nothing shipped — the badge only ever existed in Unreleased — so this is a plain removal rather than a deprecation, and its CHANGELOG entry goes with it instead of gaining a "Removed" counterpart. What goes: `ui::taskbar` and its `ITaskbarList3::SetOverlayIcon` poll, the `taskbar_status_icon` config flag and its Settings → Window & Tabs row and strings, `Tty7App::taskbar_signals`, `TerminalView::shell_busy` / `RemoteTerminal::shell_busy` (the overlay was their only caller), the `raw-window-handle` dependency and the `Win32_UI_WindowsAndMessaging` feature it needed, and the feature docs in both languages. A stale `taskbar_status_icon` left in someone's `config.json` is ignored, as any unknown key is. The tray badge and the in-window status dots are untouched; they were always the ones the taskbar was mirroring. |
||
|
|
7c0598c7e0 |
fix(bell): ring the system bell on Windows (#359)
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> |
||
|
|
27bb1864df |
feat(windows): taskbar status overlay per window (#355)
* feat(windows): taskbar status overlay per window (#199) Stamp a colored status dot on each window's taskbar button using the same palette as the in-window agent dots: - blue while a shell command or agent is working, - amber when an agent is waiting on the user, - green when work finishes while the window is unfocused (cleared on activation). Adds a `taskbar_status_icon` setting (default on, Windows only) and a Settings -> Window & Tabs row. The overlay is updated by a foreground poll that aggregates agent status and shell busy state across each window's panes, diffing against the current taskbar badge and only calling ITaskbarList3::SetOverlayIcon when the badge changes. Includes unit tests for overlay priority and the done-while-unfocused edge tracking. * fix(taskbar): retry a failed overlay instead of caching it as drawn Four fixes on top of the overlay: - A failed SetOverlayIcon was still recorded in `shown`, so a badge the taskbar never took was remembered as drawn and never retried. Stamp now reports success, and a failure drops the interface so the next tick re-creates it — which is also what an Explorer restart needs. - `create_failed` was a permanent latch: one CoCreateInstance failure killed the badge for the whole process, though Explorer may simply not be up yet when the first window opens. Use the tray's attempts/cooldown backoff instead, which this module otherwise copies. - The overlay's accessibility description was hard-coded English in an app that localizes everything else. Reuse the panel and tray strings. - Render the dot at 32px, not 16. SetOverlayIcon wants 16x16 at 96 dpi, so at 150%/200% scaling the shell upscaled a 16px icon; `tray::icon` already renders at 32 off macOS for the same reason. Also drops the Win32_Graphics_Gdi feature: CreateIcon, DestroyIcon and HICON all live in Win32_UI_WindowsAndMessaging, and the build and the taskbar tests pass without it. Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
603bca171e |
feat(updater): add windows updates and cross-platform nightly support (#330)
* 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>
|
||
|
|
618855cf4a |
fix(windows): brand toast notifications with a tty7 AUMID (#340)
* 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> |
||
|
|
b7e08c7e11 |
feat(windows): add optional windows explorer context menus (#310)
* 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> |
||
|
|
a6754b28bc | feat(update): install verified macOS releases in app | ||
|
|
6fc8bcb2cf | chore(release): v26.8.1 | ||
|
|
74f4f1a35a | chore(release): v26.8.0 | ||
|
|
86eba1e2c2 |
feat(cli): make a captured pane readable, and stop panicking on a closed pipe
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. |
||
|
|
c275960ceb |
feat(cli): ship the CLI in every installer and put it on PATH at launch
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. |
||
|
|
b46183688e |
fix(cli): review findings — CI coverage, kept-pane filing, endpoint and lifecycle honesty
- 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 |
||
|
|
10b6741368 |
refactor: rename the GUI binary to tty7-app, freeing tty7 for the CLI
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
|
||
|
|
bed22d899e |
Keep workspaces whole: remote reopen/restart recovery, and cross-workspace restore guards (#257)
* feat(remote): keep a remote workspace whole across reopens and restarts Reopening a remote workspace — or coming back to one whose `tty7-server` had been replaced — landed on a screen of `tty7 — disconnected` panes with their coding-agent conversations gone. Several independent holes added up to that; this closes them together, and picks up the surrounding work the same session produced. **Telling a restarted server from a blinked link.** `ControlHelloOk` now carries an `instance` minted once per server *process*. Nothing else in the handshake changes across a restart — `build` and both dialect numbers survive it — so a reconnect had no way to know its `pane_id`s were dead. It does now: a different instance rebuilds the window from its layout (same tabs and splits, fresh shells in the saved cwds) instead of re-attaching to a process that is gone. An absent instance means *unknown* and is never read as a restart. **An attach can now fail.** `Attach` has no synchronous reply, so the client returned `Ok` unconditionally and the daemon's `Error` frame was read much later by the reader thread, which has no arm for it — the pane then landed in the *link is down* state instead of falling back to a fresh shell. The client now reads far enough into the reply to classify it on the kind byte (the snapshot behind it can be megabytes) and hands those bytes to the reader thread, so a successful attach loses none of its replay. Local and remote attaches get different waits: the local one is on the UI thread. **The agent session survives to be resumed.** `TerminalView` raises `AgentSessionChanged` when the pane's agent reports a new native session id, so the layout on file catches up instead of waiting for the user to happen to open a tab. A pane that is still connecting now carries its agent through `PendingSpawn` — a save landing in that window used to write `agent: null` over the record — and `land_pane` sends `--resume` when the attach turned out to need a fresh shell. **Ending sessions says so on file.** "End Sessions" kills the panes and then drops their ids from the record, pushing the cleared layout to the machine that owns it (design §10: the remote's copy wins, so a local-only clear would be undone by the next open — the open this exists for). **The new-tab dropdown lists the window's machine.** `Host::shells` and a `Shells` control request (dialect v2) make the "+" menu a property of the machine the window is bound to. A remote window filled from this computer's `/etc/shells` offered `/bin/zsh` on a box whose zsh is elsewhere, and every pick failed to spawn. **An install reports its bytes.** The download and the SFTP upload each report progress, relayed to the client over the routed connection as a `RoutePrompt::InstallProgress`, and painted as a bar under the machine's row in the switcher. ~8 MB across two hops behind the word "connecting…" was indistinguishable from a hang. **The installer compares dialects, not version strings.** `tty7-server --protocol` prints what a binary speaks without starting it, so a connect adopts an already-running server it can talk to rather than prompting about a build difference and uploading 8 MB the machine did not need. **Switcher.** A machine's `⋯` menu holds "New Workspace" (it was a row under every machine, pushing the list a quarter of a card down) and a new "Disconnect", which drops the connection and leaves the windows open and read-only. The suspension lasts exactly as long as that machine has a window on it. Also drops three design/contract docs for the now-shipped remote-workspace work. * fix(session): stop one workspace's panes from being restored into another A restart put a copy of one workspace's seven tabs — cwds, layout and recorded agent sessions — in front of another workspace's own tabs, and auto-resumed every one of those agents a second time: six `claude --resume <id>` pairs running in parallel against the same conversations, one set per window. The record-level corruption that seeded it is still unattributed, but every mechanism that let it propagate, amplify, or go unnoticed is closable, and this closes them. **Panes now know their owner.** `Spawn` can carry the workspace the pane is created for; the daemon stores it immutably and reports it in `List`'s `PaneInfo.owner`. Restore refuses to re-attach a pane another workspace owns (`pane_attachable`) — before this, a saved id landing on somebody else's live pane attached silently, which is how one window could pick up another's shells. The field rides a new `SPAWN_OWNED` frame with a struct payload (the legacy spawn payloads are positional tuples an old daemon cannot grow), gated on a new `pane-owner` feature string: a client only sends it to a daemon that advertises it, so the legacy kinds stay byte-for-byte what old daemons expect. A pane with no recorded owner stays attachable by anyone — that is the pre-field behavior, not a new risk. **Saved pane ids are bound to the daemon process that issued them.** `DaemonVersion` now carries an `instance` minted once per process (the local twin of the control hello's), the GUI caches it at the `ensure_running` handshake, and each local workspace records it as `daemon_instance` beside its layout. Claiming a workspace whose ids came from a different instance blanks them first: daemon pane ids restart from 1, so after a reboot every saved id points at whatever unrelated shell holds the number now, and the aliveness check cannot tell a survivor from a squatter. A blank on either side means "cannot tell" and never trips it. Unlike the duplicate-claim case below, this path keeps the agent resume — the pane is genuinely gone with its daemon, and the fresh shell resuming the conversation is the feature. **A duplicate claim loses its agent resume along with its pane id.** `dedupe_pane_ids` kept the loser's layout *and* its `agent_session_id`, so the blanked leaves took restore's spawn-fresh path and auto-typed `claude --resume` for conversations the winning workspace's panes were still running — the doubling above. The winner keeps the panes and the resume; the loser keeps only cwds. **Cross-workspace saves are caught at the write.** Every terminal view remembers the workspace whose window created it, and `save_session` logs an error naming both ids if a window ever records a pane created for a different workspace — the tripwire for the still-unattributed seed corruption, so a recurrence is caught in the act instead of reconstructed from `session.json` archaeology days later. Wire compatibility both ways: `PaneInfo.owner`, `DaemonVersion.instance` and `Workspace.daemon_instance` are `#[serde(default)]` struct fields (old peers' JSON decodes, new fields are ignored by old readers), and `SPAWN_OWNED` is feature-gated as above. `daemon_instance` is client-owned in the design-§10 storage split — it names the local daemon, and the field-census test pins the classification. * fix(session): resume the agent when a local pane dies mid-restore `session_to_pane` decided whether to send a coding agent's `--resume` from `restore.is_none()` — i.e. from whether the pane looked alive when the restore started. But `alive_panes_on` runs one `List` at the top of the restore, while the attaches happen per leaf afterwards. A pane that exited in between failed its attach, fell back to a fresh shell inside `spawn_shell_terminal_in`, and then landed in the `restore.is_some()` arm: an empty shell with its conversation dropped. `ShellParts.restored` already answers this exactly, and the remote path already reads it in `land_pane`. Carry it onto `TerminalView` so the synchronous local path can read it too, and branch on that instead of re-deriving the answer from a set that may be stale by the time it is used. No behaviour change on the paths that were already correct: a view that was never restoring anything reports `restored: false`, which is the same answer `restore.is_none()` gave them. * fix(remote): check the server instance against the record, not just memory A remote workspace's pane ids were only guarded against server restarts by `RemoteLinks::instances`, an in-memory map. On the first connect after the client starts, every machine is a first sighting, so `server_restarted` answers false — and a `tty7-server` that was replaced while the client was closed sails straight through. Its pane ids restart from 1, so the saved ones now name unrelated shells, and the reconnect attaches to them: the exact id-reuse failure the local side already guards against. `Workspace::daemon_instance` was local-only for the stated reason that a remote server's identity is tracked live per connection. That tracking is correct but not sufficient — it cannot survive the client restart that makes the question worth asking. So the field now means the same thing on both sides: which process minted the pane ids in this record. `WorkspaceStore::serving_instance` picks the local daemon or the far machine's server depending on the workspace, and `finish_attempt` compares it per workspace before deciding to re-attach or rebuild. It stays client-owned: it records what *this* client last saw, so two clients on one remote workspace each keep their own and neither may overwrite the other's. An unreachable machine still records nothing, which is what keeps a good stamp from being erased with `None` — that would disarm the next check. Also in these three files: the §N references to the deleted design docs, cleaned up as part of the sweep in the following commit. * docs: drop the references to the deleted design documents The three documents this branch removed were cited ~280 times: `design §10`, `contract §8`, `§17` and friends in comments, five references by file path in code and manifests, five in CI workflows and one in the release skill. Every one of them now points at nothing. Rewritten rather than merely stripped, because most were not decoration: "design §10 makes the remote's `workspaces.json` the authority" becomes a statement in its own right, and the several that carried a Chinese phrase from the document as their justification say the same thing in English instead. Where the reference was purely parenthetical it is simply gone. Not touched: `PRD §7.1`, `brief §8` and the like, which name documents this branch did not remove and were already external before it, and the `RFC 4648 §10` test-vector citation, which is a real specification. The `host boundary` CI job loses `(§10.6)` from its name. It is not one of the required checks, so branch protection is unaffected. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
7194236985 |
fix(terminal): prevent fullwidth punctuation overlap and speed OSC mark scanning (#250)
* fix(terminal): stop wide glyphs overlapping after fullwidth punctuation gpui's apply_force_width_to_layout tells a base glyph from a zero-advance combining mark by whether the shaped x advanced past half the forced width, and CJK fullwidth punctuation fails that test (U+FF08 advances ~0.47 em against a 0.6 em half-slot). In a batched wide run the glyph after such a character was classified as a mark and painted on top of it. Shape each wide glyph on its own line instead: the first glyph of a line is unconditionally a base, so the heuristic never misfires. * perf(terminal): intern wide-segment strings via char_string Each wide glyph now shapes alone, so its text is a single-char string — reuse the char_string memo instead of allocating a fresh String per cell per frame. The interned SharedString is also what keys gpui's line layout cache, so a CJK-dense repaint allocates nothing. * perf(terminal): skip MarkScanner's Text state ahead with SIMD memchr The scanner runs over every batch the client receives, and ordinary output — where the only byte that matters is ESC — dominates each one. Skip to the next ESC with memchr instead of stepping per byte, exactly as tty7-core's OscTokenizer already does: measured on an 8 MB batch of plausible output, 1.6 GB/s became 8.3 GB/s. Declare memchr for the root crate — it left with the OSC tokenizer's move down to tty7-core, and this is the first use since. * fix(terminal): advance segment_row past each wide glyph The unbatching change dropped the `col += 2` along with the batching loop it lived in, so the wide-glyph arm pushed its segment and looped on the same column forever, growing `segs` until allocation failed — the 6 GiB abort on the Windows CI runner, and a machine-freezing memory climb under a local `cargo test`. --------- Co-authored-by: lizhi <lizhi20@xiaomi.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
c469e10312 |
Merge remote-tracking branch 'origin/main' into feat/remote-workspace
# Conflicts: # Cargo.lock # Cargo.toml # src/core/config.rs # src/ui/pane.rs |
||
|
|
1996f2ae12 | chore(release): v26.7.6 | ||
|
|
5cf767c9d9 |
chore: finish the resvg dedupe — bump gpui-component to 0.47
resvg 0.47 landed in tty7 (#227) and the gpui fork (#237), but gpui-component still declared its own resvg = 0.45.1, keeping a legacy resvg/usvg/tiny-skia 0.45/0.11 stack in the tree. The fork now pins 0.47 (l0ng-ai/gpui-component@2264ff99 — no source changes needed; its only resvg user, the Windows native-menu rasterizer, uses APIs unchanged across the bump), so this moves the pin and drops the last duplicate: the lockfile now carries a single resvg/usvg/tiny-skia stack at 0.47/0.12, and `cargo tree -i resvg@0.45.1` matches nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c04f0ab8f6 |
chore: dedupe the resvg stack with the gpui fork
PR #227 bumped tty7's direct resvg to 0.47 while the gpui fork still pinned 0.45, so the tree compiled two resvg/usvg/tiny-skia stacks. The fork's tty7 branch now carries resvg 0.47 (l0ng-ai/zed@3aac3ef); move the gpui pin there so gpui's SVG renderer and tty7's tray-icon rasterizer share one 0.47 stack again. gpui-component still declares its own resvg 0.45.1 (semver-incompatible with 0.47), so one legacy 0.45 stack remains until that fork catches up - noted in the manifest comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2c18d6b7f4 | Merge origin/main into dependabot/cargo/sha2-0.11.0 | ||
|
|
2794be9ff3 | Merge remote-tracking branch 'origin/main' into dependabot/cargo/resvg-0.47.0 | ||
|
|
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`.
|
||
|
|
99f40122f8 |
deps: align resvg manifest requirement with the 0.47 lockfile bump
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> |
||
|
|
e36717a19e |
deps: align Cargo.toml sha2 requirement with 0.11 lockfile bump
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> |
||
|
|
0be3b67640 |
fix(render): stop italic CJK rendering as unrelated CJK on Windows
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> |
||
|
|
0030b98faa | chore(release): v26.7.5 | ||
|
|
f5b4a65ec4 |
fix(terminal): render emoji presentation sequences at their real width
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. |