Commit Graph
308 Commits
Author SHA1 Message Date
575d2cd68e fix(agents): drop the previous session id when the foreground agent changes (#957)
同一 pane 里前台从 Claude 换成 omp,或别的 agent 的 hook 写进这个 TTY 时,旧的 session id 会留在新 agent 上,Fork 就会拿错 id。

Co-authored-by: stevelliu <stevelliu@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 13:52:36 +08:00
l0ng-ai f852629ba1 fix(procinfo): read proc_listallpids' answer as a pid count, not bytes (#947)
libproc's proc_listallpids takes its buffer size in bytes but answers
with the number of pids, and the macOS process table read that number
as bytes twice: once sizing the buffer (count / 4 + 64 slots) and once
reading back how much was filled (count / 4 again). On a Mac with ~700
processes only the first ~60 pids reached the table.

The kernel lists its newest processes first, so what survived was
whatever started most recently. A `go run` server launched a moment ago
was in; the pane's shell, started long before, usually was not, and a
walk from a root missing from the table returns nothing: no processes,
no ports, and a probe state of Ok. A freshly built parent/child chain,
which is what a quick check of the probe builds, is exactly the case
that happened to work.

The listing now goes through list_all_pids, which takes the call as a
closure so the size arithmetic is tested off a Mac against an emulation
of libproc's convention.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-25 13:49:28 +08:00
l0ng-ai d18f797a6c Merge pull request #945 from l0ng-ai/fix/942-wsl-files-panel
fix(files): keep a WSL tree in step with its distro (#942)
2026-09-23 21:38:06 +08:00
l0ng-ai 8fa9651814 fix(files): keep a WSL tree in step with its distro (#942)
Three things went wrong with a Files panel rooted on a distro's \\wsl$
share:

- A drop brought a `name:Zone.Identifier` file along with every
  downloaded file. fs::copy is CopyFileEx on Windows, which copies the
  NTFS alternate data streams, and the share has no streams to keep one
  in, so it lands as a file of its own. A copy onto a WSL share now
  moves the contents only; one onto NTFS still goes through fs::copy
  and keeps the mark.
- A file removed from the shell in the distro stayed in the tree. The
  share accepts a ReadDirectoryChangesW and never reports a change, so
  the watch looked healthy and was deaf. Directories on a WSL share are
  now watched by a notify PollWatcher every 2s instead, made the first
  time one is needed.
- Right-clicking a row did not select it, so nothing marked the row the
  menu was about. A right click now selects the row without opening it.
2026-09-23 19:26:26 +08:00
l0ng-ai 439f19ff85 Stop waiting on remote round trips from the UI thread
Dropping a RemoteWatch sent WatchClose with a blocking call, and the last
handle is usually let go on the UI thread: from Tty7App::render via
scm_sync_watchers, and from scm_watch_opened when an open lands after its
subscription moved on. Every window froze for a round trip each time, up
to WatchClose's 5 s deadline on a quiet link. Sampling a live instance
caught ~1.1 s of such stalls in 10 s.

- ControlClient::post sends a request without registering for its reply;
  the reader already drops replies nobody is waiting for.
- RemoteWatch::drop posts WatchClose instead of calling it.
- pane_workspace_for built the full SSH spec, keychain lookups included,
  only to strip the secrets again. It now builds it from NoCredentials,
  which takes the securityd trips off pane_liveness::sweep.

Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
2026-09-23 19:00:54 +08:00
l0ng-ai 7666e2fd83 Merge pull request #941 from l0ng-ai/feat/852-portable-mode
feat(windows): portable mode keeps data beside the executable
2026-09-23 16:58:12 +08:00
l0ng-ai 386ccc3d81 Merge pull request #939 from l0ng-ai/feat/720-font-smoothing
feat(fonts): make macOS stroke thickening configurable
2026-09-23 16:58:03 +08:00
l0ng-ai 296a8a3c3f Merge pull request #938 from l0ng-ai/feat/760-stable-workspace-numbers
feat(workspaces): number workspaces in a stable order (#760)
2026-09-23 16:57:59 +08:00
l0ng-ai a012321542 Merge pull request #936 from l0ng-ai/feat/892-741-codebuddy-cursor-hooks
feat(agents): CodeBuddy CLI support and Cursor CLI hooks
2026-09-23 16:57:50 +08:00
l0ng-ai 3d4c158278 fix(agents): open Cursor turns on beforeSubmitPrompt, count tool calls as activity
The interactive cursor-agent TUI fires beforeSubmitPrompt and stop; only
print mode (-p) lacks them. Mapping postToolUse to prompt-submit froze the
activity counter (opportunistic git refresh, agents --changed), shortened
turn timing and hid tool-less turns. Map it to tool-complete instead.
2026-09-23 16:54:06 +08:00
l0ng-ai f5a97ed68c feat(windows): portable mode keeps data beside the executable (#852)
A portable ZIP install whose folder has both the .tty7-portable marker
and a data\ folder next to tty7-app.exe now uses <exe dir>\data as its
config directory, so settings, sessions, scrollback and the daemon's
socket travel with the program. --config-dir and TTY7_CONFIG_DIR still
win. The folder is an explicit opt-in because the marker has shipped in
every portable ZIP since the in-app updater arrived: keying on it alone
would move existing users onto an empty directory at their next update.

The portable directory is not the machine's config dir, so a portable
copy never adopts the installed tty7's legacy tree. An updater test pins
that data\ is not a managed root and survives an in-place update.
2026-09-23 15:44:27 +08:00
l0ng-ai 57ca21dd68 feat(workspaces): number workspaces in a stable order (#760)
SelectWorkspace1-9 and the Window menu numbered workspaces by most
recent use, so switching to one moved it to slot 1 and reshuffled the
rest. Number them by the order this client first had them instead
(the append-only views list), skipping synced remote references until
they are opened; opening one moves it to the end so it takes the next
free number. The switcher keeps its MRU list and shows each row's
number.
2026-09-23 15:38:28 +08:00
l0ng-ai 592058acc7 feat(fonts): make macOS stroke thickening configurable (#720)
Add a macOS-only `font_thicken` key (default true) and a Settings row
under Appearance > Terminal text. When off, AppleFontSmoothing is pinned
to 0 in this process's NSArgumentDomain before gpui's text system first
reads it, so glyphs render at the face's own weight. The volatile domain
is in-memory only: nothing is persisted and no other app is affected.
gpui caches the preference in a OnceLock, so a change applies after a
restart; no gpui fork change is needed.
2026-09-23 15:23:10 +08:00
l0ng-ai fe7b6e0b76 feat(agents): wire Cursor CLI (cursor-agent) into agent hooks (#741)
Cursor's ~/.cursor/hooks.json is a flat hook map — `command` directly on
each entry under `hooks.<event>` — so it reuses the flat writer Crush
introduced. Two things are Cursor-specific:

- The file needs `"version": 1` at its root or Cursor ignores it. tty7
  now writes it when it creates the file or finds it missing, never
  overwrites a version the user set, and reports a versionless file that
  holds our hooks as Outdated so refresh repairs it.
- Payloads name the session `conversation_id` (only sessionStart repeats
  it as `session_id`), and most events carry `workspace_roots` instead of
  `cwd`. Both are now read as aliases, which is what Copy Session ID and
  resume were missing.

Events: sessionStart, beforeSubmitPrompt, postToolUse, stop, sessionEnd.
cursor-agent does not fire beforeSubmitPrompt today (a known gap on
Cursor's side), so postToolUse also reports prompt-submit: the first tool
call opens the turn and Cursor's stop, which the CLI does fire, closes it.
A turn with no tool calls never shows as working. None of Cursor's
permission hooks are installed, since those would block on the empty
stdout tty7's hook prints.
2026-09-23 15:21:34 +08:00
l0ng-ai 9f842975ce feat(editor): add ToggleDocumentPreview / ToggleDocumentWrap actions (#754)
The code panel's Preview/Edit and Wrap buttons only toggled state in their
click handlers, so there was no way to reach them from the keyboard. Register
both as actions, unbound by default like ToggleDocumentFill and the
DocumentWidth* actions, list them in the command palette and the Keybindings
page, and route the buttons through the same methods.

Instead of config keys for the initial state, the last-used state is
remembered (editor_soft_wrap / editor_markdown_preview), the way diff_view
and scm_graph_expanded already are. A file opened at a line target always
opens as source so the cursor is visible.
2026-09-23 15:18:58 +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 d534c085b9 Merge pull request #930 from l0ng-ai/chore/bump-control-dialect
chore(control): bump the dialect to v10 so remote hosts pick up daemon fixes
2026-09-23 12:34:32 +08:00
l0ng-ai 611c87af0c Merge pull request #931 from l0ng-ai/feat/palette-update-server
feat(palette): add "Update tty7 server" for this computer and the remote host
2026-09-23 12:34:29 +08:00
l0ng-ai 4ee61bb09d Merge origin/main into fix/857-stream-escape-split
Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
2026-09-23 12:34:17 +08:00
l0ng-ai 35891e1b64 feat(palette): add "Update tty7 server" for this computer and the remote host
Local: restarts the local daemon onto the app's build through the existing
restart flow and confirmation, or says it is already current when the probed
daemon reports this build and no mismatch is pending.

Remote: a new RouteAction::UpdateServer makes the local daemon force-install
this build's server over one already speaking our dialect
(Installer::replace_forced: upload to a temp name, probe, rename, then
cycle_daemon), for SSH and WSL. Offered only when the window's workspace is
on a host whose server we install; gated on the local daemon advertising
FEATURE_UPDATE_SERVER, since an older one cannot decode the action.

Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
2026-09-23 12:31:17 +08:00
l0ng-ai a4205e6255 chore(control): bump the dialect to v10 so remote hosts pick up daemon fixes
No message changes. A same-dialect install trusts whatever binary already
sits at tty7-server-c9p6 and Update Server only restarts it, so daemon-side
fixes (#828 modes restored on re-attach, #857 replay ring / clipboard
sniffer) never reached remote hosts. A new filename makes clients upload
the matching server.

Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
2026-09-23 12:18:37 +08:00
l0ng-ai f1244015c5 fix(daemon): never cut the replay ring or the clipboard sniffer mid-sequence (#857)
Two places could turn the tail of an escape sequence into visible text.

The replay ring evicts from the front at an arbitrary byte once it reaches
its cap, and a replay starts the client's emulator in its ground state. A
cut through Pi's `ESC ] 133;C BEL` line mark was painted as `33;C` on every
re-attach; a cut through a CJK character as a replacement glyph. The ring
now folds evicted bytes through a minimal VT state machine and keeps
evicting until the front is outside every sequence and not on a UTF-8
continuation byte.

The OSC 5522 tokenizer dropped a held `ESC ]` (plus any undecided
identifier bytes) and the `ESC` that interrupted it, so `ESC ]` split from
`ESC [31m` by a read boundary reached the client as literal `[31m`. It now
forwards those bytes and lets the new sequence be judged normally; only an
interrupted clipboard write's own payload is still dropped.

The daemon's out-of-band stripping moves into `lift_out_of_band` so tests
can drive it: a Pi-style streamed frame passes through byte for byte at
every split point, and the client's emulator parses it to the same grid at
every split point.
2026-09-23 11:49:09 +08:00
l0ng-ai b635fa57fb fix(ssh): retire a routed password prompt its attempt gave up on (#820)
#827 stopped a declined prompt from being asked again, but left the
prompts themselves with no notion of whether anyone was still waiting on
them. A routed auth prompt sat in the mailbox, in the parked queue behind
the sheet on screen, or on screen itself, until somebody answered it —
even after the connection attempt that raised it had timed out and moved
on. Answering it sent the secret into a dropped channel.

That is the report's sequence. A link drops while nobody is at the
keyboard; each reconnect attempt raises a password prompt and times out
unanswered, and before #827 the supervisor dialled again and again, so one
dead prompt per attempt piled up behind the first sheet. The user comes
back, types the password into a sheet nobody is listening to, the next one
comes up, one of them happens to be the live attempt and connects — and
the dead ones keep coming up however they are closed. #827 ends the
attempt loop on a timed-out password, but a key passphrase declined by
timeout still falls through to other methods and a transient failure, and
a single stale sheet is still left on screen either way.

A PendingAuth now carries a weak handle whose only strong count lives in
the responder for as long as it waits, so it can say when it has been
abandoned. The pump drops abandoned prompts instead of raising or parking
them, and takes down an on-screen routed sheet whose asker has gone,
moving on to whatever else is asking. The GUI also waits no longer than
the daemon's handshake does (the broker's 120s rather than 180s), so the
sheet comes down when the attempt behind it actually fails, not a minute
later.
2026-09-23 11:31:00 +08:00
l0ng-ai 65b50491d3 Merge pull request #918 from l0ng-ai/fix/config-symlink-write
fix(config): write through symlinks instead of replacing them
2026-09-23 10:20:10 +08:00
l0ng-ai 40183a5044 fix(daemon): always re-exec once instead of trusting the responsible pid
responsibility_get_pid_responsible_for_pid reports a process as its own
responsible process both when it truly is and when the one it inherited
has exited. A daemon that outlived its GUI - the state a handoff is
meant to repair - therefore looked already disclaimed and was skipped.

Verified headless: a daemon from origin/main launched by a process that
then exits gives new panes no attribution (the shell reports itself);
an in-place restart to this build keeps the pid and existing shells, and
new panes then report the daemon as responsible; a fresh start does too.
2026-09-23 09:55:52 +08:00
l0ng-ai 821dde716c fix(daemon): disclaim the launcher's responsibility on macOS (#909)
macOS attributes Local Network (and other TCC) decisions to a process's
responsible process, fixed at spawn and inherited from the parent. The
daemon is spawned by the GUI, so it and every shell it forks answered to
that GUI. Once the GUI exits while the daemon lives on - every in-place
update, a crash, a force-quit - new panes are no longer attributed to
tty7.app and non-platform binaries get EHOSTUNREACH on the LAN until a
full quit-and-relaunch.

At the top of run_daemon, re-exec in place via posix_spawn with
POSIX_SPAWN_SETEXEC and responsibility_spawnattrs_setdisclaim, so the
daemon (tty7.app's own executable) is responsible for itself. Running
there also covers the far side of a handoff, which repairs a daemon
started by an older build without losing its panes. The SPI is looked
up with dlsym; if it is missing or anything fails, startup continues as
before. A marker argument prevents a second attempt if the first did not
take.
2026-09-23 09:47:27 +08:00
l0ng-ai 2430acb4ba fix(config): write through symlinks instead of replacing them
write_atomic renamed its temp file over the given path. When that path is
a symlink (config.json linked into a dotfiles repo), rename(2) replaces
the link itself, so the first save — dragging the sidebar, running a
palette command — silently turned it into a plain file and broke sync.

Resolve the symlink chain first and write next to, and rename over, the
file it finally points at. Covers every write_atomic caller (config.json,
views.json, window.json, machines, presets, agent hooks).
2026-09-23 09:40:08 +08:00
l0ng-ai ae89ff0110 Merge main and preserve SSH credential saving in settings workflows 2026-09-23 08:54:49 +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 f9e8c6eda1 Merge pull request #908 from l0ng-ai/fix/889-sticky-osc-tab-title
fix: retire an OSC 0/2 tab title when the command that set it exits (#889)
2026-09-23 00:38:59 +08:00
l0ng-ai a4dbdc7bf1 Redesign settings navigation, search, and editing workflows 2026-09-23 00:24:38 +08:00
l0ng-ai 96e5f23eae fix: retire a replayed title whose C mark rolled out of the ring (#889)
A window that reattaches to a pane mid-command (app restart during a long
agent session) replays the daemon's 8 MiB output ring. Once the session
outgrows it, the command's 133;C is gone while its OSC 0/2 titles remain,
so TitleLifetime treated them as prompt titles and the command's D never
retired them: the tab stayed stuck exactly as in #889, while the daemon's
own record had already cleared.

The replayed Prompt state already says a command owns the pane. When it
does and the replay carried no prompt mark at all, everything replayed was
written under that command, so seed the reader's TitleLifetime as running
with a command-owned title.
2026-09-23 00:13:34 +08:00
l0ng-ai 6846571142 Refine native UI hierarchy, icons, and sidebar chrome 2026-09-22 23:20:31 +08:00
l0ng-ai 404b6afd1d fix: retire an OSC 0/2 title when the command that set it exits (#889)
A tab went on reading "✳ fixing the switcher" long after Claude Code had
quit and the pane was back at its own prompt in a real directory. An OSC
0/2 had no owner and no end — only another OSC 0/2 ever replaced it — so
the last title any program wrote in a pane outlived it forever, and the
`Osc` rung of the label ladder kept outranking the `Cwd` below it with a
name for a session that no longer existed.

The shell integration already says when a command starts and stops. A new
`core::osc::TitleLifetime` reads OSC 133;C / 133;D alongside the titles:
a title set *between* them belongs to that command and is retired by its
`D`; a title set at a prompt — the shell's own, or one pinned by hand —
belongs to nobody in particular and is left alone; a pane with no shell
integration sees neither mark and keeps every title, exactly as before.

Both readers run it over the same bytes, in stream order, so the tab strip
and the switcher can never disagree about whether a title is still current:

- the daemon's `OscSniffer` turns a retirement into the reset it already
  understood, clearing `PaneRecord::osc_title` for the switcher and CLI;
- the window's pane reader sends `AlacEvent::ResetTitle` after the chunk
  the emulator just parsed, so its own terminal's title goes back to the
  pane's default and `stated_title` says nothing.

Stream order is what keeps a re-titling shell whole: tty7's zsh helper
prepends the `D` emitter to precmd and the PowerShell one titles inside
its prompt function, so a shell's own OSC 0/2 lands after the `D` and is
simply the last word rather than something to undo.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-21 22:47:13 +08:00
l0ng-ai a723e71c9a fix(keybindings): give Alt+1..9 a way out, and stop one bad line resetting the file (#901)
Alt+1..9 are vim's tab keys, and tty7 takes all nine for Go to Tab.
Two things stood between the reporter and getting them back.

**Nothing in the app could leave an action unbound.** Backspace on a
Keybindings row that has recorded nothing *reset* the row — dropped the
override so the action gets its shipped chord back. On a row nobody had
overridden, which is every row the first time it is looked at, that is a
no-op: pressing it over Alt+1 left Alt+1 sitting exactly where it was,
which reads as the default restoring itself. `config.json` has spelled
"no chord" as `[]` since #868, but no gesture wrote it.

Backspace now writes that empty list. The row falls to `—` and grows the
**Reset** button every overridden row has, which is the way back to the
default. The capture hint names the key, and the docs say what it is for.

**A keybinding line serde could not read failed the whole `Config`.**
`keybindings` is a hand-edited map and was strict, so `"ActivateTab1":
null` — or a number, or an object — quarantined `config.json` and started
the app on built-in defaults. Every rebinding in the file then read as
its shipped default, and the next settings write persisted those
defaults over what the user had written. It now reads one entry at a
time, like every other hand-edited nested key here: the lines that name
a shortcut bind, a line that does not is logged and skipped.

Tests, each failing on the unfixed code:

- `ui::app::keybinding_gpui_tests::backspace_on_a_row_unbinds_the_action_rather_than_restoring_its_default`
- `core::config::tests::a_keybinding_line_that_cannot_be_read_does_not_take_the_config_with_it`

and `ui::keymap::gpui_tests::alt_digits_can_be_moved_off_the_tab_actions_for_good`
pins the merge and a save/reload round trip: a list replaces the shipped
Alt+1, `[]` leaves nothing, and neither comes back after a restart.

Fixes #901

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-21 22:42:45 +08:00
l0ng-ai d07850a98e Merge pull request #887 from l0ng-ai/fix/868-additive-keybindings
fix(keymap): a keybinding in config adds a chord instead of replacing the default (#868)
2026-09-15 23:26:50 +08:00
l0ng-ai 436ea04f33 fix(keymap): a keybinding in config adds a chord instead of replacing the default (#868)
effective_bindings kept one chord per action and set_binding overwrote that
slot, so "NextTab": "cmd-shift-]" silently took Ctrl+Tab away.

A string in keybindings now adds a chord beside the action's default (or
preset) chord; "" still unbinds, as configs and the docs already rely on; a
list is the exact chord set, [] unbinds. Configured chords are installed after
every shipped one, so a chord the user names wins a tie with another action's
default. The Settings page lists every chord of an action, and recording a
shortcut writes the list shape (it sets the binding) and takes only the stolen
chord from the action that had it.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:48:19 +08:00
l0ng-ai ef55505a6a fix(agents): don't re-badge a turn the reader already saw when a pane is rebuilt
Switching workspaces or reopening a window from the tray throws a pane's
TerminalView away and builds a new one over the same daemon pane. The new
view starts with no last status, so an agent that was already Done arrives
as None -> Done, which poll_agent_status cannot tell from a turn finishing
live, and every unfocused rebuilt pane got its unread badge back.

The daemon now counts finished turns per agent session (turns, bumped on
entering Done), and views leave an app-lifetime mark per (host, pane) of the
session, turn count and badge the reader was last shown. A rebuilt view's
first sight of Done takes the badge back from a matching mark instead of
raising a new one; a turn that finished while the view was gone has no mark
or a lower count, and still badges.

Fixes #870

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:45:15 +08:00
l0ng-ai 37c2ef2176 test(keymap): pin additive keybinding semantics (#868)
Regression tests for issue #868: a chord added in config.json must join the
action's default chord rather than replace it, an empty string or list must
still unbind, a list replaces the chord set, the tmux preset composes, a user
chord wins a tie with another action's default, and Settings recordings write
the exact-set shape.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:40:42 +08:00
l0ng-ai 611c6030a4 Merge pull request #881 from akhenakh/crush
added support for Crush agent
2026-09-14 18:56:50 +08:00
l0ng-ai 85b4450756 fix(agents): keep Crush's PreToolUse from opening a turn it can never close
Crush fires only PreToolUse, and it mapped to prompt-submit. With no Stop
behind it the pane stayed on Working until Crush exited: every close asked
whether to cut Crush's work short, the tray and dot stayed on working, and
fork warned mid-turn. Map it to tool-complete, which still records
session_id/cwd for resume and bumps activity without moving the status.
2026-09-14 18:31:59 +08:00
l0ng-ai 2de26bbf35 fix(pi): restore the pre-prompt status when a UI prompt closes
ui_prompt_end sent prompt-submit unconditionally, but Pi also opens
prompts while idle (/model, a command's select). Closing one left a
finished or fresh pane reading "working" with no agent_end to clear it.
Remember the last turn event and re-emit that instead.
2026-09-14 18:31:45 +08:00
Fabrice Aneche 3fefb647e1 test(agents): isolate Crush hook test from XDG_CONFIG_HOME 2026-09-13 20:59:42 -04:00
netcatty a8da265087 Add Pi UI-prompt events so the pane reports waiting-for-user
The shared Pi / Oh My Pi bridge only subscribes to four lifecycle events, so
a pane running Pi never shows "waiting for you" while a dialog is open —
`ask_user_question`, permission gates and any other extension prompt all run
under the hood with the status dot still reading "working".

tty7 already has the vocabulary for this (`question-asked`,
`permission-request`), and `AgentEventKind::QuestionAsked` /
`PermissionRequest` already map to `AgentStatus::Waiting` in
`cli_agent.rs`. The Pi extension seam to feed them is
`pi.on("ui_prompt_start")`, which fires around every blocking user-facing
prompt with `event.kind` telling select / confirm / input / editor / custom
apart.

Two handlers, each guarded on its own so an Oh My Pi fork that does not
expose the hook loses only that event rather than the whole bridge:

- `ui_prompt_start` → `permission-request` for `kind === "confirm"` (a
  permission or destructive-action gate), `question-asked` otherwise.
- `ui_prompt_end` → `prompt-submit` so the status returns to working once the
  dialog closes. Without it the pane would stay on "waiting" until the next
  stop, which is wrong for the model's continued work after an answer.

Deliberately not included: `tool-complete`. The Pi bridge emits with
`spawnSync`, so one event per tool call would block the extension host for
the duration of a process spawn on every read/grep/edit.

The test that asserts the bridge's subscriptions now covers both new event
names.

Verified: the `format!` template still compiles and renders both new
handlers, and the bridge contains every string the test asserts.
2026-09-13 15:47:07 +08:00
Fabrice Aneche c2d2db2cfc added support for Crush agent 2026-09-11 21:56:22 -04:00
l0ng-ai 66c42e48b7 Merge pull request #873 from WhiteDG/feat/qodercli
feat(agents): add Qoder CLI integration
2026-09-11 15:00:07 +08:00
l0ng-ai 63b3951ef3 fix(agents): detect Qoder through the binary its docs tell you to run
The npm package installs two binaries. `qoder` is a dispatcher that routes
to the CLI for a bare invocation, a flag, or a prompt, and only hands off to
the IDE for `ide`/`chat`/`serve-web`/`tunnel` or a path that exists — and it
is the one the documentation tells people to run. Both are
`#!/usr/bin/env node` scripts, so what the pty carries is node plus the path
to the shim; the dispatcher's child, where `qodercli` appears on the path,
is not the process group leader and is never read. Detecting `qodercli`
alone missed every session started the documented way.

An IDE launch now wears the CLI's avatar for as long as the launcher takes
to exit, which is the cost of covering the common case.

Also: `--session-id` restores a session rather than naming a new one, so say
that where the flag is stripped, and assert Qoder has no `Notification` seat
instead of putting a payload through a hook map that has none.

Claude-Session: https://claude.ai/code/session_01LAqfzqELnoDWU56LBXS1Nh
2026-09-11 14:38:40 +08: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
hhdebb 56238bf3bb fix(tabs): take the agent's status mark off the title it writes (#847)
Agents animate in the terminal title while they work, and they do not
agree on an alphabet: Claude Code cycles the quadrant circles and rests
on an asterisk, others step through the braille frames, some write
nothing at all. Rendered as they arrive, a column of tabs carries a mark
in front of some rows and not others, in three vocabularies — while the
row already says what the agent is doing, in one, with its status dot.

So the mark comes off, for everyone, with no setting. A switch would not
settle this: nobody opens settings to decide how a spinner is drawn, and
a default-off toggle buys two render paths to maintain forever in order
to answer a question that has one right answer per person and no way for
the app to know which.

**A known alphabet, not a shape.** The obvious rule — a leading character
that is non-ASCII and above some code point, followed by a space — matches
by shape, and `🔥 build`, or `📁 ~/repo` written by somebody's shell
integration, fits it exactly and quietly loses its first character with no
way to ask for it back and no clue as to what took it. Matching marks we
have actually seen costs the same and cannot do that: the braille block,
the four quadrant circles, and Claude Code's resting asterisk. When an
agent invents a mark that is not on the list, the failure is today's
behaviour — the mark stays — which is the safe direction to fail in, and
adding it is a line in the table.

Two things the rule insists on, both to keep it from reaching past what
it is for. A mark only counts with whitespace behind it, so `✳fixing` is
a word that starts with a character rather than a mark in front of one.
And a title that is *only* a mark keeps it: taking it would leave an empty
string, and an empty title is not a tab called nothing, it is a tab that
falls back to its number — less than the mark was saying.

It happens in `TabView::label`, which is where a title becomes a label, so
the strip, the sidebar, the switcher and the rename box's prefill all
agree without being told separately — and, because `label` reaches a
given name before it reaches the title, a tab somebody deliberately
called `✳ release` keeps what they called it. That ordering is the only
thing standing between a user's name and a rename behind their back, so
there is a test on it rather than a comment. Three existing tests carried
`✳` in their fixtures and now expect it gone. The one in `switcher.rs` was
asserting that a tab in another window is named the way a local one would
be, which is still exactly what it asserts; the one in `tty7-cli` is the
table getting this for free, since `tab_label` reads `label` and so
`tty7 ls` says what the tab strip says without either being told about the
other. The daemon's fixtures keep their marks on purpose: a title is stored
as the terminal wrote it, and only what turns one into a label takes
anything off.

This leaves the row with nothing moving in it, which is a real loss and is
answered separately: `AgentStatus::dot_rgb` returns three flat colours,
and a `Working` dot that breathes says the same thing in the vocabulary
the row already speaks.
2026-09-11 01:10:21 +08:00
l0ng-ai ab26166f96 fix(review): keep pane_procs's doc on pane_procs, round rtt before the unit
The new `link_rtt` landed between `pane_procs`'s doc comment and
`pane_procs` itself, so the comment about walking pane process trees
documented the latency probe instead.

`format_rtt` also compared the unrounded milliseconds against 1000, so a
999.6 ms round trip printed as "1000 ms" — a millisecond reading past
the range the millisecond branch exists to cover. Round first, then pick
the unit.

Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER
2026-09-10 18:19:39 +08:00