Commit Graph
122 Commits
Author SHA1 Message Date
l0ng-ai ee817bae01 feat(ui): v5 modal cards, and an IME crash fix in the code editor (#993)
* fix(deps): gpui-component keeps highlight boundaries on char boundaries

Typing Chinese through the IME in the code editor could panic the app:
after a sync parse ran past its 2ms budget, the stale injection layers
put a style boundary inside the committed glyph and the text system split
the line mid-character. Debug builds miss the budget often enough to hit
it; the fork now snaps every style range to the current text.

* feat(ui): bring the modal cards onto the v5 design

- Confirmations take v5's alert shape: title and detail as one
  paragraph, answers beneath, no header row or footer rule, 360px wide
  and set lower, at eye height.
- Dialog buttons take the field's 7px corner and 14px padding; a
  secondary answer rests on the well's faint fill instead of bare text.
- The scrim dims rather than blacks out: 45% dark, 14% light.
- The New Workspace form joins the other cards: 12px corner, borderless
  wells for its fields, an esc cap and the shared 40px footer.
2026-09-28 13:35:53 +08:00
l0ng-ai f647a19746 feat(editor): draw the editor's right-click menus like the rest of the window (#992)
* feat(editor): draw the editor's right-click menus like the rest of the window

The text's menu was gpui-component's native OS menu: another font and
material, no shortcuts, and Go to Definition / Show Code Actions that no
language server ever enables. It is now a PopupMenu with Attach to Agent
(the selected lines ride along as #L3-9), Undo/Redo, the clipboard,
Find, Go to Line, and the file's own items: Open in Browser for web
files or Open with Default App, Reveal, Copy Path, Copy Relative Path.

The file tabs get a menu too: Close, Close Others, Close to the Right,
and the same file items. Closing several asks once about unsaved edits.

* chore(deps): gpui-component moves the caret on right-click under a host menu
2026-09-28 13:35:15 +08:00
l0ng-ai bc08fc49f4 fix(deps): gpui-component paints text selection under the glyphs (#991) 2026-09-28 11:41:11 +08:00
l0ng-ai 23e4ea79f5 chore(deps): gpui-component with v5 menus
Picks up l0ng-ai/gpui-component bce0d8ec: popup and context menus take
the v5 popover look — a neutral hover step, 28px rows, a hairline ring
and v5's long shadow.

Claude-Session: https://claude.ai/code/session_01Aeskbok7Ah7dvKWnaaJFEg
2026-09-28 08:45:02 +08:00
l0ng-ai 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.
2026-09-28 00:32:18 +08:00
l0ng-ai 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 `#`, `\`, `&#47;`)
  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&#58;///...` 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.
2026-09-27 19:04:42 +08:00
l0ng-ai ed939bbc26 feat(search): browse every agent's past sessions, locally and on remote workspaces (#977)
* feat(search): list more agents' past sessions, and fork, copy or hide one

The Sessions tab listed Claude Code and Codex only, and a row could only
be resumed.

- Past sessions of Gemini CLI, Qwen Code, Pi, Oh My Pi, Kimi Code,
  Copilot CLI, Droid, Qoder CLI and CodeBuddy are read from where each
  keeps them (honouring QWEN_HOME, COPILOT_HOME, KIMI_CODE_HOME, …), with
  the name the agent or user gave the session, else the first prompt.
  Context blocks agents prepend (<system-reminder> and kin) no longer
  hide a prompt.
- Cmd/Ctrl-E on a session row opens its actions: Resume, Fork Session
  (agents that can fork, with the configured launch flags), Copy Session
  ID, and Remove from List. Removing keeps the search open, drops the row
  at once and remembers it in `hidden_agent_sessions`; the agent's own
  history is not touched.

OpenCode and Cursor keep sessions in SQLite and are not read yet.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM

* feat(search): list remote workspaces' sessions, and OpenCode and Cursor

The Sessions tab only ever read this computer, and left out the two
agents that keep their history in SQLite.

- agent_history moves into tty7-core, and the scan goes through the
  Host: a local workspace reads this machine in-process, a remote one
  asks its server (new ControlRequest::AgentSessions; CONTROL_VERSION
  11 -> 12, so each remote host takes one Update Server). Resumed or
  forked sessions open on that machine, in the directory they ran in.
  The last answer is kept per host so the tab does not open empty.
- OpenCode: top-level, unarchived sessions from opencode*.db (or
  $OPENCODE_DB); a placeholder title gives way to the first prompt.
- Cursor CLI: chats under ~/.cursor/chats (or $CURSOR_CONFIG_DIR), named
  from meta.json or store.db. Cursor files a chat only under the md5 of
  its directory, so it is placed by matching open tabs' and other
  sessions' directories; chats nothing matches are left out, since they
  could not be resumed anywhere.
- SQLite is bundled (rusqlite), opened read-only, falling back to an
  immutable read when the writer's WAL cannot be shared.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM

* test(search): wait for the real scan before seeding sessions

The search's own scan runs on a real thread. On CI it landed after the
test seeded its rows and replaced them, so the edit gesture found no
row. The test now waits for the scan first. Assertion messages no
longer print session ids (CodeQL rust/cleartext-logging).

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM

* fix(search): harden the past-session scan and note it in the changelog

- Honour CLAUDE_CONFIG_DIR for Claude Code's projects, as the hooks
  installer already does.
- Read a Codex rollout's head as bytes: the 2 MiB cap can split a
  multi-byte character, and read_line then dropped the whole session.
- Escape `%` and `#` (not only `?`) in the immutable SQLite URI fallback,
  so a database under such a directory still opens.
- Refuse a session id starting with `-`: ids now come from file and
  directory names on disk, and one would be read as a flag by the
  resume or fork command.
- Update the unreleased Sessions changelog entry for the new agents,
  remote workspaces, the Cmd-E actions and the v12 dialect bump.

* fix(search): spell the immutable SQLite fallback as file:///C:/ on Windows

SQLite reads file:C:/x as a relative path, so the fallback open (and its
test) failed on Windows. An empty authority and a leading slash name the
file on every platform.

* test(search): keep ? out of the fixture directory name on Windows

Windows file names cannot hold a '?', so the fixture's create_dir_all
failed there before the fallback was ever opened.
2026-09-27 18:18:05 +08:00
l0ng-ai 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 08497d57

The title in the title-bar band, full-row headings, SECTION_GAP rules and
the shell footnote's spacing read better than the tightened version. The
bug fixes from that commit stay: the nav gap under the search, the stray
rule on Window & Tabs, the SSH column alignment and nav width, and the
one-line Integrations rows.

Claude-Session: https://claude.ai/code/session_01Q9vsQSxAZjkwT7nRAiFF1J

* revert(settings): restore the pre-v4 settings layout, on the rail's fill

The v4 restyle (541a887a) and the follow-ups crowded the page. Bring
settings.rs back to main's layout and give its sidebar the main window's
tab-rail fill, so the two sidebars read as one surface.

Claude-Session: https://claude.ai/code/session_01Q9vsQSxAZjkwT7nRAiFF1J

* feat(settings): rebuild the settings window after the v4 design

Rewrites the settings page to the Settings design: a sidebar with search,
per-page modified counts and a "Modified only" switch; quiet grouped rows
with an inline Reset; and the design's own controls (switch, segmented,
stepper, slider, text field, dropdown and popover menus) in a new
`settings/kit.rs`.

- Appearance: Light / Dark / System cards and a theme menu per slot with a
  live preview, search, keyboard navigation and swatches. Font menus are
  searchable and draw each family in itself.
- Keyboard shortcuts: back link, search, "Restore N changed", Default/tmux.
  A recorded chord another action already has now asks Replace / Cancel
  instead of taking it over silently.
- SSH: one column of recent hosts, "Show all" by source, search; details and
  a six-field editor open in place. Auth, jump/proxy, forwarding and
  advanced sections are no longer shown; saved values are kept.
- Integrations: machine menu, agent search, install summary, agent icons,
  and a per-row menu (Reinstall, Reveal hook file, Uninstall).
- General gains startup and restore; updates and the server move to About.
- Search results group live rows by page; a Modified view lists changes.

The page code moves out of settings.rs into src/ui/settings/.

* fix(ui): lay truncating names out at their full width

Moves the gpui fork to 5d366e6, which stops a size measured under
truncation from answering the later whole-text measure. Before it, a
truncating name beside other content in a flex_1 column read as just its
ellipsis with the whole column free. Adds a switcher test that fails
without the fork change.

* fix(scm): seat the History chevron on the change groups' column

The History header now draws its chevron in the change groups' own box
and size, so its title starts where Staged Changes and Untracked do.
Folded, the header drops to 24px with even padding instead of the
expanded section's taller band.

* chore: ignore local design mockups and Impeccable state

* fix(macos): show enter and tab shortcuts correctly in menus

Moves the gpui fork to 5c390b9, which maps enter and tab to their native
key equivalents. A menu item bound to secondary-enter, like
ToggleFullscreen, read as ⌘E.
2026-09-27 09:48:25 +08:00
l0ng-ai d843b82286 chore(release): v26.9.3 2026-09-23 19:21:12 +08:00
l0ng-ai 6aa83b4bd9 chore(release): v26.9.2 2026-09-10 11:50:47 +08:00
l0ng-ai 6649cccbc7 Merge pull request #822 from l0ng-ai/fix/notification-poll
fix(notify): stop polling Notification Center from the UI thread
2026-09-09 17:35:45 +08:00
ayamirandl0ng-ai 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>
2026-09-09 17:05:58 +08:00
l0ng-ai 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
2026-09-09 16:50:42 +08:00
l0ng-ai 644945d137 style(ui): flatten the inline controls and give tooltips a real shortcut slot (#803)
* style(theme): drop the lift under every inline control

Theme::shadow gates exactly one thing -- the shadow_xs an inline control
(button, input, select trigger, checkbox, radio, slider knob) paints under
itself -- and never the drop shadow on a menu, tooltip or popover, which each
draw theirs unconditionally. Left on, every field and button in the window
carried a faint lift that nothing else here has: this chrome separates surfaces
with low-contrast fills and hairlines, so a control sitting a millimetre above
the panel was the one place claiming depth. Panels that really do float keep
their shadow.

Also bumps the gpui-component pin, and records why the switch and slider keep
their accent: both were tried on the neutral ramp the segmented controls use,
and a dark-grey "on" against a light-grey "off" turned out not to be a large
enough step to read while scanning a column of rows.

* style(tooltip): render a chrome tile's chord as a chord

chord_hint pasted a label and its shortcut into one string -- "Hide sidebar
<cmd>B" -- and handed that to Button::tooltip. Inside the card the chord then
wore the label's own size and colour, so the tooltip read as one odd sentence
rather than as a name with a shortcut beside it.

Tooltip already has a key_binding slot that sets a chord apart on the right, a
size down, in muted_foreground. What was missing was a way to hand Button a
built tooltip instead of a string; gpui-component grew tooltip_element for
that. chord_hint becomes chord_tooltip, and key_hint gains a key_stroke sibling
so a caller can reach the Keystroke rather than only its formatted text.

* style(settings): one field width, and a chevron that is not a patch

The right-hand column had three widths, each picked where it was written: text
fields 260, sliders 240, dropdowns 180. Every row still ended on the same right
edge, so on one page the difference read as controls aligned carelessly rather
than as controls of different kinds -- and moving between Appearance and
Terminal, where the mix differs, the column visibly changed width. FIELD_W is
the one number now, at 260, the widest of the three because it is the one with
a requirement behind it: a font name or a shell path has to fit untruncated.

The Program row's shell picker was a ghost button, which fills a rounded
rectangle while its menu is open, sized by hit_target to the 24px
accessibility floor -- exactly the field's inner height, so that fill met the
border top and bottom and looked like a patch stuck over the field's right end.
It now draws with no fill in any state, the way Select draws its own chevron.
Its menu was min_w(200) anchored TopRight on a chevron that sits inside the
field, so it hung off the field's right half with its left edge 110px in from
the field's own; it is now as wide as the field it drops out of.
2026-09-08 08:34:13 +08:00
l0ng-ai d16746a9af fix(deps): restore the lockfile edges #799's merge walked back (#802)
The merge for #799 re-resolved Cargo.lock and pointed twenty consumers at
older copies of dependencies that were already in the tree for other
crates. No `version =` line moved, so the change is invisible to the usual
scan of a lockfile diff, but the graph regressed:

  * 15 crates off `windows-sys 0.61.2` onto `0.60.2`
    (anstyle-query, anstyle-wincon, dirs-sys, errno, miow, muda,
     nu-ansi-term, quinn-udp, rustix, socket2, stacker, tempfile,
     tray-icon, uds_windows, winreg)
  * `winapi-util` off `windows-sys 0.61.2` all the way onto `0.48.0`
  * `gpu-allocator` off `windows 0.62.2` onto `0.58.0`
  * `iana-time-zone` off `windows-core 0.62.2` onto `0.58.0`
  * `dlib` off `libloading 0.8.9` onto `0.7.4`
  * `bindgen` off `itertools 0.13.0` onto `0.11.0`

Nothing in that PR asked for it. Its only dependency change was the gpui
fork rev, and the range it moved over touches one file in
`crates/gpui/src/elements/list.rs` and no manifest, so the resolution was
incidental to the merge rather than required by it.

This points those twenty edges back at the versions they held before, which
is what a fresh resolve picks. Every version already present in the lock
stays present: `windows-sys 0.60.2` is still there for `notify 8.2.0`,
which pins `^0.60.1`, and the older `windows`/`windows-core`/`libloading`/
`itertools` copies still serve their own consumers. So this drops no
duplicate builds; it only stops the newer copies from being compiled
alongside older ones for crates that had already moved on.

Lockfile only. No manifest and no source changes, and `cargo metadata
--locked` accepts the result without wanting to rewrite it.
2026-09-08 08:18:26 +08:00
dependabot[bot] 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.
2026-09-07 22:23:28 +08:00
l0ng-ai 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.
2026-09-07 22:13:57 +08:00
dependabot[bot] 6474e25a24 deps: bump the cargo-minor-patch group across 1 directory with 3 updates (#776) 2026-09-07 22:09:19 +08:00
l0ng-ai 20b73adb2a chore(release): v26.9.1 2026-09-07 21:00:55 +08:00
l0ng-ai 37be703d5b chore(release): v26.9.0 2026-09-04 17:42:08 +08:00
dependabot[bot] 436e9c4320 deps: bump the cargo-minor-patch group with 2 updates (#724)
Bumps the cargo-minor-patch group with 2 updates: [uuid](https://github.com/uuid-rs/uuid) and [ureq](https://github.com/algesten/ureq).


Updates `uuid` from 1.24.0 to 1.24.1
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.24.0...v1.24.1)

Updates `ureq` from 3.3.0 to 3.4.0
- [Changelog](https://github.com/algesten/ureq/blob/main/CHANGELOG.md)
- [Commits](https://github.com/algesten/ureq/compare/3.3.0...3.4.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.24.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: ureq
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-25 16:09:02 +08:00
l0ng-ai 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.
2026-08-20 22:35:32 +08:00
Austin Spragginsandl0ng-ai 2cdc26f357 Wire hooks, resume and detection for Kimi Code CLI (#694)
* feat(agents): wire hooks, resume and detection for Kimi Code

Kimi Code CLI takes its hooks as [[hooks]] entries in the same
config.toml that holds the user's providers and models, so this adds a
third install strategy — a format-preserving TOML merge on toml_edit —
beside the JSON map merge and the owned files. Like Qwen it reports
permission requests first-class, so it gets no Notification hook.
Resume rides `kimi --session <id>`; fork stays unwired, Kimi
documents none.

Closes #693

Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com>

* fix(agents): harden the Kimi Code TOML hook merge and its resume flags

The TOML merge strategy the Kimi wiring introduces round-trips a shared
config.toml cleanly, but three gaps sat behind it.

`hooks_state` counted only the marked entries that still named an event,
so a hand-edit that dropped the key off one of nine entries left the
remaining eight matching the roster exactly and the file reported
Installed with a broken entry in it. Every marked entry now counts,
which is what the JSON merge already did and what `refresh_hooks` needs
to see.

A `hooks = []` spelled as an empty inline array made install fail
outright -- toml_edit keeps an empty array and an array of tables apart,
but the two say the same thing and neither carries any configuration. It
is now promoted rather than refused. Every other wrong-shaped `hooks`
key -- a string, a table, a non-empty inline array -- still refuses with
the file left byte-for-byte alone.

`Stop` is not the only way a Kimi turn ends: its own event reference says
`Stop` does not fire on interrupts and `Interrupt` fires instead, and a
turn that dies on an error reports `StopFailure`. Without those two an
Esc or a failed turn left the pane on "working" for good and `tty7 wait`
could only ever time out. Both are observation-only events and report
the same end of turn `Stop` does.

On resume, `--agent` and `--agent-file` join the stale flags: Kimi
rejects either next to `--session` at startup, and resuming rebinds the
session agent by itself, so replaying them turned a working resume into
a launch error.

Tests cover the wrong-shaped `hooks` keys, a config.toml that does not
parse on both install and uninstall, a file that does not exist yet, a
second install being byte-for-byte the first, mangled and surplus marked
entries, an uninstall threading between the user's own entries and the
tables after them, and the `--session=<id>`, bare `--session`,
`--continue` and `--agent` spellings on the resume path.

---------

Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-20 09:51:05 +08:00
l0ng-ai 9f34cd3501 fix(editor): stop scrolled-out text painting over the line numbers
Bump the gpui-component fork to 070d1a2, which clips the editor's scrolling
content to the right of the gutter. Text, selections, indent guides and the
cursor all paint from a bounds origin that horizontal scrolling has already
shifted left, so scrolled-out content kept painting under the line-number
column; the only thing hiding it was the gutter quad painted afterwards,
which works only while `editor.gutter.background` is opaque.

`apply_theme` clears that key to transparent so the panel can sit on a
gradient or image window background without a seam, which is exactly the
case the upstream code does not cover. Note that dependency in the theme,
so the next person to touch it knows the transparent gutter is not free.
2026-08-16 19:05:53 +08:00
l0ng-aiandl0ng-ai 0df604054d fix(daemon): keep a lingering daemon findable and reapable after quit-and-stop (#655)
* fix(daemon): keep a lingering daemon findable and reapable after quit-and-stop

Quit-and-stop could strand a daemon that had already unlinked daemon.sock
and deleted daemon.pid but never finished exiting: libc exit() runs atexit
handlers and static destructors beside dozens of live threads, and a
finalizer that blocks leaves the process holding the singleton lock with no
name on disk. Every later launch then spawns a daemon that stands down
against the lock and times out red, forever.

Three changes, each a fallback for the others:

- on_shutdown keeps the pidfile: once the endpoint is unlinked it is the
  only handle anything has on a process that is not gone yet. A pidfile
  that outlives a clean exit was already handled by recorded_daemon_is_dead
  and the reap path.
- The daemon exits through _exit(2) (after flushing the logger), skipping
  the atexit/destructor window entirely; everything owed to disk is flushed
  explicitly in on_shutdown.
- spawn::stop reaps with the pid it captured before asking the daemon to
  die, instead of re-reading a pidfile an old build's shutdown may have
  wiped mid-stop; reap_recorded_daemon keeps the pidfile when the process
  survives even SIGKILL, so the next attempt still has someone to reap.

* review: fix stale stop() comment, pin the mid-stop pidfile-vanish ordering in the test

The comment at the top of stop() still claimed a clean shutdown removes
the pidfile, which this branch just made untrue; it now states the real
reasons the pid is captured early. The vanishing-pidfile test now asserts
the sweeper's delete actually landed while stop() was waiting, so a
future shrink of PROCESS_EXIT_TIMEOUT cannot silently turn it into a
weaker scenario.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-16 17:35:39 +08:00
l0ng-aiandl0ng-ai 05de7ae33a fix(new-tab): keep the SSH menu inside a menu's shape (#649)
* fix(new-tab): keep the SSH menu inside a menu's shape

The saved-host rows carried names and endpoints long enough to drag the panel
out to the 500px ceiling PopupMenu falls back to, and the row that meant to
elide was clipped mid glyph instead. The menu now stops at 360px, and a row
that runs out of room cuts the endpoint first — the name is what the reader is
picking by, so it keeps whatever is left rather than being squeezed to "..".

The height ceiling moves up to fit the shape everyone actually sees — nine
shells, both headings, six hosts and the two closing rows — so the default menu
arrives whole instead of scrolled with "Local" cut off above, and is capped
again against the window so a short one never gets a menu taller than itself.

The rule above the split hint goes: a separator divides two lists of things to
pick, and the hint is a footnote about the list it follows.

Bumps gpui-component, where a scrollable PopupMenu painted a scrollbar whether
or not it overflowed, custom rows could not elide, and labels had no padding of
their own.

* fix(new-tab): measure the menu ceiling off the viewport, and elide nameless hosts

`window_bounds()` answers how a window should be reopened after it is
closed, so a fullscreen macOS window reports the bounds it would restore
to rather than the screen it currently fills. A terminal spends much of
its life fullscreen, where that reading capped the menu at 80% of a
window nobody is looking at — putting back the scrollbar and the
cut-off `Local` this branch is here to remove. `viewport_size()` is what
every other window-relative size in the app already measures against.

A host saved on its address alone is *named* `user@host:port` and carries
no note, so it took the plain-item path — bare text with nothing to elide
against, on the longest string in the menu and the row least able to cut
it. Every host row is a custom element now, and `menu_row` drops its
right half when the note is empty rather than holding the gap open with
a zero-width child.

Also drops 17 unrelated dependency downgrades that rode along with the
`gpui-component` bump. The lockfile moves only the three `source` lines
it meant to; `cargo check --locked` accepts it.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-15 14:24:22 +08:00
l0ng-ai f1144deb9f fix(ui): restyle the in-app notification to sit in tty7's own visual language (#646)
Bumps the gpui-component pin to pick up the notification restyle: flow-positioned status icon and close button that centre on the first line at any wrap count, a hairline-shadow surface in light theme, and a type ranking expressed in rems so it survives the ui_font_size setting.
2026-08-15 11:03:13 +08:00
l0ng-ai 71c6783fb4 chore(release): v26.8.3 2026-08-12 18:41:37 +08:00
l0ng-ai 2dc6a88af6 merge: main into the Source Control branch
Conflicts were the two streams touching the same seams, resolved by
taking the newer decision on each side:

- main's interface font scale (rems tokens) wins in right_panel.rs; the
  SCM panel keeps its local px steps until it moves onto that scale,
  and the now-unused PANEL_TEXT constants are gone.
- main's l10n_keys! macro (idents only) means the key list carries no
  doc comments any more; our SCM keys fold into it, and PanelUntracked
  stays deleted — its only caller was the panel this branch replaced.
- main's Command::localized palette style carries our Git group; ORDER
  keeps main's visibility and our width.
- main's ansi_seed/clear_ink refactor in presets.rs carries the lane
  colours: lanes() now clears through the same helper semantics uses.
- file_tree keeps both: main's drag-and-drop targets and this branch's
  git decorations per row.
- diff_overlay keeps both: main's sidebar-count write-back on snapshot
  install and this branch's epoch read and untracked preview.
- main's window.prompt SSH-close confirmation supersedes the bespoke
  modal our branch still carried; main's tile-glyph revert stands.
- main's two new guards are satisfied: the fourteen SCM actions carry
  authored names on the Keybindings page (their palette wording, plus
  a new CmdGitToggleGraph), ja translates ScmDetached, and CmdGroupGit
  joins the kept-in-English list — Git is a name.

2571 tests, 0 failures.
2026-08-10 13:25:30 +08:00
l0ng-ai 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.
2026-08-10 10:38:04 +08:00
l0ng-ai 0106430ecd merge: main into the Source Control branch
The one conflict is an import list in `diff_overlay.rs`: this branch added
`SharedString` for the unified view's row labels, main added `Background`
and `Hsla` for the window backdrop work. Both sides are still used, so the
resolution is the union.

Worth recording why this merge happened when it did. `main` moving is not
normally urgent — branch protection dropped its strict check, so a branch
behind main still merges — but a *conflicting* branch is different: GitHub
cannot compute `refs/pull/N/merge`, and every workflow that triggers on
`pull_request` silently stops running. Three pushes in a row registered no
CI at all on #424 while other PRs kept going green, which reads as a GitHub
incident and is really just an unresolved conflict.
2026-08-09 16:20:19 +08:00
ARNOandl0ng-ai 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>
2026-08-09 15:14:54 +08:00
l0ng-ai b4e7add65d chore(release): v26.8.2 2026-08-09 09:59:26 +08:00
l0ng-ai 9914a939b4 chore(deps): regenerate the lock for tty7-core's smallvec
The dependency was declared but the lock was never refreshed, so every
worktree building against it produced the same one-line diff.
2026-08-09 00:27:31 +08:00
l0ng-ai 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.
2026-08-08 22:40:04 +08:00
Hongwei Qin bb72be338d fix(windows): respect system proxy for remote server downloads (#364)
The GUI update check already uses reqwest, which reads the Windows
system proxy from the registry by default. The remote server
installer / bundled-server fallback uses ureq, which only reads
HTTP_PROXY/HTTPS_PROXY environment variables unless the
win-system-proxy feature is enabled.

Enable ureqs win-system-proxy feature so that release downloads
inside the daemon also honor the Windows system proxy set by tools
like Clash (System Proxy mode), v2rayN, etc. This is a no-op on
non-Windows platforms.

Fixes the inconsistency where the update check could reach GitHub
through the proxy but the actual download would time out trying to
connect directly.
2026-08-07 11:57:14 +08:00
Hongwei Qin 4a8a4bcbaa feat(proxy): macOS system proxy, Windows SOCKS parsing, manual override (#367)
Resolve an HTTP/SOCKS proxy for tty7's own update checks and release downloads, from (in order) a new `http_proxy` config field, the platform system proxy — Windows registry / macOS SCDynamicStore — and the HTTP_PROXY/HTTPS_PROXY/ALL_PROXY environment variables.

Programs running in a pane are deliberately unaffected: they inherit their proxy from their own environment, as in any other terminal.

Fixes #365.
2026-08-07 11:35:21 +08:00
Hongwei QinandHongwei Qin 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>
2026-08-07 11:27:07 +08:00
l0ng-ai 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.
2026-08-06 21:40:25 +08:00
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>
2026-08-06 10:12:41 +08:00
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>
2026-08-05 10:27:37 +08:00
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>
2026-08-04 21:09:07 +08:00
ayamir a6754b28bc feat(update): install verified macOS releases in app 2026-08-02 22:25:10 +08:00
thomasandClaude Opus 5 984452a83b fix(deps): pick up the gpui fix for the portal-triggered borrow panic on Linux
26.8.1 panics on launch under Wayland on a VMware Ubuntu guest:

    gpui_linux/src/linux/wayland/client.rs:924: RefCell already borrowed

The xdg-desktop-portal event source notified windows of the initial
color-scheme and button-layout replies while still holding
`client.borrow_mut()`, and those callbacks re-enter GPUI, which reaches
the same `RefCell` through `with_common`. Whether it fires depends on
whether the portal reply beats window creation, so a slow VM loses that
race every time.

Fixed in the fork (l0ng-ai/zed@3a4acfd) for both the Wayland and X11
clients by collecting the window pointers and dropping the borrow before
notifying. Windows and macOS never compile that crate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 15:14:12 +08:00
l0ng-ai 00607cc52f fix(release): bump tty7-cli/tty7-core/tty7-server lockfile versions to 26.8.1 2026-08-01 08:59:48 +08:00
l0ng-ai 6fc8bcb2cf chore(release): v26.8.1 2026-08-01 08:55:21 +08:00
l0ng-ai 74f4f1a35a chore(release): v26.8.0 2026-08-01 00:23:33 +08:00
thomasandClaude Fable 5 b8dc38fb98 fix(input): anchor the IME candidate window at the fake caret, not the parked cursor (#275)
Cursor-hiding TUIs (Kimi CLI, Ink apps) draw their caret as a reverse-video
cell and leave the real cursor wherever the frame's last write ended — for
Kimi that is the input box's right border, and the IME candidate list was
stranded there. When the cursor is hidden and its row holds exactly one
caret-sized inverse run, snap the IME anchor (and the marked-text preview)
to that run; rendering is untouched.

The gpui side (bumped here) now also answers IMR_QUERYCHARPOSITION — the
query the Windows 11 Microsoft Pinyin IME uses instead of CANDIDATEFORM —
and re-anchors the candidate window on every WM_IME_COMPOSITION.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 23:48:01 +08:00
l0ng-ai 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.
2026-07-31 19:18:44 +08:00
l0ng-ai 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.
2026-07-31 16:49:22 +08:00