Commit Graph
24 Commits
Author SHA1 Message Date
l0ng-ai b063ba97a0 refactor(settings): fold Window & Tabs into General, drop two settings (#982)
The Window & Tabs page is gone. Its three remaining tab settings (new tab
position, tab bar position, auto grouping) are a Tabs group on General,
below Startup & restore, so the nav has seven sections.

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

The now-unused window settings icon goes with the page.
2026-09-27 19:11:45 +08:00
l0ng-ai 84935813d5 feat(terminal): the mouse wheel no longer zooms the font by default
mouse_zoom_modifier now defaults to none. The platform modifier is cmd on
macOS, held for so much else that the font jumped size mid-scroll (#668).
Picking a modifier in Settings brings the wheel zoom back; cmd+/cmd- are
unchanged.
2026-09-27 10:42:02 +08:00
l0ng-ai 6fcf659e04 feat(search): tabbed Search Everywhere with a Sessions tab to resume agent sessions (#969)
* feat(search): replace the command palette with tabbed Search Everywhere

The palette was one flat list that every new kind of row had to be squeezed
into: tabs and SSH hosts rode along as "Switch to Tab: …" and "SSH: …"
commands, and the only way to narrow to one kind was a magic seed word.

Search Everywhere splits it into sources behind one trait — All, Actions,
Terminals, Hosts — each with its own empty-query layout and ranking. The All
tab shows each source's top rows, ordered by best match, with a row that
opens the full tab. Tab / Shift-Tab walk the tabs and keep the query.

- Terminals lists every open tab of every workspace (reusing the switcher's
  tab rows) and jumps to it wherever it lives, plus the shells and agents.
- Hosts replaces the separate "Add Connection" input: a typed address or
  full `ssh …` line offers to connect.
- Fixes Return doing nothing after a search that found nothing, or when the
  search opens pre-filtered: gpui-component re-picks the row from a stale
  frame; the delegate now re-arms the first row.
- Fixes `ssh -p 2222 me@box` being offered as a quick-connect address with
  user `ssh -p 2222 me`.

The keymap action stays `TogglePalette` so custom bindings keep working.

* feat(search): a Sessions tab to resume past agent sessions

Search Everywhere gains a Sessions tab listing the Claude Code and Codex
sessions on this computer, read from ~/.claude/projects and
~/.codex/sessions ($CODEX_HOME). Sessions that ran in the focused tab's
directory lead; the All tab offers the last three of them before anything
is typed. Return opens a new tab in the session's directory and runs the
agent's resume command with its configured launch flags.

- Only each transcript's head and tail are read, off the window thread,
  and cached by path, size and mtime: ~170ms cold for 50 sessions,
  under 1ms warm. The search opens on the cached list and fills in.
- Titles: /rename name, then the agent's own title (ai-title, Codex's
  session_index.jsonl), then the first thing typed, skipping harness
  injections. Codex rollouts it ran for itself (subagent/internal) and
  sessions never asked anything are left out.
- A session whose directory is gone is refused with a notice rather than
  resumed where the agent cannot find it.
2026-09-27 09:38:31 +08:00
l0ng-ai 39ceb35fdd feat(cursor): give the shell prompt its own cursor shape (#963)
A new prompt_cursor_style setting (follow, block, bar, underline) shapes
the caret while the shell waits at a prompt, whether tty7's inline editor
or the shell's own line editor draws it. `follow`, the default, keeps
cursor_style everywhere, so nothing changes until it is set. Any other
value leaves cursor_style to the programs the shell runs: bar here with
cursor_style block gives kitty and ghostty's bar at the prompt and a block
inside a TUI that never sets a shape, such as Claude Code. A vi-mode
prompt keeps the shell's own insert/normal shapes.

Settings shows both cursor shapes as dropdowns, kept in step with resets,
language switches and config edits made outside the window.

Closes #958
2026-09-27 09:35:18 +08:00
l0ng-ai cf0e8f02e6 feat(sidebar): pinned groups above auto groups (#962)
* feat(sidebar): keep pinned groups on the workspace and derive the rest

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

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

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

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

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

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

Every way into a pinned group the design calls for:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- `TerminalView::native_ssh_cwd` and its helper existed for the sidebar's old
  folder grouping of native SSH panes; an SSH tab now groups by host, and
  nothing else read it.
- The file tree's context menu takes `cx` instead of `danger` and the new
  groups flag, back to the argument count it had on main.
- A title test builds its workspace in the initializer.
2026-09-25 16:53:26 +08:00
l0ng-ai afcb8aa2dd feat(agents): quick launch for detected CLI agents (#961)
* feat(agents): quick launch for detected CLI agents (#955)

Every agent whose launch program is on PATH becomes a palette command,
"Agent: <name>", ordered by frecency and bindable as LaunchAgent:<slug>.
"New Agent Tab" (Cmd+Shift+A on macOS; unbound elsewhere, where
Ctrl+Shift+A is select-all) launches the most recently used one, and the
New Tab menu gains a single "Launch Agent..." row that opens the palette
pre-filtered to them.

A launch always opens a new pane (a tab in the active tab's cwd, or a
split when picked from the palette with Alt held) and types the command
into that pane's shell once it exists - immediately for a local pane, on
landing for a remote one via PendingSpawn::run_on_land - never into a
pane that was already there. Detection, status and resume then work as
for a hand-typed agent.

The command is the agent's bare binary unless the new `agent_launch`
config map overrides it. A wrapper named there is detected as its agent
without an `agent_commands` entry: the daemon folds the programs
`agent_launch` runs into its alias map (interpreters, shells and real
agent names excepted), reloaded when config.json changes instead of
once per process, and a mapped script run under its interpreter
(`bash ~/bin/cc`, `node cc.js`) is now recognised too.

A running agent's pane menu gets "Set Current Launch Args as Default",
which writes its launch argv - minus session flags and positional
prompts, joined with the settings' quoting - into `agent_launch`.

Remote workspaces cannot be asked for their PATH through the Host
trait, so they offer the agents previously seen running in that
workspace (WindowView::seen_agents).

* fix(agents): count only agents that start under a view, not ones reattached to

A view rebuilt over a running pane - every agent tab after an app
restart, or a workspace switched back to - saw its agent appear from
nothing and reported it as detected, bumping that agent's frecency once
per launch of the app. The terminal now remembers whether its link was
an attach, and such a view only reports agents once its shell has been
seen back at the prompt with no agent in front.

Also pins down that the agents seen in a remote workspace, its quick
launch list, persist in views.json with the rest of the workspace.

* fix(daemon): probe the foreground as soon as a new program takes it

The foreground probes ran on output only, at most once per 500ms
interval. A quick launch types the agent's command the moment the shell
is up, so the agent drew its whole first screen inside the interval of
the prompt it was typed at and then waited for a key: the pane never
learned it was running an agent until something else printed. Seen in
a dev instance, where a launched Claude Code stayed undetected at its
trust prompt.

The reader now asks the pty for its foreground process group on every
read (one ioctl) and probes immediately when it changes.
2026-09-25 16:47:59 +08:00
l0ng-ai 27483e893d feat(ui): collapse the New Tab menu's shell list to three by frecency (#960)
The + menu listed every shell the machine reports (nine on a stock macOS box)
above the SSH hosts. The Local section now names the default shell, always
first, then only shells that have actually been opened, by frecency, three rows
at most, the same way the SSH section caps its hosts.

Shell usage is recorded in a new `shell_frecency` config map, keyed by the
inventory label, bumped from both the menu row and the palette. Every shell is
now a palette command titled "Shell: {label}" (same word in every locale), and
an "Other Shells…" row opens the palette pre-filtered to them. That row is
hidden when the menu already names the whole inventory. Running a shell command
from the palette respects the ⌥/Alt split modifier like the menu row.
2026-09-25 16:44:32 +08:00
l0ng-ai de15f9b0ab feat(ssh): keep saved hosts in servers.json and add an SSH tab title setting (#952)
Saved SSH hosts and their usage counts move out of config.json into
servers.json beside it, so config.json can be synced between machines
without carrying a server list (#911). An older config.json is split on
first load: servers.json is written first (0600), then only the two keys
are removed from config.json, leaving every other key as it was. When
both files hold hosts, servers.json wins and the stale copy falls out of
config.json at its next save. A servers.json that does not parse is kept
aside and blocks saves, as config.json does; hand edits hot-reload.

Settings -> Window & Tabs -> SSH tab title (`ssh_tab_title`) pins an SSH
tab to the profile name (saved host name, ~/.ssh/config alias, or the
address typed for a quick connect) or the hostname, on the OSC title's
rung of the existing label ladder: a renamed tab still wins, OSC titles
are still tracked, local panes are untouched (#726).
2026-09-25 16:34:38 +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 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 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 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 03c3081a6e feat(ports): detect and forward what a remote pane is serving
A remote workspace's ports were never listed. The pane lives in the
peer's registry and QueryProcs asks this machine's daemon, which has
never heard of it, so the answer was an empty list — indistinguishable
on screen from a pane serving nothing. Add a control request so the peer
answers instead, gated on a feature so an older server says "I cannot
tell you" rather than "nothing is listening".

With the ports visible, the forward becomes something the user should
not have to think about: a port opens on a click, and a new one is
forwarded unasked, at the same number where that number is free here.
The watch runs with the panel shut, which is when a port appearing is
most worth saying something about.

Ports and Forwards were two sections that never mentioned each other; a
row is now a port, and the forward is where that row says it comes out.
Adding one by hand asks for one number instead of five fields, with the
rest of the ssh -L grammar one disclosure away.

Claude-Session: https://claude.ai/code/session_01TPXrptp2rCGKjXaz4xE3Lq
2026-09-08 21:01:50 +08:00
ayamirandl0ng-ai e231b16fb3 feat(ssh): allow remote image clipboard writes (#766)
* feat(ssh): allow remote image clipboard writes

* fix(ssh): keep a profile's clipboard grant across a re-attach

A native ssh pane's OSC 5522 permission is decided by the spec that
dialled the host, and the daemon is the only side that holds it. A window
reopening onto a pane that outlived it attaches by pane id, has no spec
to read, and sends `allow_remote_clipboard_write: false` — which the
daemon took as the new answer and the pane's own view took as a refusal.
Both sides then said no, so the first restart after switching the
permission on turned every copy into an `EPERM` with the switch still
reading "on".

Pin the spec's answer in the pane and route both attach and detach
through one decision point, so a pane that carries a spec keeps that
spec's answer whatever an attaching client claims, and a pane without one
— everything on a remote `tty7-server` — is exactly as permitted as its
controller says. On the client side, refuse only what the pane can see is
forbidden and leave the verdict to the daemon otherwise.

Also: release a failed transfer's buffered bytes instead of parking up to
`MAX_CLIPBOARD_BYTES` per pane until the next request, and answer the
capability probe with the permission actually in force rather than a
constant that always reads as "off".

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-02 14:12:28 +08:00
oldhuandl0ng-ai 0b6c332618 feat(settings): add interface font family configuration in appearance typography (#761)
* feat(settings): add interface font family configuration in appearance typography

* fix(settings): let the interface font go back to the system face

Three things the new **Interface font family** row spelled once and needed
twice.

`apply_theme` only wrote `Theme.font_family` when the setting was `Some`,
and `Theme::change` never puts it back — it rewrites the field only when a
theme config names a face, and none of ours does. So picking a font worked,
and picking **Default** back saved `None`, redrew every window in the font
the user had just cleared, and only came true at the next launch: a setting
that looked like it had applied instantly and had not. The face is now
assigned in both directions, against the stock value read once before
anything overrode it.

The dropdown's first row borrowed the bold/italic label, "Default (match
primary)" — which promises the *terminal's* primary family. The interface
falls back to the system UI font instead, so the row said the chrome would
come out in Hack while the description beside it said the opposite. It gets
its own label in all three locales.

`ui_font_family` was also the one key in `config.json` that disappeared when
unset; every other optional key is written as `null`. Dropped the
`skip_serializing_if` so the file still lists it, and documented the key in
the two tables that enumerate the typography settings.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-09-02 14:07:59 +08:00
l0ng-ai 958d8b7442 feat(window): dock the code panel and the diff overlay beside the terminal (#625) (#685)
* feat(window): dock the code panel and the diff overlay beside the terminal (#625)

Opening a file covered the workspace. The terminal underneath kept
running and was neither visible nor typeable, so reading a file while an
agent talked was a toggle loop: open it, close it to read the reply, open
it again. The Files tree already docks; the two surfaces you go to *from*
it did not.

They dock now, as a flex sibling of the terminal column rather than a
narrower overlay — that distinction is the feature. `set_grid_size` is
driven by the terminal element's laid-out bounds, so a column takes width
away from the grid and the PTY reflows into what is left; a card painted
over half the workspace would have left the grid full width with half of
it hidden.

`overlay_top` stops ordering a pair and starts choosing between them: a
column has one child, and two `flex_1` siblings would split it and fight.
Fill mode keeps the old vector, the old opaque paint and the old platform
hoist untouched, so nothing about today's overlay changes for anyone who
picks it.

- Half the terminal column by default; drag the divider, double-click it
  to cycle a third / half / two thirds, or use the palette commands. Two
  thirds deliberately runs past the half-window cap the side panels obey
  — only the terminal's floor binds it.
- `DOCUMENT_MIN_W` joins the width budget: both side panels reserve it
  the way they already reserve each other, and the column is derived from
  the *live* sidebar and panel widths rather than their floors, so a
  panel someone dragged wider is width the terminal keeps.
- A window too narrow to seat both fills for that frame. The fallback is
  derived at render time and never stored, so widening re-docks on the
  next frame with nothing to undo.
- Fill or dock is per tab, on the header's context menu. Reading a long
  file over the whole window in one tab while an agent keeps half of
  another is the normal case, and one global switch made each of those
  flip the other. A tab that has not been told reads `document_layout`
  from the config, which is what a fresh tab starts as — and which the
  menu therefore does not write, since every untold tab is reading it.
- Everywhere but macOS the title bar spans the workspace, which left a
  bar's height of nothing above the column. The header is drawn into it,
  and behaves like the title bar it now sits in. With the detail panel
  closed the column reaches the window's right edge, so the header stops
  short of the trailing chrome through a width the tab strip's own
  reservation shares.
- The docked headers drop the traffic-light inset they never had to
  clear, and the diff header's branch name becomes the thing that yields
  so the view toggle and the close tile survive a column's width.

New in `config.json`: `document_ratio`, and `document_layout` for what a
fresh tab starts as. Four new actions, bindable and unbound by default.

* fix(window): hold the docked column to widths the strip and the file agree on

Three defects in the document column, each with a guard test that fails
without its fix.

The tab strip did not know a column had taken width off it. On macOS the
strip lives inside the terminal column and sizes itself to the window less
the detail panel, so a docked document left it 340 points wider than the
column it sits in and the chips ran on under the column — the same overrun
the panel's own reservation was added for. Everywhere else the strip spans
the workspace and the column's hoisted header is drawn over its trailing
end with no fill of its own, so a chip left under it showed through the
file name and stayed clickable through it. The column's width now comes off
`strip_w` on macOS and off `corner_w` elsewhere, which is where the panel's
already goes.

The divider wrote widths the file would not keep. `Config::sanitize` holds
`document_ratio` to 0.2..=0.8; the drag clamped in pixels only, so a column
pushed against either edge of a wide window was saved outside that band and
reopened somewhere else — on a 2560-point body, 232 points from where it
was dropped. The band is a pair of shared constants now and the drag clamps
to it, the way the font size and its stepper were made to agree in #550.

The palette named the config's layout rather than the tab's. Fill is per
tab, so a tab told to fill was still offered "Document: Fill Window" — a
row that named the state it was already in and did the opposite. It reads
the active tab through `ChromeState` now.

Also: `document_layout`'s doc comment still described the global switch an
earlier draft had, three lines after the field became a per-tab default.
2026-08-19 18:03:51 +08:00
l0ng-ai ef333bf055 feat(terminal): make the wheel-zoom modifier configurable (#676)
Cmd-scroll zoomed the font with no way to move it or switch it off, so a
thumb left on Cmd resized the terminal mid-scroll (#668). The modifier is
now a setting: the platform modifier by default, or Ctrl, Alt, or none.

Stored as the choice rather than the resolved key, so one config file
still means the same thing on a Mac and on a Linux box. Settings ->
Terminal -> Mouse carries the picker; off macOS Ctrl and the platform
modifier are the same key, so it shows one cell for them.
2026-08-18 12:16:17 +08:00
webdev 422808191d feat(sidebar): group a tab by its folder when its cwd is not a repo (#631)
`sidebar_grouping` gains a third, opt-in mode, `repo-or-directory`: group by repository home as before, and when the repo probe has landed and answered "not a repo", group under the cwd itself instead of filing every such tab under Scratch. A probe that has not run yet resolves to no decision, so a tab keeps the group it already has rather than bouncing through Scratch mid-probe. The decision lives in one `resolved_group` free function shared by the per-frame key derivation and spawn-time seeding.

The default (`repo`) and flat modes behave exactly as before, and an unknown value in an existing config still degrades to `repo`.

Knock-on: `machine_mirror::subject_path_of` names a window after its most common group, so in the new mode a window of plain shells takes its name from the most common directory rather than from the first pane's cwd.

Closes #620.
2026-08-14 15:57:36 +08:00
webdev 0346e35b40 fix(shell): stop injecting into a zsh or fish the user gave arguments to (#629)
The zsh and fish arms of `shell_integration::setup` never checked `has_custom_args`, so a shell the user launched with their own arguments was injected anyway — fish had `-C <script>` appended to its argv, zsh had its ZDOTDIR swapped. Both arms now sit behind the same gate bash, PowerShell and WSL already used, hoisted to a single early return ahead of the dispatch so a new ShellKind cannot silently reintroduce the bug.

Docs now describe what the code does: the `shell` row's own `{"program": "fish", "args": ["-l"]}` example loses integration under this rule, and the shell-integration note distinguishes user-written arguments from the ones detection supplies (Git Bash, WSL).

Part of #624; the native-input-mode half is separate.
2026-08-14 15:57:20 +08:00
l0ng-aiandl0ng-ai 72db26d15a feat(prompt): let the shell's own line editor own the prompt (#633)
Closes #624

tty7's inline editor takes the prompt the moment OSC 133 reports one, and
until now the only way to keep it off was to hide the shell's own name
from tty7 so integration never armed — which costs the prompt boundaries,
cwd and exit codes as well. Someone who binds `history-beginning-search-
backward-end` to Up in their zshrc had no way to reach it, and the local
history the editor walks instead is per-view: a command run in one pane is
not in another's list, so the shell's shared history looked broken too.

The new `prompt_editor` switch (Settings -> Input -> Prompt, on by
default) hands the line back. Off, every key at the prompt goes to the
PTY, so ZLE / readline / fish do the editing and what the user bound
behaves as written. Shell integration is untouched by it.

The gate is one line in `input_inactive_reason`, which every path that
could take the prompt from the shell already asks: keys, IME commits,
paste, Tab, the completion and reverse-search menus, the input bar. That
is what makes this a mode rather than a special case per key.

`shell_owns_prompt` learns the flag too, and that half matters more than
it looks: the gap hold and the typeahead record both exist to feed the
local editor, and `flush_typeahead` sends ^U to erase the line before
moving it there — on a line only ZLE is editing, that erases the user's
work. Ctrl-R landing on the PTY also stops raising the missing-integration
notice: the shell owning it is what was asked for.

Turning it off mid-line hands what is typed to the shell the way an
unknown chord does, so the text is still on the prompt to finish. Live
panes follow the switch, including a hand edit of config.json in another
window.

Tab completion and history search are menus tty7 opens inside that editor,
so the page greys them out and says why while it is off. Only their text
dims — a switch already draws its thumb at 35% when disabled, and dimming
the row on top of that leaves a pill with nothing visible in it. Their
stored values are left alone and come back with the editor.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-14 14:46:22 +08:00
l0ng-aiandl0ng-ai d343fd8a13 feat(links): open file links in tty7, resolved on the pane's own host (#568)
* feat(links): open file links in tty7, resolved on the pane's own host

A clicked file path now opens in the built-in editor at the line and
column the link named, and the Files panel reveals it; a directory link
opens the panel on that directory. Settings -> Terminal -> Links -> Open
files with picks between the built-in editor, the OS file association
and a command, migrating anyone who had already set link_file_command.

Detection is split into a filesystem-free candidate parser and a probe
callback, so a pane whose paths live on another machine resolves them
there instead of against the local filesystem -- an absolute path used
to open this machine's copy silently. A pane running ssh typed into a
local shell can answer for neither side and no longer offers file links
at all.

Relative paths are measured from the directory the work is happening in
(the agent's, not the shell's kernel cwd) and then from the repository
around it, and a path that matches nothing under either now says so
instead of the click doing nothing.

* fix(links): keep a remote path off the local openers, and off a dead end

Review follow-ups on the file-link work.

- A file resolved on another machine now opens in the built-in editor
  whatever `link_file_open` says. Under `system` or `command` the path was
  handed to a local `open` / `code --goto`, which threw away the resolution
  just done on the pane's host and silently showed this machine's copy — the
  same bug this branch set out to fix, left live for two of the three modes.
  A directory outside every tree root says so instead of opening a local file
  manager on a path that belongs to the far side.

- `flush_link_probes` takes the host before it takes the wanted paths.
  `take_wanted` moves them into the in-flight set on the promise that a call
  is carrying them; a host that had gone away broke that promise for good and
  left those paths permanently unanswered — no underline, and a click that
  says nothing.

- `~` no longer borrows this machine's `$HOME` for a pane whose paths are
  elsewhere. A cwd outside `/home` and `/Users` used to fall back to it, so
  `~/.zshrc` on a Linux box became `/Users/me/.zshrc` and was asked about —
  and possibly answered — over there.

- An unresolved absolute or `~`-rooted path no longer claims it was looked
  for under the pane's directory. It never was: roots are only for relative
  paths.

- A pending tree reveal counts down whether or not its row was found. A row
  that never reported bounds kept the request alive for good, re-issuing a
  scroll on every render and holding the column against a hand scroll.

- The repo root comes from `GitStatusCache` when the git-status probe has
  already asked about that directory, rather than a second round trip.

Tests: the migration `link_file_open` exists for (an old config with a
command lands on Command, one without on the editor), a probe with no host
staying wanted, and `~` refusing this machine's home for another one.

* test(links): only claim a leading slash is absolute where it is

`is_rooted` asks `Path::is_absolute`, the same question `FileCandidate::paths`
asks before it decides the roots do not apply — and on Windows `/etc/hosts`
answers no to both. The predicate is consistent; the assertion was not, so it
now lives in a unix-gated test of its own next to the untouched one.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-12 22:48:16 +08:00
webdev 4da3868797 feat(shells): let the new-tab menu carry entries the user wrote (#534)
Closes #443
2026-08-12 10:52:35 +08:00
l0ng-aiandl0ng-ai 707fd1867b docs: add a Mintlify documentation site (#478)
38 pages under docs/, written against the source rather than the README:
config keys and their clamps from core::config, default keybindings from
ui::keymap, every CLI verb and flag from tty7-cli, agent aliases and
hook/fork/resume support from core::cli_agent, and Settings paths taken
from the actual en-US strings.

docs/features.md and its zh-CN translation are retired — everything in
them now lives in a page of its own, plus the two things they carried
that nothing else did (IME input, the performance notes). README and
README.zh-CN point at docs/ instead.

Screenshots and videos are placeholders for now: docs/images/placeholder.svg
with a caption naming what each shot should be.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-11 00:38:11 +08:00