* fix(ui): make Cancel the default button on the file-tree delete confirmation
The file tree's delete prompt was the only destructive prompt in tty7 with
the destructive action first. On macOS (NSAlert) and Windows (TaskDialog)
the first button is the Return-key default, so pressing Return deleted -
including recursive folder deletion. Linux uses gpui's fallback renderer,
which is click-only, so the swap only reorders the buttons there.
Safe option first, matching every other destructive prompt; the literal
"Cancel" is what gpui maps to PromptButton::Cancel and the Escape key.
* fix(ui): stop saying "Finder" on Linux and Windows
The file-tree context menu and the SFTP job tooltip hardcoded
Finder-flavoured labels; only the right panel's Info row was
platform-conditional. Extract that conditional into
right_panel::reveal_label() and use it at all three sites, so the action
reads "Reveal in Finder" on macOS and "Open Folder" elsewhere.
Side effect of sharing the helper: the SFTP tooltip's "Show in Finder"
becomes "Reveal in Finder" on macOS, converging a third name for the
same action.
* fix(settings): index the Grok Build agent row and re-align drifted index titles
The Grok Build agent renders a settings row but had no search-index
entry, so the row was unreachable by search. The other five agent
entries carried mechanism suffixes ("Claude Code hooks", "Pi
extension") that no longer match the rendered rows, which are titled by
HookAgent::display_name(); the index said "Option acts as Meta" where
the row says "Option (⌥) acts as Meta".
Align the titles with the rows, keep the mechanism words as search
keywords, and extend the index tests: the pinned-title list gains the
Option row, and a new test derives the Agents entries from
HookAgent::ALL so adding or renaming an agent without updating the index
fails the suite.
* no-mistakes(review): add plain search keywords for Option-acts-as-Meta entry
* no-mistakes(document): document copy fixes in changelog and apply rustfmt
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(file-tree): stop a watcher event repainting a window with nothing to draw
Issue #243 made two claims. The flicker was fixed independently on main by
4814d94 "fix(remote): keep file-tree listings on screen while they refresh",
which reached the same mechanism separately and went further on two axes. This
is only the other claim — the one in the issue's title, and the one still true
on main: `file_tree_apply_fs_events` ended in an unconditional `cx.notify()`, so
every batch from a watcher that is recursive over the root repainted the whole
window. The overwhelming majority of those batches name a directory the tree has
never listed and does not show: `.git` internals, build output, `node_modules`.
A window with nothing new to draw was being asked to draw, several times a
second, for as long as anything under the root was being written.
* `invalidate_dir` reports whether it reached anything the tree holds, and a
batch that reached nothing returns without repainting.
* What it did reach is re-read from the callback itself, rather than by
notifying in order to buy the paint that would have re-read it.
* `land_load` returns `Landed { superseded, changed }` and the caller repaints
only on `changed`, so a file rewritten in a directory on screen costs no
frames. `TreeEntry` gains `PartialEq` for that comparison.
* The `.gitignore` whole-cache branch is scoped by `gitignore_reaches_tree`:
patterns at `D/.gitignore` govern `D` and below, so unless a tracked directory
sits under `D` there is nothing to refresh. Its `cx.notify()` is deliberately
kept when the branch *is* taken — `invalidate_all` restarts the search and
only a paint re-walks it. `InFlight::pending_keys` is new, for the in-flight
half of that test.
Measured, not asserted. `Tty7App::render` feeds a per-thread draw counter under
`cfg(test)`, and gpui's test build redraws dirty windows from inside
`flush_effects` — so a headless window plus that counter answers "does this
reach render idle?" with no compositor and without the reporter's Wayland
session. Supporting seams: `test_window::harness` / `harness_with_pane` and
`terminal::view::quiet_test_pane`. `render_probe::arm` takes a draw budget so a
repaint-loop regression fails the test instead of hanging the suite — the loop
lives inside one `flush_effects` call and nothing outside it can interrupt it.
Render idle is measured as "stops drawing", not "never draws again": settling
legitimately costs a last frame as the final listing lands, so the count is
taken over a second interval once the first has absorbed that tail. Confirmed
rather than assumed — the count is 1 at 3s and still 1 at 12s.
Numbers, each taken by reverting the hunk it belongs to and re-running:
five writes under an unlisted directory 5 draws -> 0
five rewrites of a displayed file 10 draws -> 0
five .gitignore writes under node_modules 10 draws -> 0
marks left unread after a gitignore refresh 1 -> 0
The four controls — a settled panel on a non-empty, an empty and a hidden-only
directory, and a real change still arriving — pass either way, which is the
point: they were never the bug. `untracked_paths_leave_no_bookkeeping_behind`
also passes either way, because main's `invalidate_dir` already only marks what
is cached; it is a guard, not a fix.
Two things stated rather than faked. The end-to-end through a real OS watcher
did not survive: the watcher moved into the host layer, which a synthetic
`file_tree_apply_fs_events` cannot drive. For the same reason the gitignore test
asserts the marking and the re-read rather than the recomputed `ignored` flags,
which the host owns — and says so where it stops.
This commit replaces four earlier ones on this branch, squashed because they
were written against the single-host `file_tree` that no longer exists and could
not be replayed onto it. What each contributed:
* 744a08d fix(file-tree): stop the watcher blanking the panel and repainting for
nothing — the original. Its flicker half is dropped in favour of 4814d94; its
render-idle half is what this commit is.
* a88544c no-mistakes(review): scope gitignore refresh to the tree and cover
in-flight loads — the `gitignore_reaches_tree` scoping, kept. Its other half,
covering in-flight loads in the whole-cache refresh, is not carried: main's
`invalidate_all` already stales them via `InFlight::invalidate_all`.
* 0ff3b64 no-mistakes(document): correct stale watcher comment in file_tree docs
— folded into the doc comments here.
* f0e33a4 no-mistakes(document): drop tracked AGENTS.md, move render-probe caveat
into source — the deletion is moot on this base (the file was never added),
and the caveat it relocated is in `render_probe`'s doc comment.
Separately and deliberately not fixed here: on macOS the watcher reports paths
through `/private/var` while the cache is keyed by the root as handed in, so a
root reached via a symlink never matches and its changes are missed. Pre-existing
and invisible on Linux; the tests canonicalize around it and say so.
1484 tests pass across the workspace, fmt clean, clippy unchanged from
origin/main's baseline at 57 warnings. The app was not built, launched or driven;
visual acceptance is the owner's.
Refs #243.
* no-mistakes(review): correct watch scope claims, repaint on moved root, skip hidden-panel reads
* no-mistakes(review): exclude the SFTP column from the tree-drawn gate, fix stale docs
Also carries the correction the earlier messages on this branch owe the reader.
The first commit's message (and the CHANGELOG entry it shipped with) asserted
that the file tree watches its root **recursively**, and justified the whole
change on the traffic that supposedly produced: `.git` internals, build output,
everything under `node_modules`. That was wrong, not merely imprecise. The watch
is non-recursive — `sync_watch`'s own doc says so, it covers roots plus expanded
directories, and `WatchedDirs::translate` enforces it per backend. The recursive
watcher belonged to the older single-host design; the premise was carried across
the port to the host-keyed tree without being re-checked.
What is actually reachable, and all this now claims: the tree hears about a
change in a directory it is *displaying*, and a file's contents being rewritten
reports exactly as loudly as a file appearing. Comparing the re-read against
what is already on screen is what stops that repainting a window with nothing
new to draw.
Measured against that reachable case only, by reverting the comparison and
re-running: five rewrites of a file in a displayed directory cost 10 frames and
now cost 0. The figures the first message quoted for writes under an unlisted
directory and for `.gitignore` writes under `node_modules` are withdrawn — the
tests behind them synthesised watcher events this watch cannot deliver, so they
described scenarios the system cannot produce. Those tests have been removed or
reframed as guards on the predicate rather than evidence of a live symptom.
The other corrections in this round:
* A `.git` create or delete cleared the repo-root cache and then took the new
early return, so nothing re-resolved it and a moved repository root no longer
re-rooted the tree on an idle window. That regression came in with this change
and is closed.
* The watcher-driven re-read was not gated on the tree being drawn, so a hidden
panel did filesystem work it never used to do. It is gated now.
* That gate then assumed the Files tab always draws the local tree, which it
does not: a connected native-SSH pane substitutes the SFTP browser and the
local tree is never rendered. The predicate accounts for that too.
* Two stale statements of the recursive premise survived the first sweep, in
`assets.rs` and `code_editor.rs`, and are corrected.
The SFTP-substitution case is covered by a test at the predicate; the
substitution itself is a render-path branch with no headless seam, so what is
asserted is the predicate's answer rather than the panel's output.
* no-mistakes(review): gate watcher re-read on listings drawn, fix stale docs
* no-mistakes(review): delete withdrawn recursive-watch CHANGELOG entry duplicated by rebases
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(ui): make every header draggable, and keep the arm alive across a repaint (#221)
Two changes to the same code, which is why they land together.
Five rows that stand in for the title bar — the tab rail's top zone, the
settings page's top strip, the detail panel's top zone, and the code and
diff overlays' headers — armed their drag with an `Rc<Cell<bool>>`
allocated inside the render function. A redraw between the press and the
first drag event handed the next frame's listeners a fresh, zeroed cell
while the press had written to the old one, so the whole hold was dead
until you released and tried again.
The press itself schedules that redraw: these rows carry `on_double_click`,
and gpui calls `window.refresh()` on mouse-down for any element with a
click listener. So a drag only survived if the first move beat the next
vsync — 16ms at 60Hz, 8ms on ProMotion. A mouse press physically nudges the
pointer and often won that race; a trackpad press is a finger pushing down
without translating, and almost never did. That is the trackpad-vs-mouse
split the issue reports. The terminal's cursor blink (a 530ms `cx.notify()`
loop) disarms it on its own even with no press at all.
`window_move_gesture` now holds the flag in `window.use_keyed_state`, which
survives frames — where gpui-component's own `TitleBar` has always kept it,
and why the ordinary caption strip was never affected. Keyed rather than
`use_state` because one builder serves several call sites and `use_state`'s
`CodeLocation` id would collide when two of these rows are on screen at
once (the rail's top zone plus an overlay header is a real combination).
A longer-lived flag has to be cleared explicitly, so releasing outside the
row disarms too; with a per-frame cell the frame boundary did that for free.
Nothing else about these rows changes — same hit boxes, same geometry, same
`WindowControlArea::Drag`, same double-click.
Grabbing the window by a header is a property of the whole app, not a
per-surface feature, so a user never has to learn which rows are draggable.
Written down beside `window_move_gesture`, along with the two things it
takes beyond arming the gesture: non-controls inside a header take no hit
box (the rule #202 set for the "duo" mark, so the drag falls through them),
and a header whose contents *do* take hit boxes by design needs a floor on
its flexible spacer.
- `panel_title` — the detail panel's section header, shared by Info,
Outline, Changes, Files and the remote Files browser — is draggable now.
Its one un-`occlude()`d control (SFTP's refresh tile) gains the wrapper
every control on a drag row needs, or Windows' HTCAPTION eats its clicks.
- The horizontal tab strip keeps a bare 80px slice of caption. Its spacer
was a `flex_1` with no minimum, so it collapsed to exactly 0px once the
chips saturated the row (~7-8 tabs on a 1440px window), leaving only three
6px gaps and a hairline above and below the chips to grab — the "the
region that works seems very small" half of the report. The chip row's
fixed-chrome reserve is corrected to match: a stale flat 100px, sized when
the corner held a 30px "+" and a 30px "⋯", becomes the ~137px the corner
actually occupies plus the handle. Chips reach their minimum width and
truncate a tab or two sooner, and the window is always grabbable.
- The rail's top-zone spacer gains the same floor.
`ui::app::window_drag_tests` drives the real `title_bar_drag` row through
gpui's test platform, where `start_window_move` is `unimplemented!()` and a
panic is therefore a reliable "the window would have moved" detector. It
pins the invariant (press → repaint → move still drags), that a press alone
does not, that a release disarms, and that two rows on screen keep separate
arms. A control test keeps the old per-frame-cell pattern alongside and
asserts it still loses the drag to the identical event sequence — without
it, the invariant test could pass for the wrong reason.
* no-mistakes(review): occlude resize handles; correct chip-reserve arithmetic
* no-mistakes(document): reorder changelog sections; record non-draggable header exclusions
* no-mistakes(document): make panel grab-handle docs version-neutral and platform-accurate
* no-mistakes(document): make workspace_head panel-width doc version-neutral
* docs(changelog): re-file Unreleased entries after the rebase onto main
The rebase onto 64403cf applied every hunk without a conflict and still
produced a wrong file, which is the failure mode worth naming: this
branch's "reorder the Unreleased sections" commit moved its own entries
to Added -> Changed -> Fixed, and replaying that on a main whose
Unreleased had grown three new entries wedged this branch's ### Changed
and ### Fixed headings into the middle of main's ### Added list.
The result had two of *other people's* entries — "Fork an agent session"
and "Copy Session ID", both Added, both from #211 — orphaned under this
branch's ### Fixed, and a duplicate ### Changed / ### Fixed pair further
down. Git had nothing to complain about; the text merged cleanly and the
meaning did not.
Restored to main's structure with this branch's two entries filed under
the headings they belong to. No entry text changed on either side; all
seven Unreleased entries are present, verified against the union of both
parents.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(keymap): make the prompt editor's soft newline a bindable action
Shift+Enter and Opt/Alt+Enter have inserted a literal newline into the
command editor since the multi-line prompt editor landed in 54825d3, and
only a plain Enter submits. But the chords were hardcoded in the editor's
key handler, so `InsertNewline` was not a name the keymap knew: nothing to
put in `keybindings` in config.json, no row in Settings -> Keybindings, and
no way to move the gesture to a chord of your own.
Register it like every other action. `InsertNewline` is Terminal-scoped
(the handler lives on the terminal surface, as ClearScrollback and the find
trio already are) and the "enter" arm of the editor dispatcher no longer
looks at modifiers at all — the keymap dispatches the action before the key
reaches the dispatcher, so there is one implementation rather than two that
can drift.
The action ships with both of today's chords. A binding spec cannot express
alternatives — whitespace in a spec means a sequence, `ctrl-b n`-style —
and the effective table holds exactly one keystroke per action, so the
table carries shift-enter and alt-enter is installed alongside it, but only
while the action still sits on its default. Rebind it and both old chords
are retired, which is what moving a binding is supposed to mean.
The prompt editor alone answers it. With a foreground application on the
pane, the search field focused, or a completion menu / reverse search
holding the keyboard, the handler propagates, so the chord takes the exact
path it took before this action existed — a full-screen program still
receives Shift+Enter as its own chord under the Kitty protocol. Plain Enter
still submits, and the secondary-enter / secondary-shift-enter window
bindings are untouched.
Refs #182
* fix(prompt): let the newline chords through an open completion menu
Follow-up to the InsertNewline action, from review findings, resolved
against Warp as the reference implementation.
The action declined whenever a completion menu was open, on the theory
that propagating preserved the old behaviour. It did for Shift+Enter,
which used to reach the picker's accept-line arm, but not for Alt+Enter:
the picker branch is gated on `!m.alt`, so Alt+Enter skipped it entirely
and fell through to the enter arm's newline. Propagating sent it to an
enter arm that now unconditionally submits, so Alt+Enter with a menu open
ran the command.
Warp does not let the popup take these chords at all. Only a bare Enter
reaches the popup-acceptance path (a fixed "enter" binding routed into
`input_enter`, where menus and the completion popup consume it); Shift+
Enter, Alt+Enter and Ctrl+J dispatch their own actions, which the editor
resolves as a newline against `EnterSettings` without the popup ever
seeing them. Its TUI has no popup in the input's dispatch path at all.
So insert unconditionally and close the menu — a newline ends the word
being completed, and a menu still filtered on that word would be stale.
Shift+Enter with a menu open therefore changes from accept-line to
insert, deliberately and in the direction of the reference; Alt+Enter
goes back to inserting, as it did before the action landed. Plain Enter
keeps accepting the highlighted candidate.
Also repeat the dispatcher's per-key state resets (`editor_goal_col`,
`last_word_nav`), which the inline branch inherited by running inside
`handle_editor_key` and the action bypassed — the same reason
`commit_text` repeats them for the IME path.
Shift+Alt+Enter, which the old `(m.shift || m.alt)` test caught by
accident, is left submitting: gpui matches modifiers exactly, and Warp's
key table has no arm for that chord either. Recorded in a comment and a
test so it is not "restored" later as a missing default.
Refs #182
* no-mistakes(review): scope rebind's NoAction to its context; see extra chords
* no-mistakes(document): document rebindable InsertNewline chords; fix rustfmt and changelog link
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(ui): stop filled children squaring off rounded corners
The cursor-shape toggles (Block / Bar / Underline) reported in #236 look
rough because the selected segment's fill covers the whole corner of the
track it caps, and its outer edge is a hard, unantialiased vertical cut.
The track's own border arc is drawn correctly and antialiased — it just
floats *inside* that square, so the corner reads as a stair-step.
The controls were relying on `overflow_hidden` to shape their end
segments' fills to the track's rounding. It cannot do that.
`gpui::ContentMask` is a bare axis-aligned `Bounds`; `Style::overflow_mask`
builds it from the element's bounds shrunk by the border widths and drops
`corner_radii` entirely, and every shader applies it as a hard
`clip_distances < 0` discard. So the mask only ever cuts a square, and it
never antialiases the cut. A container's own corners come from somewhere
else — the quad shader's SDF, `saturate(0.5 - distance)` — which is why a
plain rounded card renders smooth while anything with a filled child in
its corner does not. That divergence is the whole bug, and the reporter's
screenshot shows both halves of it: the corner with the selected fill is
square, the corner without one is a clean arc.
The fill has to carry the radius itself, so it goes down the SDF path too.
It sits one border-width inside the track, so the concentric radius is
`outer - border`. `ui::rounding` states that rule once, with the
constants and the corner-assignment helpers, and unit-tests the
invariants (inset is strictly tighter than the outer radius, clamps at
zero, only the end segments cap the track).
Applied to every place a child paints a fill into a rounded corner:
* the segmented controls (the reported one, plus the others `segmented`
serves),
* the −/value/+ steppers' hover fills — those glyph boxes also had to be
pinned to the track's content height, because a padded auto-height box
measures 31px against a 22px content box and its rounded corner would
land 4½px outside the visible strip,
* the theme picker's flush-mounted previews,
* the diff overlay's card headers and the row that closes a card.
Not reproducible locally: this is a rendering-geometry defect, not a
platform one, but it is most visible at a device pixel ratio of 1, where
the clip's hard edge is a whole physical pixel. Verified by reading the
gpui mask/shader source and the reporter's screenshot pixel by pixel, and
by the geometry tests; the on-screen result is left for visual
acceptance.
Refs #236
* no-mistakes(review): round diff card header when body is empty
* no-mistakes(document): point Unreleased changelog link at v26.7.6
* no-mistakes(document): untrack AGENTS.md per gitignore dev-tool convention
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(agents): fork an agent session, and copy its session id
A coding-agent conversation is a single thread: to try a risky direction you
either lose the one that got you there, or you don't try it. Every agent tty7
resumes already knows how to branch — `codex fork <id>`, `claude --resume <id>
--fork-session`, `opencode --session <id> --fork`, `grok --resume <id>
--fork-session` — but nothing in tty7 reached them, so the capability was
invisible from the terminal that already knows every pane's session id.
Fork is a per-agent capability beside the existing resume table
(`CLIAgent::fork_command`), not a Codex special case: it is the same `match
self` shape, it reuses the same id validation and the same launch-flag replay,
and four installed agents qualify today. Every command was checked against that
CLI's own `--help`; agents with no fork tty7 could verify return `None` and are
never offered the action, since a guessed flag shape would only ever produce a
usage error in the pane.
Flag replay needed one correctness fix to survive this. A forked pane's own
argv *is* a fork command, so relaunching it would replay the stale subcommand
and id (`codex fork <old>` → an old id as a positional prompt) or double the
modifier (`--fork-session --fork-session`). `codex fork` now sheds its
subcommand exactly as `codex resume` did, and `--fork-session` / `--fork` join
their agents' stale session-targeting lists. That also settles restore: a forked
pane restores through `resume_command`, which now drops the fork flag — a
restart continues the fork rather than branching it again.
Placement follows where the user asked from. A pane-level ask is spatial, so
the pane right-click menu offers Split Right / Left / Down / Up (pane splits
gained a `before` slot for the Left/Up half, which the tree had no way to
express). A tab-level ask isn't, so the tab context menu — inherited verbatim by
the sidebar rows, which is where the request came from — opens the fork in a new
tab with no placement question. The bare action behind the palette, the File
menu and Settings → Keybindings takes the tab-level meaning.
The three ways a fork can't run all surface rather than no-op: no session id
yet (hooks not installed) and a remote pane (the command would shell the *local*
agent) render the row disabled instead of hiding it, so the capability stays
discoverable, and the action paths that have no row to grey out say so in a
notification. Forking mid-turn is allowed but announced — agents fork from the
persisted transcript, so the turn in flight is absent from the copy — and the
parent is untouched either way.
Copy Session ID sits beside Copy Working Directory. Codex has no
copy-or-duplicate subcommand, so "copy the session" is the id: paste it into
`codex resume`, a bug report, or another tool.
Deliberately not built: any reading or writing of an agent's own session files.
tty7's exposure stays the public CLI contract plus the hook payload's session
id, so a change to Codex's rollout format or its version-numbered SQLite index
costs at most a visible shell error. Forked tabs also look exactly like their
parent, by decision — "Rename Tab" is the answer.
Closes#211
* no-mistakes(review): perf(terminal): compute fork menu enablement at menu-open time
* no-mistakes(document): docs: correct fork action surfaces, label, and remote limits
* fix(agents): label forking the same for every agent
The fork row said "Branch Session" on Claude Code and "Fork Session"
everywhere else, on the strength of a source comment claiming "Claude Code
calls it branching". It does not. `claude --help` documents the flag as
`--fork-session`, described as "When resuming, create a new session ID instead
of reusing the original"; the only occurrences of "branch" in its help are an
unrelated git-branch review option. The claim came from otty's own UI wording,
which I mistook for Claude's vocabulary and then wrote into the source as
fact — so the comment goes with the special case rather than being left behind
as a false statement about someone else's tool.
The split was also inconsistent with itself: Grok takes the identical
`--fork-session` flag and was already labelled "Fork Session". Every agent that
has the capability calls it forking — `codex fork`, `--fork-session` on Claude
Code and Grok, `--fork` on OpenCode — so one wording covers all four.
`fork_label` keeps returning `Option<&'static str>`: it is still the UI's single
capability gate (`None` = no verified fork command, no row offered), and
per-agent wording stays expressible should one ever genuinely diverge.
Generated commands are untouched — the existing table test still pins
`claude --resume <id> --fork-session` and the other three verbatim.
Also drops the two doc sentences that promised the per-agent label, and the
stale "Branch Session" mentions left in comments; no occurrence survives
anywhere in the tree.
* no-mistakes(review): fix(agents): fork the pane the tab menu row named
* no-mistakes(document): rewrap fork menu comment after label unification
* fix(agents): repoint Pi's token-gate comment after the rebase
Rebasing #211 onto #240 moved the session-id token gate out of
resume_command and into the shared session_command_flags helper, so
Pi's comment pointing at "the token gate above" no longer names
anything. Comment only; the gate itself is unchanged.
* no-mistakes(document): correct fork placement rationale in menus and changelog
* chore: untrack AGENTS.md per gitignore dev-tool convention
tty7 keeps agent-memory files out of the repo: `/CLAUDE.md` is already
ignored, and on disk it is a symlink to `AGENTS.md`, so tracking the
target defeated the convention. Ignore `/AGENTS.md` alongside it and drop
the tracked copy; the file stays on disk, where the notes belong.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* 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>
resvg 0.47 landed in tty7 (#227) and the gpui fork (#237), but
gpui-component still declared its own resvg = 0.45.1, keeping a
legacy resvg/usvg/tiny-skia 0.45/0.11 stack in the tree. The fork now
pins 0.47 (l0ng-ai/gpui-component@2264ff99 — no source changes needed;
its only resvg user, the Windows native-menu rasterizer, uses APIs
unchanged across the bump), so this moves the pin and drops the last
duplicate: the lockfile now carries a single resvg/usvg/tiny-skia
stack at 0.47/0.12, and `cargo tree -i resvg@0.45.1` matches nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #227 bumped tty7's direct resvg to 0.47 while the gpui fork still
pinned 0.45, so the tree compiled two resvg/usvg/tiny-skia stacks. The
fork's tty7 branch now carries resvg 0.47 (l0ng-ai/zed@3aac3ef); move
the gpui pin there so gpui's SVG renderer and tty7's tray-icon
rasterizer share one 0.47 stack again.
gpui-component still declares its own resvg 0.45.1 (semver-incompatible
with 0.47), so one legacy 0.45 stack remains until that fork catches up
- noted in the manifest comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every character came out as a different character, one for one, consistently
— it read as a broken locale or a mangled encoding, and it was neither.
Hack, the bundled default, has no CJK, so those cells are shaped through the
font-fallback chain. gpui's Windows backend then threw away the face
DirectWrite shaped the run with and looked a fresh one up by family, weight and
style. That round trip mapped DirectWrite's italic to oblique — the enum is
numbered OBLIQUE = 1, ITALIC = 2, and the mapping had them the other way around
— so an italic fallback face resolved to a request for an oblique one, and a
family with no oblique face (Maple Mono NF CN, first in our Windows chain) came
back as its upright face instead. The glyph indices were right; the outlines
they indexed belonged to a different face, at a fixed glyph-id skew.
Fixed upstream in our gpui fork by registering the face DirectWrite actually
chose rather than re-deriving one, which also closes a latent use-after-free in
the same cache: it keyed fonts by a raw pointer to a face nothing held a
reference to, so a released face could be aliased by any later allocation.
Bumps the fork pin; no tty7 code changes. Covered there by two tests in
`gpui_windows::direct_write` — one asserting a shaped run's glyphs round-trip
through the font id the run reports, one asserting every font-face cache key is
owned by the font it maps to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#214 added the switch itself; this is the wiring around it that a new
setting in this codebase is expected to carry.
- Index the row in `settings_search_entries`, which the settings search
box matches against. Without an entry, searching "dim", "fade" or
"unfocused" — the words someone actually looks for — finds nothing,
and the switch is only reachable by scrolling to it. Pinned in
`index_titles_match_rendered_row_labels` so the title cannot drift.
- Pin the default and the round trip, as every other `default_true`
flag here does (see `confirm_window_close_defaults_on_and_round_trips`):
a config written before the switch existed must still dim, and a
`false` must survive save/load or the effect comes back next launch.
- Hand the flag to `Pane::render` instead of reading the `Config` global
from inside it. `pane.rs` had no global state before, deliberately —
the leaf type is generic so the tree logic can be tested with plain
values. The caller already computes the split test the dimming was
gated on, so it can compute this too: one lookup per frame rather than
one per leaf, and the tree stays renderable without a Config global.
While there, `show_focus` is now named for what it does — nothing
drew a focus ring; it only ever gated the fade.
- Move the row below "Follow theme". That button clears the opacity and
blur overrides only, and a third row directly above it read as
something it would also reset.
- Changelog entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`TERM` names terminfo capabilities; it cannot answer "which program is
this". The de-facto standard pair that does — `TERM_PROGRAM` and
`TERM_PROGRAM_VERSION`, introduced by Apple Terminal and set by iTerm2,
WezTerm, Ghostty, VS Code and tmux — went unset, so anything asking was
told nothing.
Plenty asks. Capability probes (`supports-color`, `supports-hyperlinks`,
and the CLI ecosystem built on them) read the program name to decide on
truecolor and OSC 8; editors branch on it for terminal-specific
workarounds; shell prompts adapt their glyphs to it. Absent, they all
fall back to their most conservative behaviour. The `TTY7` marker we do
export is no substitute: it exists so globally-installed agent hooks
stay silent in other terminals, and nothing third-party looks for it.
Both new variables stay overridable through `env` in `config.json`,
unlike `TERM` and `COLORTERM`. Those two state what the pane's decoder
implements, which isn't the user's to contradict; the program name is an
identity, and posing as another terminal is a legitimate way to get a
tool that only recognises a fixed list to light up.
Building the pane's environment is now one function returning the pairs
in application order, so that precedence is testable without a
`CommandBuilder` or a real `config.json`.
Local panes only. ssh forwards environment variables solely by agreement
between client and server (`SendEnv`/`AcceptEnv`, `LANG` and `LC_*` by
default), so a native-SSH pane still sees whatever the remote host sets
for itself — as is already true of `COLORTERM` and `TTY7`.
Closes#212
Every config-dir file is read by a loader that treats any parse error as
"there is no file" and falls back to defaults. serde_json rejects the
U+FEFF a BOM puts before the opening brace, so a BOM never surfaced as a
broken config — it surfaced as an absent one, and the app booted on
defaults with nothing to explain it.
Windows makes that easy to hit by accident: PowerShell's `>`, `Out-File`
and `Set-Content -Encoding utf8` all write a BOM, so editing config.json
from a shell was enough to lose every setting.
Strip a leading BOM in the three loaders whose files people hand-edit:
config.json, session.json (which dropped every workspace the same way),
and themes/*.yaml. read_to_string decodes the marker to one U+FEFF char,
so this strips the char, not the three raw bytes — and only the first
one, since a second is content the parser should still reject.
window.json and update.json are left alone: they are machine-written
state a relaunch rebuilds, never hand-edited.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
A pane learned its working directory from OSC 7 alone, which only shells
tty7 manages to inject its integration into ever emit. A shell that execs
into another one from its rc file (`exec fish` at the end of .zshrc), a
nested shell started by hand, or any shell with no integration at all
emits none — and since a pane's cwd is seeded with its spawn directory,
such a pane does not report *no* directory, it reports a permanently
stale one. New tabs and splits, the git probe and path completion all
follow it to the wrong place, with nothing on screen to say why.
Read the cwd from the process table too, on the same half-second
foreground poll that already detects SSH sessions and coding agents, and
reconcile it with what the pane reports:
- A remote pane is left alone. The local process table can only see the
ssh client's own directory, which is the confusion apply_remote_context
clears the cwd to avoid in the first place.
- No reading is "nothing to read", never "no cwd", so it cannot clear one.
- When both name the same directory the shell's spelling wins. $PWD keeps
the symlinked route the user walked in through, and that is the path a
new tab should open in; only a genuine disagreement moves the pane.
The three platform cwd readers move from DaemonPane methods to free
functions so the reader thread can call them; the method stays as a thin
delegate for List.
Fixes#187
macOS fills the window's leading corner with the traffic lights and
`TITLE_BAR_LEAD` reserves them 80px. Everywhere else that corner held
nothing: the caption row's only contents are the rail's "+" and collapse
at the rail's right edge and the corner chrome at the window's, so the
left third of the row read as unfinished rather than restrained — while
Windows treats the top-left as the app's identity slot.
Three parts, all of them about that row:
- `window_mark()` draws the "duo" mark (the app icon's own art) at the
head of the rail on `CONTENT_INSET`, the line the search box and every
row label below it start on, and follows the rail's controls into the
title strip when the sidebar collapses. It is drawn, never clicked: no
hover capsule, and deliberately no `occlude()`, so the drag region
underneath still takes the press and the strip stays grabbable.
- The rail's stand-in row now reserves the same hairline the real
`TitleBar` draws inside its own height. Without it the bar centred
content on 19.5 and the rail on 20, and the mark hopped half a pixel
as collapsing the rail handed it from one to the other.
- With the detail panel open off macOS the bar is hoisted above
`[terminal | panel]` so the window controls can reach the corner, which
left the code and diff overlays — anchored to the terminal column —
starting 40px down, with headers drawn to *be* the title bar landing a
row low. They now hang on the row that owns the bar, inset by the
panel's width. Covering the caption row that way needs the headers to
carry its gestures, which neither ever did with the panel open or
closed: `title_bar_drag()` gives both (and the rail's row, which grew
the same wiring by hand) drag-to-move and double-click-to-zoom, and
their controls are `occlude()`d so HTCAPTION stops eating the clicks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
The default `font_fallbacks` list was macOS-only -- Menlo, Hasklug Nerd
Font Mono, Maple Mono NF CN, Apple Color Emoji. Fallbacks resolve by
family name against installed fonts, so off macOS the whole chain matched
nothing and every glyph the primary lacked was left to the platform's own
cascade. Bundled Hack maps 1548 codepoints and zero ideographs, so on
Windows that was every Chinese character in every pane, and every emoji.
The fall-through is not only a matter of which face you get. `element.rs`
pins each wide cell to `2 x cell_width`, and Hack advances 0.60205em, so
a two-column slot is 1.2041em -- while every stock CJK face (Microsoft
YaHei, PingFang SC, Noto Sans CJK) advances 1.0em. `force_width`
left-aligns, so the ideograph hugs the left of its slot and the remaining
0.2em shows up as a gap on the right of every character. Measured on
Windows at font_size 15: left bearing 1.49px, right bearing 4.90px.
Branch the defaults per platform, keeping Maple Mono NF CN first
everywhere -- 0.6em Latin, 1.2em CJK, the one exact two-cell fit against
Hack (bearings 3.06px / 3.62px, ink centered). It stays referenced by
name only, never bundled, at ~20MB per weight.
Changing `Config::default` alone would reach nobody who already has a
`config.json`, which is every existing user. So `fallback_chain` appends
the platform's stock faces the same way it already pins Hack: a fallback
is consulted only after everything ahead of it has missed, so appending
can never displace a face the user chose, and the file is never rewritten.
Verified by driving two builds against one config naming only absent
macOS faces: before, the CJK line differed from an explicit Microsoft
YaHei chain by 3571 pixels (the cascade picked something else); after, it
is pixel-identical.
The GUI-launch locale fallback exported a literal `LC_CTYPE=UTF-8`. That
name is a BSD libc alias with no glibc equivalent, and the stock
`ssh_config` ships `SendEnv LANG LC_*`, so it rode along to every host we
ssh into. There `LC_CTYPE` outranks the `LANG` the host sets for itself
and then fails to load, dropping the remote shell to the C locale --
re-creating the mangled non-ASCII output the fallback exists to prevent.
Derive the locale from the system locale instead, the way Terminal.app
and iTerm2 do: reduce the CFLocale identifier to its POSIX `lang_REGION`
stem, and fall back to `C.UTF-8` then `en_US.UTF-8`. Every candidate is
checked against `/usr/share/locale` before it is exported, so tty7 never
hands a shell a name the C library cannot load -- including on machines
whose region combination has no installed locale (`en_CN` is an ordinary
macOS setting that resolves to no locale at all).
Still `LC_CTYPE` only, and still only when no locale is inherited or
configured, so message/date/number localization and explicit user
overrides are untouched.
Fixes#178
CHANGELOG: keep both Unreleased sets, with the history-search entry under
Added beside the multi-window ones and the Ctrl+J/M fix in its own Fixed
section.
Review follow-ups on top of the multi-window work.
- A brand-new workspace came up on the home page with no shell, because
`claim` always hands back an (empty) session and the window treated that
as "restore this". A first run and `New Workspace` now take the
first-run path again and spawn a terminal; the launch that exists to
show the workspace picker asks for an empty window explicitly
(`FreshStart`).
- The close-window prompt promised sessions "will be restored the next
time you open tty7", which is no longer what happens — the workspace
detaches and waits in the picker. Both it and the one-time detach hint
now point at the title bar's workspace menu rather than the macOS
Window menu, which does not exist on Windows or Linux.
- `ToggleSftp` read the panel state off the config, which is now only
what a *new* window starts with; it reads this window's own state.
- `SelectWorkspace1..9` were unbindable: registered as actions but absent
from the keymap tables. Added with no default chord (⌘1–9 is the tab
row's).
- `theme_commands`' doc comment had been captured by a function inserted
above it, and the Window menu's slot→action mapping was a second copy
of the title-bar chip's.
- CHANGELOG: drop the ⌘1–9 claim (no such binding ships), and document
the chrome tile sizing that rode along with this branch.
The local command editor consumed every Ctrl chord at the prompt, matched
or not, so two things the shell owns quietly stopped working (#163).
^J and ^M carry accept-line's control codes — Enter by another name — but
fell into `apply_readline_ctrl`'s no-op arm, so the keys did nothing at
all. Route them through the same path Enter takes, via a shared
`accept_line`, so the completion picker and the history menu treat them
identically.
^R was recognized, but only ever opened tty7's own history menu, with no
way back to a `bindkey`ed widget (fzf, percol). Add `history_search`
(default on, Settings → Terminal → Keyboard): with it off, the edited
line is handed to the shell and the raw ^R follows it, so whatever is
bound there answers. The "shell integration never engaged" notice stays
quiet in that case — ^R reaching the PTY is then the point, not a gap.
`handoff_tab_to_shell` generalizes to `handoff_line_to_shell(chord)` to
carry the ^R handoff; the Tab path is a thin wrapper over it and its
behavior is byte-for-byte unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflict in the sidebar's control row: main wrapped both tiles in
`occlude()` so Windows' HTCAPTION drag doesn't swallow their clicks, while
this branch moved their geometry onto `chrome_tile_sized` / the shared
`TILE_*` constants. Kept both — occluded wrappers around helper-sized tiles.
tty7 had exactly one window, so `main` opened it inline and every app-wide
duty — tray, menus, the quit hook — lived in `Tty7App`'s constructor. This
splits those apart: a *workspace* is the persistent identity (tabs, splits,
cwds, name) and a *window* is a transient view onto exactly one of them.
- `ui::windows` — the app-level window registry and the single place that
opens a window. Exactly one window per workspace is enforced there: the
daemon gives each pane a single subscriber, so a second window on the same
panes would silently steal the first's output. `open` focuses the existing
window instead. New windows cascade so one never lands on top of another.
- `WorkspaceStore` owns session.json, so windows never race each other as
writers. Closing a window *detaches* — panes keep running in the daemon
and the entry stays for the picker; `StopWorkspace` kills the sessions and
keeps the layout; `DeleteWorkspace` also forgets it.
- Window menu lists every workspace with a monogram badge and a liveness
dot, ⌘1–9 for the first nine. Same list in the palette; closed ones also
appear in a home-page picker with a coarse relative age.
- Sidebar collapse and right-panel visibility move onto `Tty7App`, so
toggling one window's chrome leaves the others alone; the config value
becomes what a new window starts with. Panel *width* stays shared — a
width is a preference, not a view state.
- Tray, menus, and the quit hook now walk the registry rather than
belonging to a single window.
Protocol goes to v2: `RemoteKind::Wsl` is a new enum variant, which is not
the additive change it looks like — the enums carry no `#[serde(other)]`, so
a v1 peer fails the whole decode and drops the pane's connection. The
handshake now catches that skew and offers a restart.
The replay ring stored raw PTY bytes with no geometry history and attach
replayed all of it at the final recorded size. Any resize during a session
(pane split, window drag) meant older bytes re-wrapped at the wrong width
on replay, so a TUI's cursor-up redraws (Claude Code's inline renderer is
the canonical case) landed mid-frame and every redraw leaked stale rows
into the reattached pane's scrollback -- duplication that never existed
live (10 markers live vs 45 replayed in the regression scenario).
The ring is now a sequence of geometry-tagged segments: resize seals the
current segment (retagging an empty tail in place), cap eviction drops
emptied segments, and attach replays a Size -> Snapshot pair per segment.
The client reader already applies each Size to its grid right before the
paired Snapshot advances (pending_size), so it reflows between segments
exactly where the live client did -- no client changes needed.
* feat(view): explain a dead Ctrl+R instead of failing silently (#46)
When shell integration never engages in a pane — typically because a
figterm-style PTY shim (kiro-cli-term, qterm) exec'd over the shell and
swallowed its OSC 133 reports — the whole command-editor overlay is
absent by design, and Ctrl+R used to fall through to the raw PTY with no
hint of why the history menu didn't appear.
Now that raw-path Ctrl+R raises a one-shot, per-pane notice (floating
bottom-right) saying integration hasn't engaged, refined off-thread with
the daemon's foreground-process name when it matches a known shim: the
wrapper is the culprit worth naming, since "install integration" advice
would mislead — the hooks are installed, something between the shell and
tty7 is eating their output. The chord still reaches the PTY, so the
shell's own reverse-i-search keeps working as the fallback.
Guards keep it honest: silent inside an 8s startup grace window (slow rc
files legitimately haven't reported yet), on the alt screen, or once
integration has engaged (a running foreground command is then the
obvious reason); retracted if a slow shell engages late; dismissed by
the next keystroke or a 15s timeout.
* docs(readme): restructure around Why/Features, cut prose and boilerplate
Replace the prose About section with a four-point Why tty7 list, split
Features into prompt vs window groups written as one-line benefit
bullets, drop emoji section headers, fold acknowledgements/contributing/
license into a one-line footer, and keep zh-CN in sync throughout.
* feat(history): Ctrl+R fuzzy search menu with run metadata
Ctrl+R grows from the single-line reverse-i-search into a browsable
menu of ranked candidates floating beside the prompt:
- Matching is fuzzy (src/terminal/fuzzy.rs, a dependency-free
affine-gap aligner: word-boundary and consecutive-run bonuses, gap
penalties; space-separated query terms must all match), blended with
the existing frecency scores so a command you run constantly — or
ran in this directory — outranks an equally-good textual match.
- An empty query lists the whole history by frecency, so bare Ctrl+R
is a "recent & relevant" browser. Matched characters highlight in
the rows; Ctrl+R/Down and Ctrl+S/Up move the selection, Enter loads
the line into the editor, Cmd+Enter runs it outright. The classic
(reverse-i-search) prompt line stays.
- History records now carry run metadata: new lines are
<ts>\t<exit>\t<cwd>\t<command>, written when the command finishes
(zsh INC_APPEND_HISTORY_TIME-style) so the exit code sniffed from
OSC 133;D lands in the record; older formats still parse, and
zsh/bash HISTFILE timestamps carry over when seeding. The menu shows
"ran 3h ago" and a red x-badge on commands whose last run failed.
- RemoteTerminal exposes prompt_seq/last_exit_code so the view can
tell a fresh post-command prompt report from the stale pre-submit
state even when 1 Hz polling misses a fast command's running window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Selecting with the mouse — drag, double-click word, triple-click line,
over terminal output or the prompt's command editor — copies the
selection the moment the gesture ends, no Cmd+C needed. Opt-in via
Settings -> Terminal -> Clipboard (config key copy_on_select), off by
default so a stray selection never overwrites the clipboard.
Closes#34
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(hints): dismiss held-modifier tab badges on window activation flip
The badges were dismissed only by ModifiersChanged (release) or a real
keypress. Deactivating mid-hold — cmd-tabbing away, Spotlight, a click
into another app — delivers the modifier release to whatever app is key
by then, so this window never saw it and the badges stuck on until some
later keypress; mouse-only use left them up forever.
Dismiss on every window-activation flip via observe_window_activation.
Flipping on *both* directions also cancels a reveal scheduled just
before the switch, so the timer can't pop badges up in a window the
user already left.
Tested with a headless gpui harness: Tty7App now builds through a
with_session seam (a zero-tab session restores the home page, spawning
no terminal/daemon), the test activates the window for real, simulates
the bare-secondary hold, then deactivates and asserts the badges (and a
pending reveal) are gone. Session's save/load test takes a shared file
lock so parallel tests can't clobber the pinned session.json.
* style(hints): satisfy rustfmt in gpui test harness
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>