Commit Graph
52 Commits
Author SHA1 Message Date
ARNO 643e0f7d95 feat(agents): add Qoder CN CLI integration (#988)
* feat(agents): add Qoder CN CLI integration

* fix(agents): spell Qoder CN's config override QODERCN_CONFIG_DIR

The China build of Qoder resolves its configuration through QODERCN_CONFIG_DIR.
The first pass invented QODER_CN_CONFIG_DIR instead, so an install that set the
real one was treated as unrelocated: hooks went to ~/.qoder-cn and history was
scanned from a directory the CLI never writes to. The test that should have
caught it set the same invented name, so it only proved the name agreed with
itself.

Picks up the rest of the review as well — detect the `qoder-cn` dispatcher
beside the other two binaries, call it the mainland-China build rather than a
different vendor, and drop the QODER_CN_HOOK_EVENTS alias for the table it only
pointed at. The serialized enums keep their variants appended; only the
independent ALL arrays moved QoderCLICn next to QoderCLI.
2026-09-28 13:35:10 +08:00
l0ng-ai b063ba97a0 refactor(settings): fold Window & Tabs into General, drop two settings (#982)
The Window & Tabs page is gone. Its three remaining tab settings (new tab
position, tab bar position, auto grouping) are a Tabs group on General,
below Startup & restore, so the nav has seven sections.

Removed outright:
- SSH tab title (`ssh_tab_title`, #726, unreleased). The sidebar already
  groups SSH tabs under their host, and renaming a tab pins its name.
- Open diff preview from sidebar counts (`sidebar_diff_preview`, #247).
  The sidebar counts and the Info panel's changes row always open the
  diff overlay; the large-tree stall it worked around is bounded. An old
  config.json that still carries either key loads as before.

The now-unused window settings icon goes with the page.
2026-09-27 19:11:45 +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 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 b5cb6efe7a style(sidebar): mark pinned groups with ◆ instead of a pin icon, drop the divider (#972)
The pin icon read as a smudge at header size. Kept groups now carry a small
◆ beside their name — every kept group, label groups too, since with the
divider gone it is the one thing that tells kept from derived. On a folder
group the mark is still the click that unpins it, and the hover button that
pins an auto group shows the same character.

The divider is no longer drawn at rest. It keeps its (smaller) place in the
layout and its recorded bounds, so pinning a header by dragging it up and
handing a tab back by dropping it below behave exactly as before, and it
still shows as a line while it is the drop target.
2026-09-27 09:31:49 +08:00
l0ng-ai cf0e8f02e6 feat(sidebar): pinned groups above auto groups (#962)
* feat(sidebar): keep pinned groups on the workspace and derive the rest

Replace the sidebar's hand-made groups with the model from #955: the sidebar
groups tabs by repo automatically, and you pin what you want to keep.

- Pinned groups (`PinnedGroup`: id, optional name, optional folder, fold) are
  stored on the workspace in the machine tree, in display order, and a tab
  points at one by `GroupId`. Everything else is an auto group worked out
  every frame and never stored: by repo home, or by `user@host` for an SSH
  pane — native or a shell that ssh'd onward — so `/home/ubuntu` on two
  machines no longer lands under one header.
- A tab whose cwd *enters* a pinned folder joins it (deepest folder wins; a
  repo home equal to the folder counts, which keeps worktrees with their
  repo). It is edge-triggered through `EntryWatch`, so a tab dragged out
  while still inside the folder stays out until it leaves and comes back, and
  a tab restored at launch is not pulled in by where it already sits.
- Groups sync as one `WorkspaceSetGroups` / `GroupsChanged`, pushed only from
  an edit and adopted from every pull, so a fresh window can never push an
  empty set over the workspace's. The machine hands tabs naming a dropped
  group back to auto grouping in the same mutation. Control dialect → v11.
- Config: `sidebar_grouping` (three modes) and `sidebar_collapsed_groups` give
  way to one `sidebar_auto_grouping` toggle; folds live with the workspace.
- `tty7 tab ls` reports the pinned group a tab is in (name or folder leaf;
  JSON carries id, name and folder).

* feat(sidebar): draw pinned groups above a divider, with their own gestures

The sidebar now reads as two halves: the groups you keep, in the order you
put them, then a divider, then the groups it works out (Arc-style).

- Pinned headers drag-reorder among themselves (their own reorder surface, so
  a pinned header cannot be dropped among the derived ones); the order lands
  on the workspace's group list, not on the tabs.
- An auto header carried above the divider is pinned when let go. With
  nothing pinned yet the divider appears during that drag as a "Drop here to
  pin" zone, since a hairline at the top of the list is nothing to aim at.
- A tab kept in a pinned group and dropped anywhere below the divider goes
  back to auto grouping; the divider lights to say so.
- An empty pinned group stays, with a "+ New Tab" row that opens a tab in its
  folder (or where ⌘T would, for a label group) and files it there.
- Folder groups carry a pin mark that unpins on click and a tooltip with the
  folder; auto headers show pin and "+" on hover.
- Header menus: pinned — Rename, Set Folder… (local workspaces), Use Current
  Tab's Folder, Clear Folder, New Tab, Unpin (folder groups), Delete. Auto —
  Pin Group, New Tab. Nothing renames an auto group; nothing pins implicitly.

* feat(sidebar): open folders as pinned groups from Finder, the file tree and the palette

Every way into a pinned group the design calls for:

- Drop a folder from Finder or Explorer onto the sidebar to pin it (a local
  workspace only — a dropped path is this machine's, and a folder group keeps
  a directory on the workspace's host). Files are let fall.
- "Pin as Group" on a folder in the file tree, on local and remote workspaces
  alike, since the tree and the group are both on the workspace's host.
- Palette "New Group" makes an empty label group and opens its name for
  typing; "Open Folder as Group…" picks a folder with the system picker, pins
  it and opens a tab in it. The picker browses this computer, so that one is
  not offered on a remote workspace.
- Tab right-click "Move to Group" lists the pinned groups plus "New Group…",
  which files the tab in a fresh label group with its name open for typing.

Pinning a folder already pinned hands back the group that keeps it rather
than making a second one to split its tabs with.

* fix(sidebar): let groups that arrive from elsewhere pull no tab into a folder

A window draws its first frames before its copy of the workspace's groups
lands, so every tab's entry watch recorded "in no folder" — and the groups
landing then read as each tab walking into its folder. A restored tab, or one
dragged out of its folder group, was pulled back in on every launch.

Groups adopted from a pull or from another window's `GroupsChanged` now start
every tab's watch over from where it is; only this window's own pin gathers
the tabs inside the folder, and says so tab by tab. A tab also goes up with
the group it names even when the window does not know that group yet, so a
sync in that same gap cannot send every kept tab back to auto grouping.

* docs(sidebar): describe pinned and auto groups, and log the change

Rewrite the sidebar page's grouping section around "grouped by repo
automatically; pin what you want to keep": the divider, folder and label
groups, the edge-triggered join, every way to pin, and the header menus. The
configuration reference swaps `sidebar_grouping` for `sidebar_auto_grouping`,
the CLI reference describes the GROUP column as the pinned group, and the
changelog gains an Unreleased entry (#955).

* fix(sidebar): file a tab opened by the CLI in a pinned folder into it

A tab that reaches a window as TabCreated — from `tty7 tab new` or another
window — started its entry watch as a restored tab, so opening one inside a
pinned folder left it in the auto group below. It is as new as a tab opened
here, and now joins the folder like one; every window that hears of it
reaches the same answer.

* test(machine): build the group sets in their initializers

Clippy's field_reassign_with_default on the two WorkspaceGroups the
set-groups test assembles.

* fix(sidebar): draw restored tabs in their auto group, and title by repo again

Auto groups are not stored, so after a restart every tab sat in Ungrouped
until its own repo probe came back, then jumped; before pinned groups the
stored repo key put it in place on the first frame. Each tab now carries
`last_auto`, the auto group it last resolved to, as a hint: stored with the
tab, sent up alongside its group in `TabSetGroup` whenever the live answer
moves, and used to draw the tab until the probe answers. The probe always wins
and rewrites the hint, and the hint never outranks a pinned group or the
folder-entry rule. Another window's hint only fills a gap, so two windows can
never bounce a disagreement between them.

The workspace's fallback title regained the repo majority it lost: the most
common pinned folder first, then the repo most unpinned tabs were last filed
under (a worktree counting toward its repo home), then a pane's cwd.

* refactor: drop what the new sidebar left unused, and two clippy findings

- `TerminalView::native_ssh_cwd` and its helper existed for the sidebar's old
  folder grouping of native SSH panes; an SSH tab now groups by host, and
  nothing else read it.
- The file tree's context menu takes `cx` instead of `danger` and the new
  groups flag, back to the argument count it had on main.
- A title test builds its workspace in the initializer.
2026-09-25 16:53:26 +08:00
l0ng-ai 9f034558c0 feat(ssh): switch a port forward off without losing its rule (#953)
A forward could only be removed, so pointing one local port at another
remote target meant deleting the rule and retyping the other one (#439).

Forward rules gain an `enabled` flag (serde default on, so saved profiles
and older peers keep every rule live) and the daemon a SetForwardEnabled
op for panes and workspaces. Off releases the listener and keeps the
entry; on rebinds it from the rule it was made from, and is refused by
name when another switched-on forward of the same owner holds the port.
The Ports panel and the Settings rules editor each get a switch; a
switch flipped in the panel on a rule from a saved host is written back
to that host.

Closes #439
2026-09-25 16:38:00 +08:00
l0ng-ai 455e74dc2c feat(scm): filter changed files and show them as a tree (#950)
* feat(scm): filter changed files and show them as a tree

* test(scm): key the tree test's settle on the panel's own root
2026-09-25 13:53:44 +08:00
l0ng-ai 458c923aaa docs: lead with the tour video instead of the old hero screenshot
The README now opens on an animated WebP of the one-minute tour (GitHub
won't play an mp4 inline), linking the full-quality mp4. The docs home
page autoplays the mp4 in the hero slot. The old hero screenshot and the
separate tour poster block are gone.
2026-09-24 08:28:23 +08:00
l0ng-ai fed4aec170 docs: replace placeholder screenshots and add a one-minute tour video
Every placeholder frame in docs/ now shows a real capture of the current
build: 20 screenshots plus two short looping clips (prompt editor, pane
drag). The README and the docs home page link a one-minute tour covering
agent status across repos, one agent driving another through the CLI,
the prompt editor, diffs, pane dragging, and sessions surviving a quit.
2026-09-24 08:18:14 +08:00
l0ng-ai 0d0dc597ee feat(agents): add CodeBuddy CLI integration (#892)
CodeBuddy Code takes Claude Code's hooks as-is: the same nested
`hooks.<Event>[].hooks[{type, command}]` shape in ~/.codebuddy/settings.json
(or $CODEBUDDY_CONFIG_DIR/settings.json), the same event names, and the same
session_id/cwd payload fields. So it rides the shared hook-map installer and
needs no payload aliases.

The event table follows Qoder rather than Claude: CodeBuddy has a
first-class PermissionRequest and Elicitation, so it gets no Notification
hook, and StopFailure ends a turn like Stop. CodeBuddy also emits
SessionStart with source "compact" mid-turn, which is filtered out the same
way Qoder's is so the pane does not drop back to idle.

Detection covers all three npm bins (codebuddy, codebuddy-code, cbc).
Resume is `codebuddy --resume <id>` and fork appends --fork-session; stale
session and worktree flags are dropped from the replayed launch argv, and
--no-session-persistence disables both commands.

Icon, en/ja/zh names and search keywords, and docs are updated.
2026-09-23 15:04:06 +08:00
l0ng-ai 9b4dd53547 Merge pull request #916 from l0ng-ai/style/sidebar-marks-and-chrome
style(ui): give agent marks their brand colour back, quiet the toolbar
2026-09-23 09:33:20 +08:00
l0ng-ai fea497433d style(ui): give agent marks their brand colour back, quiet the toolbar
The sidebar avatar dropped its brand-coloured disc for a flat theme one,
which left a column of identical grey marks: hue had been the fast way to
tell one agent from another down twenty rows, and the disc was the only
thing carrying it. Painting the mark itself restores the reading at a
third of the coloured area, so it no longer competes with the status dot
beside it — colour says who, the dot says what it wants. `mark_ink`
keeps the hue and only lifts a value that cannot be seen on the surface
under it, which is Codex and Grok's pure black on a dark theme.

The toolbar above it had the opposite problem: `+` and the panel toggle
were drawn at `sidebar_foreground`, the rung a tab title uses, so the two
controls were the darkest marks in the sidebar. They drop to
`muted_foreground`, level with the workspace chip, and the hover fill the
variant already carried answers the pointer.

The glyphs themselves: the panel icons fill their compartment so they
read as a sidebar rather than a split box, and the plus carries a sixth
more stroke than the closed shapes beside it — an open form at the family
weight reads both larger and fainter, which is what made it the odd one
out. Its cap now lands on a half pixel rather than a whole one; the
whole-pixel rungs are 10px and 12px in a 16px box, and neither is the
size.

Claude-Session: https://claude.ai/code/session_01FG2s9mbZu6LbjjmU54X7kt
2026-09-23 09:10:30 +08:00
l0ng-ai fd9f589c80 Merge pull request #914 from l0ng-ai/feat/right-panel-refresh
style(ui): unify native colors, icons, and sidebar hierarchy
2026-09-23 00:39:03 +08:00
l0ng-ai 6846571142 Refine native UI hierarchy, icons, and sidebar chrome 2026-09-22 23:20:31 +08:00
Fabrice Aneche c2d2db2cfc added support for Crush agent 2026-09-11 21:56:22 -04:00
White 9e338b1d60 feat(agents): add Qoder CLI integration
Hook events map Qoder's lifecycle to tty7's state machine: session start,
prompt submit, permission requests, MCP tool elicitation (an authorized MCP
tool can still pause for user input mid-call), tool completion, stop, and
session end. Compaction events are filtered out—Qoder emits a session-start
after compacting the active turn, which would reset the status line to Idle
without this filter, even though the turn is still running.

Settings path resolution respects QODER_CONFIG_DIR for local installs,
falling back to ~/.qoder/settings.json. Remote targets ignore the override
(a local env var must not redirect remote hooks).

Session commands support --resume and --fork-session. The resume command
strips conflicting flags (--resume, -r, --continue, -c, --session-id,
--worktree, --fork-session) from the original launch argv before appending
the new session id. The -w/--cwd flags survive (Qoder's -w means --cwd,
not --worktree). Both commands require session persistence: when
--no-session-persistence is present, there is no saved conversation to
reopen, so the commands return None.

Tests cover compaction preservation, MCP elicitation state transitions,
QODER_CONFIG_DIR's effect on the hook lifecycle (multi-case isolation),
resume/fork command generation, worktree flag handling, and persistence
requirements.

Localization complete for en/ja/zh. Icon embedded, search keywords wired.
2026-09-11 10:30:19 +08:00
ayamir fc94022ed0 feat(agent): add TraeCode CLI support (#807)
* feat(agent): add TraeCode CLI support

* fix(settings): index TraeCode agent hooks
2026-09-08 16:57:04 +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 8b5aeb0077 Wire hooks, resume and fork for the CLI agents that support them (#666)
* feat(agents): hook, resume and fork support for nine more CLI agents

Hooks go from 7 agents to 11. Gemini, Droid and Qwen merge into their
own settings.json the way Claude and Codex already do; Goose gets an
owned file under the Open Plugins layout it implements. Qwen is the only
one of them with a first-class PermissionRequest event, so it needs none
of the notification sniffing the others do -- and deliberately gets no
Notification hook at all, since that event fires for non-blocking alerts
too and would strand a pane on "waiting".

Resume goes from 10 agents to 17, fork from 5 to 9. Amp's `threads fork`
is a real subcommand that is simply missing from `amp threads --help`.

Four detection and replay bugs turned up while checking each CLI:

- `python3 -m antigravity`, the documented way to trigger Python's own
  easter egg, was detected as a coding agent. The `antigravity` binary
  is the IDE's launcher shim anyway, in the shape of VS Code's `code`,
  not the terminal agent -- that one is `agy`.
- Amp lost every launch flag on resume. It names a thread with a
  positional argument, so the stale-flag list had nothing to drop and
  the generic bare-token check rejected the whole tail along with it.
- Gemini could be handed a command line it refuses to start from:
  `--session-id` and `--session-file` are mutually exclusive with
  `--resume` and were never stripped.
- Cursor's `--continue` was not stripped either, leaving it to collide
  with the injected `--resume <id>`.

Brand colours for Aider, Goose, Droid, Vibe, Qwen and Antigravity now
come from first-party sources -- logo SVG fills and site CSS variables
-- rather than approximations. Qwen ships its real mark instead of the
generic bot glyph.

Hooks stay unwired for Aider (no lifecycle mechanism exists at all),
Cursor (its usable events gate permissions, and tty7's silent hook would
read as a failed check and auto-allow the command), Auggie (its command
field takes only script paths, needing generated wrappers, and the
constraint could not be verified without a billed run), and for Hermes,
Amp, Vibe and Antigravity, whose event sets are too thin to report a
blocked turn.

* fix(agents): strip every session-naming alias before replaying launch flags

Goose spells --session-id also as --id, --name as -n, and keeps a legacy
--path, all in one exclusive clap group; Qwen rejects --session-id next
to --resume; Vibe shortens --continue to -c. Any of these surviving a
replay broke the regenerated resume command. Qwen's --no-chat-recording
also persists nothing, so it now opts the pane out of resume and fork
like Auggie's --dont-save-session. The Qwen icon gains the 24x24
width/height every other agent mark carries.
2026-08-18 00:42:56 +08:00
l0ng-aiandl0ng-ai 3cd90c6ca6 feat(ssh): name panes after their host, reach the host form from where you are, and test a connection (#566)
* feat(ssh): name panes after their host and reach the host form from where you are

Five gaps in the SSH flow reported in #438, and the two silent no-ops
around them.

Panes now carry a display name: a saved host's own name, or its address
when nobody named it — every host imported from ~/.ssh/config arrives
nameless, and each one opened a tab reading "tty7". The name survives a
title reset and a dropped link, and `strip_host_prefix` no longer cuts
`deploy@10.0.0.5:2222` down to "2222" on its way to the tab strip.

The host form is now reachable from the machine in front of you: the
switcher's machine menu edits the host it is showing, or offers to save
one for a machine reached by address or by ~/.ssh/config alias. A live
connection dialled by hand can be kept as a host from the palette, with
everything it was dialled with carried over — the one thing that cannot
come along is an ad-hoc jump hop, and that is said out loud rather than
saved broken.

The New Tab menu lists the saved hosts, most-used first, and the typed
address route that already understood -p, -J and config aliases. Both
were behind the workspace switcher, its host dialog and the settings
list.

Finally, the three proxy fields are exclusive — map_proxy picks the
first one filled and ignores the rest — so the ones that lose now say
which field won instead of leaving it to be discovered by connecting.

* feat(ssh): test a connection from the host form, and pick a host by name

Three things the host form and the New Tab menu were missing.

The form gets a Test button. It hands the spec to the daemon, which
dials it the way Connect would — proxy, jump host, host key, auth — and
drops the connection again, reporting how long it took. A test never
rides an existing connection: one would answer for the credentials that
connection was made with, so a password typed wrong would come back
green. Anything the handshake stops to ask a person is declined on the
spot and reported as what it asked for, since a form is nowhere to
answer a password prompt and waiting out the two-minute prompt timeout
would be a worse answer than "it got that far and wants your password".
The result clears the moment a field changes: vouching for a host that
was edited since is worse than saying nothing.

The Auth row becomes a dropdown. Six methods is more than a segmented
control can label without squeezing, and it is the row that stacks
first on a narrow page.

The New Tab menu scrolls — PopupMenu only does that past 20 items, and
every shell on the machine plus a handful of hosts already runs off a
short window — and past the five hosts it lists, Find a Host opens a
filter box over all of them.

* feat(new-tab): put a search box on the New Tab menu

The menu is as long as the machine is — nine shells here, and a
~/.ssh/config with two dozen hosts in it is ordinary — so it needed
filtering, not a scrollbar and a row leading somewhere else.

A PopupMenu cannot hold a text field: it claims the keyboard for its own
navigation, and there is no search input anywhere in it. So the New Tab
button now opens a popover holding the same searchable list the command
palette is built from, with the shells and the hosts under their own
headings and one box over both. Typing filters across both groups;
typing an address offers to connect to it, the way the palette does.

The standalone host picker this replaces is gone with it, and the row
that led there — one search reachable from the button beats two behind
a menu.

* revert(new-tab): put the New Tab menu back to shells only

The searchable popover was the wrong shape for a button in the chrome:
too big and too heavy next to the tab strip it hangs off. The menu is
the plain shell list it was before this branch — byte for byte, so
nothing about it needs re-reviewing — and the hosts, the search box and
the row leading to a host picker are gone with it.

Everything that came along to serve it goes too: the positional
NewTabWithShell command, the picker's palette delegate and its compact
row metrics, the standalone host palette, and the four strings they
needed. Connecting to a saved host is the palette's job again, which is
where it was and where it works.

* fix(switcher): size and weight the machine glyphs like the icons beside them

The two machine icons are drawn by hand; every other glyph in that
gutter comes from lucide. Ours were built to a tighter box — ink 19.3 ×
17.3 of a 24 grid against lucide's 22 × 20 — so at the same nominal
16pt the local machine rendered 11.5pt of ink beside a 14.7pt globe and
read as a size smaller. Both are redrawn to lucide's extents, which
also makes them agree with each other.

The local machine's glyph was muted while every remote one was full
strength, and while its own name was full strength either way. Beside
the machine under it that read as a disabled row rather than as the
computer you are sitting at. One weight for all of them now; the
"Other Machines" globe keeps its dimmer register, which belongs to the
muted section label it sits next to.

* fix(tabs): only a port stops the host head being cut off a title

Teaching `strip_host_prefix` that `deploy@10.0.0.5:2222` is an address and
not a titled directory was done by requiring the tail to start with `/` or
`~`. Two very common titles do neither.

Debian's stock bash title is `\u@\h: \w` — a space after the colon — so
`user@host: ~/work` would have stopped being cut at all, and every one of
those tabs would have gone from reading `~/work` to `user@host: ~/wo…`. And
tty7's own PowerShell integration writes `ann@BOX:C:/src` whenever the cwd is
off the home drive, which would have read `ann@BOX:C:/src` rather than
`C:/src`.

Key on the port instead, which is the thing that actually makes the string an
address: a tail of nothing but digits is kept whole, and everything else is
the path it always was. The space belongs to the head, so the tail is trimmed
on the way out.

* fix(ssh): keep a connection test off the cache, off a stale form, and clear about a changed host key

Three ways the new Test could answer for something other than the host in
front of it.

It dialled with `reuse: false` but still took the connection cache's slot
lock, which is held for the whole handshake. So a test stalled every Connect
to the same host behind a connection it was never going to leave them — and,
queued behind a session already dialling, spent its own budget waiting and
came back "connection timed out" about a host that answers fine. A test that
is not going to touch the cache has no business locking it: it now skips the
slot entirely, and only a reusing dial takes the guard it later fills in.

The form dropped a test result whenever a typed field changed, on the
grounds that the answer was about the host as it was a moment ago — but the
authentication method is a dropdown, and changing it left the green line
standing under a handshake the form would no longer make.

And a host key that has *changed* was reported with the same words as one
nobody has accepted yet. Those are not the same news: the first is a new
host, the second is the server presenting a different key than the one on
file. `SshTestNeed` now tells them apart and each gets its own line, in all
three locales.

Verified against a live sshd on localhost: refused port and unresolvable
name come back in milliseconds with the connect path's own message, a
password host comes back `NeedsInput { Password }` in 55 ms rather than
waiting out the two-minute prompt timeout, and two tests of the same host
back to back no longer serialize.

* chore(palette): drop the root flag the New Tab revert left behind

`grouped_root` was split out of `quick_connect_root` for the searchable host
picker on the New Tab menu, which was taken back out again. Every
constructor now sets the two to the same value, so the second one is a field
and a doc comment describing a list that does not exist.

* docs(changelog): record the SSH host form, pane names and connection test

Every user-facing change in this branch: panes named after their host, the
host form reached from the switcher and the palette, and Test.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-13 15:02:58 +08:00
l0ng-aiandl0ng-ai bed3c856ed fix(panel): redraw the right panel's tab icons and put the tree glyph on the tile ladder (#578)
* fix(panel): redraw the right panel's three tab icons

The tiles render at 13px (`TILE_GLYPH`), and at that size the set had
three problems, none of them about style:

- `info` and `folder-closed` were both a rounded body split by a
  horizontal rule at nearly the same proportions, so the first and third
  tile could only be told apart by a notch a pixel and a half wide.
- `git-branch` drew its three nodes as filled r=2.5 discs — 2.7px across
  at render size — which buried the 2.1 strokes between them and left
  the glyph reading as three dots.
- The interior detail was too dense to survive: `info` carried two rules
  4.8 units apart and `folder-closed` a divider 3.2 units below its
  front edge, both under 2.6px once rendered.

Same three metaphors, same 2.1 pen, same rounded joins — only the
geometry moves. The nodes go hollow so the branch is drawn with one pen
rather than two. The crowded interior rules go, which leaves the folder
a clean body and makes the notch the thing that separates it from
`info`, rather than a second rule at a slightly different height. And
each mark grows into the height `panel-right` already occupies beside
them: the three had the width of that box and sat two units short of its
height, which on a row of tiles reads as three smaller icons rather than
as three different ones.

Measured as rendered ink, stroke included, on the 24 grid:

| glyph                               | before      | after       |
| ----------------------------------- | ----------- | ----------- |
| `panel-right` (unchanged reference) | 19.3 x 19.3 | —           |
| `info`                              | 19.3 x 17.3 | 19.3 x 18.1 |
| `folder-closed`                     | 19.3 x 17.3 | 19.3 x 17.7 |
| `git-branch`                        | 18.5 x 17.3 | 18.3 x 18.9 |

All three stay centred on 12, 12.

* fix(files): size the tree row's glyph off the tile ladder, not the rem one

Glyphs in this window come from the pixel ladder in `app.rs` —
`TILE_GLYPH` 13 for a chrome tile, `TILE_GLYPH_XS` 11 for the tiles a row
reveals on hover — while text comes from the rem ramp. The file tree's
leading folder/file mark was the one place drawing its size from
gpui-component's rem sizes instead, and those two systems cannot meet:
at the default `ui_font_size` of 16 the rem ladder offers `xsmall` 12 and
`small` 14 and nothing in between, so the mark was always a step under
the tab tiles above it — a speck beside a 14px name — or a step over
them, which is a row of content outsizing the navigation that owns it.

`ROW_GLYPH` is that size named once, in pixels, next to the panel's type
ramp and defined as the tab tile's own glyph, so the two now agree by
construction rather than by coincidence.

* fix(icons): put the tree's collapsed folder on the set's box

`folder.svg` is the one folder the set never squared up. It inks
19.9 x 16.1 where every other mark in the row is 19.3 wide, because its
top edge overshoots the box by 0.6 on the right — which also leaves the
whole glyph 0.3 off-centre that way, and 0.2 low.

Nothing showed while the tree drew it a step under the chrome. It is on
`ROW_GLYPH` now, the same 13px as the Files tab tile above it, and it
swaps in place with `folder-open` — already on the box — every time a
directory opens or closes, so the overhang is a shift in a fixed column
rather than a number in a file.

Same folder, same 2.8 corners, same 1.7 shoulder: the top edge gives
back the 0.6 it overshot and the body takes the 1.2 of height that puts
it on 19.3 x 17.3, centred on 12, 12, which is where `folder-open` and
the rest of the row already are.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-13 15:00:24 +08:00
l0ng-aiandl0ng-ai 3c5f14f71c fix(icons): draw the plus on the same optical bound as the rest of the set (#577)
* fix(icons): draw the plus on the same optical bound as the rest of the set

Every icon tty7 draws itself sits on one optical bound: 3.4..20.6 of a 24
viewBox, 19.3 units of ink once the round caps are counted. `plus` was the
exception at 6..18, 14.1 units — 59% of the box where the rest fill 77-80%,
and tighter even than the stock lucide glyph it replaced.

That was compensated at the call site instead, by handing the three chrome
tiles that carry a `+` a 16px glyph (`TILE_GLYPH_LINE`) where every other
tile in the same row gets 13. It worked where it was applied and nowhere
else: the port-forward section's `+` sits in a 24px tile, which has no
`_LINE` step to grow into, so it rendered at 7.6px of ink against the 10.5px
of the icons around it.

Redraw the asset onto the bound and the compensation is no longer needed
anywhere — the three call sites go back to plain `chrome_tile`, and the 24px
tile is fixed for free.

Scaling a glyph up buys stroke as well as extent, which is the part that is
easy to miss: at 16px the old art also drew 16/13 wider, so moving the `+`
to 13px thinned it by a fifth even as it got longer. A cross is two hairlines
with no fill to hide behind, so the weight had to come back in the art's own
`stroke-width`, and the value is eyeballed rather than derived — 1.0 CSS px
reads thin against the closed shapes beside it, 2.0 reads heavy-handed, and
1.75 is where it settled after walking the range on a 2x screen.

`TILE_GLYPH_LINE` stays for stock lucide `close`, which has the same tight
geometry and is not ours to redraw.

* fix(icons): keep the plus's extra weight to the 16/13 the call site was buying

The redraw put `plus` on the family's optical bound and then took its
`stroke-width` to 3.2308 — 1.75 CSS px at a 13px glyph, settled by eye
against `panel-left`, `panel-right` and `ellipsis` in the titlebar row.
Those are the three call sites the redraw was checked at, and they are
the three whose neighbours are closed shapes carrying solid fills. The
other three are not:

- Source Control's row strip draws `plus` (Stage) directly beside stock
  `undo-2` (Discard) at an 11px glyph, and pairs it with stock `minus`
  (Unstage) one group up. Both stock glyphs stroke 2. At 3.2308 the `+`
  came out 1.48 CSS px against their 0.92 — the stage button reads as
  the emphasised one in a strip where nothing is emphasised, and a
  stage/unstage pair stops looking like a pair.
- The switcher's Add SSH Host row draws it at 16px in the same gutter
  column as `search`, which is ours and strokes 2.1.
- Settings → SSH puts it next to stock `search` and stock `ellipsis`.

Reading a size heavy is the same failure as reading a size small, so
the weight goes to the one value that is derived rather than chosen:
2.1 × 16/13 = 2.5846, the family's own stroke times the scale-up the
three chrome tiles were already applying. At `TILE_GLYPH` that renders
at 1.4 CSS px, which is exactly what the titlebar `+` has always drawn
at — so nothing that was verified on screen moves, and the three tiles
that never had a `_LINE` step to grow into stop being overdrawn. The
arm ends move to 4.9846..19.0154 to keep the round-cap tips on whole
pixels at the new weight; the ink extent is unchanged at 16.6.

The test now derives the weight from `panel-left`'s own `stroke-width`
instead of carrying an eyeballed band, and checks both arms rather than
the vertical one — a cross edited on one axis passed before.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-13 14:49:33 +08:00
l0ng-ai 657bb4e0b7 chore(assets): crop the hero screenshot tighter around the window 2026-08-11 21:54:19 +08:00
l0ng-aiandl0ng-ai 6d47406544 docs(readme): add a hero screenshot of the workbench (#475)
One 1600x1132 WebP (184 KB), shown at 900 px in both READMEs.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-10 21:00:42 +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 ad23a4223b fix(icons): a chrome glyph is the size of its neighbours
Every icon in the detail panel's top row is drawn by the same tile at the
same 13px, so the sizes looked settled in the code — but each one paints a
different amount of the 24 box it lives in. Measured as rendered ink,
git-branch was 14.6 wide against folder's 19.9, a 36% spread on a row of
five, which is why the Changes tab read a size smaller than the tabs on
either side of it. The two folders were also squat, and folder-open — the
Reveal button directly under the folder tab — put a second, differently
sized folder in the same column.

Redraw the three outliers into the box info and panel-right already use:
3.4–20.6 across, 4.4–19.6 down. git-branch stops 4% short of the others on
purpose — it is three solid discs, and a filled shape reads heavier than a
hairline outline at an equal extent.
2026-08-09 13:09:47 +07:00
l0ng-ai fee48a4c99 Merge origin/main into integration/polish
main shipped v26.8.2 and 15 fixes while this branch was open. Resolved:

- zh: main's #417 decided the background process is called "server" in
  Chinese, and that decision is newer than this branch's "服务器" — took
  it, kept this branch's typographic quotes around {machine}, and
  whitelisted SettingsServer in the new every-key-is-translated test,
  since the zh heading is now that English word on its own.
- presets.rs: this branch factored main's inline `clear` closure into
  Theme::clear_ink / ansi_seed; same arithmetic, so kept the methods.
  #400's border and caret floors and #413's legible-palette flag both
  survive untouched.
- app.rs: took main's Option-typed `alive` argument, kept this branch's
  note on why a dropped tab is worth a sentence.
- README / docs: agent count is now exactly 18 with Oh My Pi, so the
  precise number replaces both "17" and "~18"; the zh feature doc keeps
  its translated menu names and gains Oh My Pi in the fork list.

Six keys main added are gone because the surfaces that used them were
rewritten here: the home screen's relative time now runs to years, hook
failures name install vs remove, Full Screen left the View menu on
purpose (AppKit adds its own), the SFTP filter says "search files", and
the settings index titles its CLI row by its own label.
2026-08-09 12:28:42 +07:00
l0ng-ai 3796ec3399 fix(completion): stop a rustup flag describing itself
`rustup doc --unstable-book` carried "--unstable-book" as its
description, so the menu printed the flag twice and explained nothing.
Every sibling reads as the title of what it opens — The Cargo Book, The
Rust Reference, The Embedded Rust Book — and rustup's own help calls
this one The Unstable Book.

Found by sweeping the specs for descriptions that merely repeat their
own flag. The other eleven hits are terse but real: docker login
--password really is "Password".
2026-08-09 00:44:33 +07:00
l0ng-ai 0ab2e47829 fix(completion): describe every entry in the specs this machine can verify
Eleven of the twenty-nine tools with undescribed entries are installed
here, so their descriptions could be quoted from the tool rather than
written from memory. That closes nine more specs completely — rustup,
brew, gh, make, pip, pip3, rg, bundle, cmake — and most of npm.

Each line comes from the tool: rustup help for the --toolchain and
--profile flags and default-host, brew(1) for `edit`, `gh completion`
for --shell, make --help for -j, pip help for --files and --index,
rg --help for --sort and the --no-pcre2-unicode deprecation, bundle help
for `config` and `help`, cmake --help and `cmake -E` for theirs. The
five `npm team` subcommands and the `--` separator are read off their
usage signatures. Every spec here writes descriptions without a trailing
period, so these do too.

194 gaps across 28 specs, now 162 across 19. `npm run --silent` is left
alone: npm ships no man page here to quote.
2026-08-09 00:33:23 +07:00
l0ng-ai 40698ed344 fix(completion): describe cargo add/remove --color like the other 41
Every other --color in the cargo spec reads "Coloring: auto, always,
never"; add and remove had none, so the one flag that appears on nearly
every cargo subcommand explained itself everywhere except the two
newest.

The spec's other cargo gaps are left alone on purpose. They are not
unwritten text: `cargo tree --no-dev-dependencies`, `--prefix-depth`,
`--no-indent` and the `vendor` flags beside them are from the old
cargo-tree subcommand and current cargo has no such flags, while
`cargo config get` is undescribed in cargo's own help. Writing prose for
those would document flags that do not exist.
2026-08-09 00:03:36 +07:00
l0ng-ai 50909b0f84 fix(completion): finish the descriptions git's completion menu was missing
The README sells "every flag and subcommand with its description", and
git — the spec people hit most — had five entries with none: the `blame`
subcommand, `bisect help`, and three flags. `blame` sat in the menu as a
bare word between neighbours that all explained themselves.

Filled from git's own help output rather than written from memory:
"Show what revision and author last modified each line of a file",
"Print a long usage message", "Dry run", "Prune remotes after fetching",
"Read pathspec from file".

A sweep of all 97 specs found 194 more gaps in 28 other tools — dotnet
30, yarn 27, bun 22, flutter 18, conda 14, cargo 13 lead it. Not filled
here: those need each tool's own help to quote, not invention.
2026-08-09 00:01:12 +07:00
l0ng-ai 80e8e09548 feat(scm): turn the Changes tab into Source Control and wire the surface up
Renames `RightPanelTab::Changes` to `Scm` in place. `#[serde(rename =
"changes")]` works in both directions, so what lands on disk is unchanged
and a build from before this commit reads the config back without kicking
anyone off the panel they left open — a fourth variant could not do that,
and 260px has no room for a fourth tab tile anyway. The action name
`ShowRightPanelChanges` stays put because `Config::keybindings` is keyed by
it, and every existing custom binding would otherwise be orphaned.

The panel body moves to `src/ui/scm/panel.rs` byte for byte; it still
renders the flat `git diff HEAD` list. Alongside it the module gets the
pieces the rest of the feature is built from: the shared status glyph and
colour tables (one definition instead of three that drift), the path and
timestamp helpers a 260px column needs, and the panel's state types.

Also wires the whole surface: fourteen actions, their key bindings, ten
palette commands in a `Git` group of their own, and the translations.
`ScmCommit` takes `secondary-enter`, which macOS already gives
`ToggleFullscreen`; the two coexist because the commit binding is scoped
to the commit box, and `every_default_chord_is_claimed_by_exactly_one_action`
now checks uniqueness per context instead of globally, which is the actual
invariant gpui enforces.

`Config` gains `diff_view` and `scm_graph_expanded`. Both default to what
happens today.

Two new icons. `git-sync.svg` is deliberately not `refresh.svg`: the panel
header already carries a refresh tile, and the same glyph meaning two
different things one row apart reads as a bug.

Adds `every_action_has_a_binding_arm`, which walks every action in
`default_bindings` rather than only the ones shipping a default keystroke.
The gap it closes is an action listed in Settings with no `make_binding`
arm behind it: the user assigns a key and the key silently does nothing.
2026-08-09 00:51:06 +08:00
l0ng-aiandl0ng-ai 817447bd48 feat(agents): recognize Oh My Pi and install its status hooks (#405)
Issue #376 asked for `omp`. Oh My Pi is a fork of Pi (can1357/oh-my-pi,
descended from badlogic/pi-mono), but the fork is where the similarity
stops for our purposes: it ships one binary of its own — `omp`, the only
`bin` in `@oh-my-pi/pi-coding-agent`, and it never installs a `pi` — and
it keeps its config under `~/.omp`. A pane running it was therefore not
detected at all, and aliasing `omp` onto `CLIAgent::Pi` would have been
worse than nothing: the status bridge would land in `~/.pi`, and Resume
Session would offer `pi --session <id>` to a binary that spells that
flag `--resume`.

So it gets its own variant, wired the whole way through:

| | |
|---|---|
| Detection | argv stem `omp`, distinct from `pi` in both directions |
| Avatar | its own mark, normalized from the project's `assets/icon.svg` |
| Resume | `omp --resume <id>`, opting out on `--no-session` |
| Fork | `omp --fork <id>` — a verified fork command, so the menu item appears |
| Hooks | Settings → Agents, at `~/.omp/agent/extensions/tty7/index.ts` |

The status bridge is the one piece the fork did not change. Oh My Pi
inherited Pi's extension contract intact — same default-exported factory,
same `session_start` / `agent_start` / `agent_end` / `session_shutdown`,
same `ctx.sessionManager.getSessionId()` — so `pi_extension_ts` now takes
the agent and substitutes two things, the package it imports the type
from and the slug it calls the emitter with. Pi's generated file is
byte-identical to before, so no installed bridge goes stale.

`--resume`, `-r` and `--session` are three spellings of one flag in Oh My
Pi; all three shed when a session command is rebuilt, while `--session-dir`
is a different flag and rides along. `fork_command` now honors the same
`--no-session` opt-out `resume_command` already did — Oh My Pi rejects
`--fork` outright under it, and no existing agent declares an opt-out.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-08 13:10:38 +08:00
l0ng-aiandl0ng-ai b9956ad6af feat(switcher): two-column panel with IDEA-style Ctrl+Tab (#380)
* feat(switcher): two-column panel with IDEA-style Ctrl+Tab

The switcher listed workspaces only; reaching a tab inside one meant
opening it first. It is now two columns — workspaces on the left, the
tabs of whichever one the cursor sits on to the right — and Ctrl+Tab
raises it as a most-recently-used tab switcher that commits when the
modifier comes up.

Picking a workspace or a tab now switches this window in place. A second
window is something you ask for, with the platform modifier or "Open in
New Window", rather than what happens by default.

New workspaces get a codename ("amber-yak") instead of inheriting
whatever directory their first shell started in. The generator moves out
of worktree.rs into core::codename so both callers share it.

* fix(switcher): review fixes — platform-gate the Ctrl+Tab key test, park the new-window tab

- The key_intent test asserted only the macOS half of the Tab chord; off
  macOS Ctrl *is* the secondary modifier, the raw key falls through on
  purpose (the chord arrives as NextTab), and the test now says so
  instead of failing there.
- open_at_tab probed a freshly opened window's tabs exactly once, but a
  new window hydrates them asynchronously — route through
  activate_tree_tab so the pick parks until the tab arrives.
- Escape mid-rename backs out of the rename instead of tearing down the
  whole panel.
- A one-tab workspace reads '1 tab', not '1 tabs'.

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

* style: cargo fmt

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

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-07 14:30:40 +08:00
Hongwei Qin 0190036a2e chore(ui): remove the Outline right panel (#375)
Removes the Outline right panel and all of its wiring: the `RightPanelTab::Outline` variant, the `ShowRightPanelOutline` action and its handler, the tab-strip button, the palette command, the keymap arm, `render_panel_outline`, `TerminalView::{command_marks, scroll_to_mark}`, the `one_line` helper, and four i18n keys.

An existing config value of "outline" falls back to the default Info tab via `de_lenient`, and a user keybinding naming `ShowRightPanelOutline` degrades to a logged warning rather than breaking the keymap.

Also drops what the removal stranded: `Marks::list` is now `cfg(test)` (its last production caller was `command_marks`), and `icons/list.svg` — the Outline button's icon, with no other user — is gone.

Closes #374
2026-08-07 10:48:32 +08:00
l0ng-aiandl0ng-ai 3ae340408b fix(windows): bundle Microsoft's ConPTY so panes can answer color queries (#360)
* fix(windows): bundle Microsoft's ConPTY so panes can answer color queries

The in-box conhost swallows a pane process's OSC 11 background query: it
never reaches tty7's emulator and no reply is ever written back, so
applications that choose a light or dark UI from the terminal background
render a dark UI under a light theme.

tty7 already answers OSC 10/11/12 from the live theme, so nothing was
missing but a pseudoconsole that forwards the question. Microsoft ships one
as a redistributable, and portable-pty already prefers a sideloaded
conpty.dll over kernel32's, so this is packaging rather than code: the pair
goes beside tty7-app.exe, where the DLL search path finds it.

Measured on Windows 11 26200, same binary, only the pair added beside it:

  in-box conhost: the terminal side never sees the query; the client times
                  out with no reply
  bundled ConPTY: the terminal side sees ESC]11;?BEL and a real pane reads
                  back rgb:efef/f1f1/f5f5 under catppuccin_latte, which is
                  the preset's exact background

The two files are one supported unit, so the release verifier fails a
package that carries only one, a mismatched pair, or the MIT notice-less
DLL. They also join PORTABLE_MANAGED_ROOTS, without which the updater would
reject every portable archive that contains them; they are deliberately not
required by verify_portable_payload, since tty7 runs without them and a
packaging slip should fail the release rather than a user's update.

build.rs stages the pair beside cargo's output so a development build does
not quietly run on the in-box host, and the daemon logs which pseudoconsole
it got.

Closes #345

* fix(windows): restage the bundled ConPTY when it goes missing

Watching only the vendored sources meant a staged copy that left the target
directory stayed gone: the build script was cached, so it never ran again to
put it back, and the build silently fell back to the in-box conhost. Cargo
treats a rerun-if-changed path that does not exist as changed, so naming the
destinations makes the staging self-healing.

Found by deleting target/debug/conpty.dll and watching the next build not
bring it back.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-06 11:36:02 +08:00
l0ng-ai 6f9324727c Merge remote-tracking branch 'origin/main' into feat/remote-workspace
The Pi extension conflicted: main gave it the session-id forwarding
doc, this branch had already moved it from `current_exe` to the target
machine's binary. Both kept.

`a_detached_result_lands_after_its_view_is_dropped` now pumps to a
deadline instead of parking once — a `Host` call runs on `HostOps`' own
pool rather than gpui's executor, so there is nothing for
`run_until_parked` to wait on until the answer has already crossed
back.
2026-07-28 22:24:03 +08:00
6d70939824 feat(agents): make Pi a first-class agent with icon and resume (#240)
* feat(agents): make Pi a first-class agent, not a fallback one

Pi panes drew the generic robot glyph every unbranded agent shares, so a
Pi tab was indistinguishable from an Aider or Qwen one in the sidebar,
the tab chip and the tray menu (#225). Auditing the rest of the registry
turned up two more places Pi was on a default rather than handled.

The avatar. Repo practice, from the most recent addition (eced0af,
Grok): take the vendor's mark where it is usable as a 16px silhouette,
otherwise lobehub/lobe-icons' transcription (MIT) with the notice inside
the SVG. Neither applies here — lobe-icons' "Pi" is Inflection AI's
chatbot, a different product whose trademark has no business on this
agent, and Pi itself (earendil-works/pi, MIT) ships no symbol to
transcribe. So this is tty7's own geometric Greek pi on the same 24x24
grid, ~3.3 stroke weight and rounded terminals as the bundled marks,
drawn as a filled silhouette because gpui rasterizes these to a tinted
alpha mask. The letter is not a trademark; nothing is vendored in. The
sky accent and the status dot are unchanged.

Resume. Pi's `--resume`/`-r` is a *boolean* that opens the interactive
picker and `--continue`/`-c` just takes the newest session; the flag
that targets one by id is `--session <path|id>` (its own flag table,
packages/coding-agent/src/cli/args.ts). So the resume command is
`pi --session <id>`, and the stale-flag list gains an arm for the five
ways to name a different session — `--session`, `--session-id`,
`--fork`, `-r`/`-c`, and `--no-session`, which would turn saving off
entirely. `--session-dir` is deliberately not stripped: it says where
sessions live, so the injected id needs it to survive.

None of which pays off unless tty7 knows an id, and it did not — the
generated Pi extension spawned the emitter with stdin ignored, so every
event arrived with session_id: None and resume_command was never
reached. The bridge now reads Pi's id off ctx.sessionManager
.getSessionId() (exposed on the read-only session manager Pi hands each
handler) and pipes it in as the emitter's JSON payload, on session_start
— which also fires for /resume, --fork and new sessions, so a mid-pane
switch re-reports instead of going stale. The load-time presence ping
stays bare; no context exists yet.

Left alone deliberately: aliases, slug, display name, accent and the
hook install/uninstall integration were already correct, and every
render site (tab chip, sidebar row, tray menu, notifications) is generic
over icon_path/accent_rgb/display_name — no other agent changes.

Guard tests, following what the module already does: the fallback set is
pinned by slug so neither adding a mark nor regressing to bot.svg can
pass unnoticed, the Pi resume form and its flag stripping are asserted,
and the Pi bridge is checked for the stdin plumbing whose absence was
the silent half of this bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* no-mistakes(review): skip resume for Pi panes launched with --no-session

* no-mistakes(document): align Pi changelog flag list with stripped flags

* fix(agents): use Pi's own mark for the avatar, not tty7 artwork

The Pi avatar shipped as original artwork on the claim that Pi publishes
no symbol. That claim was wrong: Pi's mark is at pi.dev/logo-auto.svg.
Swap the drawing for the published one and correct the provenance notes
that repeated the claim (the SVG header, the asset-source arm and the
changelog entry).

The published file is the one mark in this set that arrives with a
stylesheet — an 800x800 box whose `prefers-color-scheme` block swaps
black for white. usvg renders it anyway (it applies the base rule and
ignores the media query), so this is normalisation rather than a fix:
geometry rescaled to the 24x24 grid the rest of the set uses, class and
media query dropped for the flat sentinel fill, since gpui and the tray
both tint these as alpha masks. At this size the mark lands on an exact
4x4 grid of 6-unit cells, so the rescale is lossless.

The tray's avatar test now walks the whole roster instead of one branded
and one fallback agent, and asserts the disc came back with more than one
opaque colour. It is the only test that runs the bundled SVGs through
resvg — the asset-source test proves the bytes resolve, not that they
parse into visible geometry — and a mark that parses to nothing renders
as a bare accent disc that nothing else would catch.

Refs #225

* no-mistakes(document): correct Pi changelog icon-grid claim

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 21:34:02 +08:00
l0ng-ai 68683647ee feat(remote): build remote panes off the UI thread, and redraw the switcher
Opening, restoring, closing or listing a remote pane each ran a routed,
blocking connection straight from a gpui input callback, freezing the whole
window for the length of an SSH setup. Pane tree leaves become a PaneSlot that
can be Ready or Connecting, so the slot lands in the layout immediately and the
wait happens in it. connect_routed now carries the same debug UI-thread guard
the Host trait uses, so the class of bug fails loudly instead of shipping.

The workspace switcher is redrawn on one alignment axis with one icon size, a
guide rail tying a remote machine's rows to it, and the command palette's card
geometry. Launch restores only the workspace the user was last in.
2026-07-28 16:32:29 +08:00
l0ng-ai a15f28eb73 feat(icons): redraw the bundled icon set to one humanist spec
All 17 tty7-drawn glyphs are redrawn to a single spec: stroke 2.1, round
caps and joins, radius 3.4-4.4, a near-square 3.4->20.6 box, and filled
nodes at r >= 1.6. Metaphors stay conventional throughout - a magnifier
is a magnifier, a folder is a folder - so nothing needs relearning; what
changed is how they are drawn.

The previous set mixed weights (1.9 with a 2.4 exception for plus), left
folder and folder-closed resolving to one identical drawing, and mixed
terminal treatments across the row. This one holds every glyph to the
same five rules: optical rather than geometric sizing, handles meeting
circles on the tangent, even interior spacing, one terminal treatment,
and equal apparent area rather than equal bounds.

Two notes for future edits, both in the module docs: nothing here relies
on partial alpha surviving rasterisation (the dock block is a flat fill),
and folder-closed carries an inner rule so it stops sharing art with
folder.
2026-07-26 20:15:15 +08:00
l0ng-ai eced0af754 feat(agents): give Grok its brand avatar
Grok was drawing the generic robot glyph on a slate disc — the fallback
picked back when no usable mark was bundled.

xAI publishes its symbol only as a ~2:1 landscape lockup that bleeds off
its own canvas; traced and fitted to a 24x24 box it is unreadable as a
16px silhouette, which is what the tab chip and sidebar render. So the
bundled mark is lobehub/lobe-icons' square transcription (MIT), drawn for
exactly this avatar use. Its notice rides in the SVG.

The slate accent goes with it. That mid-tone exists for vendors whose
monochrome mark is grey or a gradient (Cursor), because a white field
vanishes on a light theme; a black field has no such problem — it stays
darker than even the darkest theme background and the white mark carries
the badge. Grok brands in black, like Codex, so it keeps that.

Adds a guard test: every CLIAgent::icon_path must resolve through the
asset source. A brand mark means touching two files, and forgetting the
registration costs the agent its avatar silently.
2026-07-25 20:17:24 +08:00
l0ng-ai 0ebe68a453 feat(ssh): fold port forwarding and SFTP into the detail panel
Both SSH tools floated over the terminal: a tunnel icon and an SFTP icon
pinned top-right, opening a 460px popover and a bottom dock. They are
pane facts, so they now live where the pane's other facts already are.

Port forwarding becomes a Forwards band on the Info tab, under Ports —
one says what the pane listens on locally, the other what it routes
across the connection. Rows take the panel's language: a mono kind
letter, the bound port as the same chip a listening port gets, hover to
remove, click to edit. The add form is inline, stacked to fit the
column. The list re-lists on the Info tab's existing 2s poll, so a
forward that dies remotely turns red on its own.

SFTP becomes the Files tab's remote mode: the tab follows the detail
pane, showing a local repository tree or that machine's filesystem. Same
browsing model as before (breadcrumb, filter, `..`-led list, per-row
right-click) relaid out for ~260px — the toolbar collapses to refresh
plus a `⋯`, and the permissions column moves into the chmod form, which
now names the mode it is editing. The header carries the hostname:
the tab swaps between two filesystems as the pane changes, and it can
rename and delete.

Transfers become a footer on the panel column rather than a tray inside
SFTP. It sits below every tab, so reading Info doesn't hide a running
upload, and stays pane-scoped rather than aggregating every pane, which
would quietly make the panel a window-level transfer centre.

Opening the browser gained a step: the shell's cwd needs tty7's shell
integration on the remote, which a freshly-connected host rarely has, so
it fell through to `/`. A new SftpOp::Realpath resolves the login
directory instead. Per-pane positions are recorded on arrival, so a
first landing at `/` can no longer be remembered as a preference.

With nothing floating over the terminal any more, the ⌘F find bar gets
its top-right slot back — it used to be suppressed while those icons
were up.
2026-07-25 11:25:06 +08:00
l0ng-ai 1ff907f4cf feat(icons): draw the detail panel and chrome glyphs to one spec
The detail panel mixed four stock lucide glyphs from four different
weights and frames, and the title bar's "+" and "..." were the stock
thin cross and three stroked r=1 rings sitting next to the app's own
filled dock tiles. Side by side that read as glyphs from unrelated sets.

Thirteen icons now share one spec: stroke 1.9, corner radius 2.6-3.2, a
3.2->20.8 optical span on both axes (circles widen a little, since a
round shape reads smaller at equal geometry), and every node filled
rather than stroked -- a stroked dot hazes over below 16px, which is
where the stock ellipsis and list bullets fell apart. "+" is the one
departure: a bare cross has no enclosing shape to carry weight, so it
runs 2.4 on a tighter span.

Two shapes changed metaphor rather than just proportion: Info drops the
circled "i" -- the most-drawn icon there is, and one that says "help" as
readily as "details" -- for a panel with two lines written in it, a
picture of what the tab opens; Changes drops lucide's long arc slung
between two floating rings for an actual commit graph, which also fixes
the 11px branch mark on every sidebar row.

The override is keyed on the asset *path*, and `icon_named!` derives
`IconName` from those same filenames, so each arm redraws its glyph
app-wide rather than only in the panel. Two consequences are handled
here rather than left to discover:

  - `folder` and `file` are redrawn too. Overriding only `folder-open`
    would have split the file tree down the middle -- expanded rows on
    the new spec, collapsed rows and every file still stock.
  - the Settings nav's About row keeps its circled "i", now carried by
    its own `circle-info.svg`. There the glyph labels a section, and a
    document shape says nothing about *About*.

Because the paths resolve on their own, the glyphs need no call-site
changes: `Icon::new(IconName::Search)` already renders the new art.
Call sites stay on the typed enum rather than stringly paths, which a
rename would break silently.
2026-07-25 10:27:23 +08:00
l0ng-ai de3896cbdf feat(right-panel): redesign the detail panel and chrome icons
Rework the detail panel's four tabs and the window chrome to read as a
deliberate, commercial-grade surface while keeping the terminal's calm.

- Chrome icons: filled-block panel toggles; Outline gets a list glyph,
  Changes a git-branch, Info a redrawn mark; glyphs go to 18px on the full
  foreground instead of the faint 15px secondary tone.
- Panel body: labelled Session / Processes / Ports bands, mono values,
  pid/port pills, status dots on Outline, neutral M/U badges on Changes,
  live counts in the headers, and roomier rows. Hue only ever lands on the
  git add / remove / fail semantics, everything else stays neutral grey.
- Fixes: the corner "…" no longer jumps a pixel when the panel opens (the
  top strip mirrors the TitleBar's hidden bottom border); that strip now
  drags the window and double-click zooms, like the rail's; and the process
  list stops flickering — the poll guard now spans the whole 2s cycle
  instead of being restarted by every repaint between ticks.
2026-07-24 16:43:06 +08:00
l0ng-ai 6af47dfb53 feat(brand): "Duo" logo refresh — mint panes mark across all icon assets
Replace the orange window-and-cursor identity with the "Duo" mark: two
offset session panes (mint #3FDD8C behind, ink #17171A in front) with a
prompt chevron, on a cream tile following the Big Sur icon grid.

- app-icon.svg is the master; app-icon.png, tty7.icns and favicon.ico
  are re-rendered from it (rsvg-convert + iconutil + PIL)
- logo.svg/logo.png/logo@256.png carry the tile-less transparent mark
- tray.svg redrawn as the same mark in template form: solid front pane
  with the chevron masked out, back pane at partial alpha
- bare (non-bundled) macOS binaries now set the Dock icon at runtime
  via NSApplication.setApplicationIconImage, so cargo dev/run shows the
  logo instead of the generic executable icon
- README version badge and the social preview switch to the new brand
  green; social preview copy re-aligned with the current README slogan
2026-07-17 16:31:41 +08:00
l0ng-ai 530be0fe5c fix(tray): redesign macOS menu bar glyph for legibility
The tray reused logo.svg (window outline + title-bar rule + colored
cursor block), which turns to mush at 18pt in template mode: the color
flattens to alpha and the icon reads as a card/wallet, not a terminal.

Replace it with a dedicated tray.svg — a bold window outline with a
prompt chevron and underscore, drawn for menu-bar size. logo.svg is
untouched; the tray was its only code consumer.
2026-07-16 17:46:51 +08:00
l0ng-aiandl0ng-ai cd9577c590 feat(agents): recognize CLI coding agents + git branch in the sidebar (#85)
* feat(agents): recognize CLI coding agents + show git branch in the sidebar

Observe (never wrap) third-party coding agents running in a pane — Claude
Code, Codex, Gemini CLI, Aider, Amp, OpenCode and ~10 more — and enrich the
UI around them, plus front each sidebar row with its git branch and diff.

Detection & identity
- Command-based detection over the foreground argv (launcher basename, and
  interpreter-wrapped `node …/cli.js` / `npx …` forms), with user rules via
  `agent_commands` in config. Brand avatars on the tab chip and sidebar row.

Rich status channel
- A per-pane state machine (idle / working / waiting-for-you / done) driven
  by agent-reported events over an OSC 777 sentinel channel
  (`tty7://cli-agent`, versioned JSON), sniffed daemon-side and streamed to
  the client (DaemonMsg::AgentStatus).
- `tty7 agent-hook claude <event>` + a palette installer wire Claude Code's
  lifecycle hooks up; the hook writes the sentinel to the controlling tty
  (with an ancestor-tty fallback for detached hook processes).
- Avatar status dot: working (blue) / waiting (amber) / done (green); an
  unread finished turn gets a crisp outer ring that clears on focus.

Notifications, resume, context feed
- "Needs your permission…" the moment an agent blocks; "finished after Ns"
  per turn, honoring the notify policy (rich turns suppress the coarse exit).
- Session resume: restored panes re-launch their conversation
  (`claude --resume …`), gated by `restore_agent_sessions` (default on).
- Palette commands send the current selection or the repo `git diff` to the
  running agent as a ready-made prompt.

Sidebar git line
- New `terminal::git_status`: off-thread `git` probe (branch, or short sha
  when detached; `git diff --numstat HEAD` line counts) with
  GIT_OPTIONAL_LOCKS=0, refreshed on cwd change or command finish, dropped
  on a stale cwd via a generation tag.
- Each row is avatar + title + `⎇ branch  +N −M` (green/red), sized to
  content; the redundant cwd/"Working…" lines and the aggregate rollup are
  gone — the status dot and branch line carry it.

672 tests pass.

* fix(agents): repair CI and refresh the git line when an agent turn ends

- The live PTY detection test used `sh -c 'exec -a codex cat'`, but
  `exec -a` is a bashism dash (Ubuntu's /bin/sh) rejects — spawn bash.
- cargo fmt over cli_agent.rs / view.rs / app.rs.
- An agent session is one long foreground command, so the back-to-prompt
  edge never refreshed the sidebar's branch/diff line while the agent
  worked — exactly when the working tree changes. poll_agent_status now
  reports a turn ending (transition into Done) and the poll reprobes git
  on that edge too.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-15 14:35:24 +08:00
l0ng-aiandl0ng-ai 4c2082235d revert(branding): restore original terminal-window logo (#59)
The #54 branding refresh replaced the original line-art logo with a
heavier Slate-tile + gradient-ring mark. Restore the original assets
(rounded terminal window frame, title-bar divider, orange cursor block)
across svg/png/icns/ico, reverting to the pre-#54 versions.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-13 10:37:24 +08:00
l0ng-ai c4bee13d83 feat(theme): file-based themes, in-app editor, and a UI/branding refresh (#54)
Replace compiled-in presets + colors.*/ansi_colors.* overrides with a file-based theme system: a serializable seed (bg/fg/accent/cursor/selection + ANSI-16, optional gradient/image/opacity/blur) with all chrome derived, light/dark inferred from luminance behind a WCAG guard, built-ins + user YAML + on-the-fly iTerm2 import via a hot-reloaded registry, and an in-app duplicate-to-edit color editor. Also: prompt-editor shift-click/word-drag selection and cross-platform word keys, ghostty-style tab labels, flat menu highlights, a redesigned app icon, the Background Service -> Daemon rename, and a gated TTY7_PROFILE build-timing probe.
2026-07-12 19:51:07 +08:00