Commit Graph
36 Commits
Author SHA1 Message Date
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
l0ng-aiandl0ng-ai 21b7f82392 feat(completion): execute dynamic generators for live candidates (#52)
* feat(completion): execute dynamic generators for live candidates

The completion engine consumed Fig specs' static shape but never ran
their dynamic generators, so positions whose candidates come from the
live system — ssh hosts, git branches — fell through to filesystem
path completion (#51: ssh <Tab> listed the cwd).

Local-only by design: the pure engine returns each pending script, the
view runs it on the background executor (/bin/sh -c in the session
cwd, 800ms timeout, kill-on-drop, 256KiB stdout cap, 5s TTL cache) and
merges the parsed lines into the open menu, generation-tagged so a
result can't outlive its session. A per-script parser registry ports
the specs' dropped postProcess transforms (git markers, docker
{{json .}}, package.json scripts, …); unmatched scripts default to
one-candidate-per-line, and hopeless outputs are suppressed rather
than inserted as garbage.

ssh/scp/sftp/rsync specs gain host generators reading ~/.ssh/config
(Include-aware, wildcard patterns skipped) and known_hosts (hashed
entries skipped, [host]:port unwrapped).

Fixes #51

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>

* docs(comments): describe borrowed conventions directly

Prior-art name-drops in comments aged poorly as the implementations
diverged; keep the behavioral rationale, drop the product citations.

* fix(completion): make generator execution CI-portable

Linux: sh -c may fork the command instead of exec'ing it, so killing
only the shell on timeout left a grandchild holding the stdout pipe —
the reader (and the caller) then blocked until the grandchild exited
on its own. Spawn the child as its own process-group leader and kill
the group; the timeout test now forces the fork case (trailing true)
so the group-kill is what's actually proven.

Windows: generator scripts are POSIX sh + awk, so the execution path
now compiles to no-suggestions there instead of failing at runtime on
a missing /bin/sh; the process-spawning tests are Unix-only to match.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-11 19:12:10 +08:00
l0ng-ai 22e1ab1694 tty7: a GPU-rendered, daemon-backed terminal in pure Rust
tty7 is split into two Rust processes: a persistent daemon that owns the
shells and a GPU-rendered client that talks to it over a local socket.
Because the shells live in the daemon, quitting and reopening the app
leaves the session intact — detach and reattach, no tmux required.

- Persistent sessions — the daemon holds the PTYs and child processes, so
  closing a window or swapping in a new build never takes a shell down.
- Performance — an 11 MB `cat` completes in 95 ms and DOOM-fire renders at
  888 fps; the daemon drains the PTY at device speed off the render path.
- Shell-aware — new tabs and splits open in the current working directory;
  zsh, bash, fish, and PowerShell are set up automatically.
- Enhanced prompt — inline completion, syntax highlighting, history, and
  in-terminal search, with rich flag/subcommand signatures for common tools.
- Tabs, resizable splits, a command palette, click-to-open links, desktop
  notifications, eight themes, and CJK/IME input.

Native builds for macOS, Windows, and Linux.
Built on Zed's gpui and Alacritty's VT core.
2026-07-06 21:54:27 +08:00