Commit Graph
682 Commits
Author SHA1 Message Date
l0ng-ai c2dcfe445f Fix the merged i18n guard: one ALL sweep, not two half-tests 2026-08-08 03:51:59 +08:00
l0ng-ai 2a9b4764bd Merge branch 'fix/first-terminal-failure' into integration/polish 2026-08-08 03:48:43 +08:00
l0ng-ai 70c82376bd Merge branch 'fix/ssh-advanced-grouping' into integration/polish
# Conflicts:
#	src/ui/i18n/mod.rs
2026-08-08 03:48:43 +08:00
l0ng-ai 6ad7a25e0b Merge branch 'fix/sftp-overwrite' into integration/polish
# Conflicts:
#	src/ui/i18n/mod.rs
2026-08-08 03:48:43 +08:00
l0ng-ai 0d698f21a3 Merge branch 'fix/keybind-recording-feedback' into integration/polish 2026-08-08 03:48:42 +08:00
l0ng-ai 966f3bebb9 Merge branch 'fix/switcher-column-hint' into integration/polish 2026-08-08 03:48:42 +08:00
l0ng-ai 43450c075e Merge branch 'fix/stacked-daemon-prompts' into integration/polish 2026-08-08 03:48:42 +08:00
l0ng-ai aa97ec94a2 Merge branch 'fix/i18n-enum-guard' into integration/polish
# Conflicts:
#	src/ui/i18n/mod.rs
2026-08-08 03:48:42 +08:00
l0ng-ai 78ca226afa Merge branch 'fix/settings-dirty-escape' into integration/polish 2026-08-08 03:48:42 +08:00
l0ng-ai 60d994c337 Merge branch 'fix/ssh-auth-error' into integration/polish 2026-08-08 03:48:42 +08:00
l0ng-ai 5a6f4ae611 Merge branch 'fix/keybindings-page' into integration/polish
# Conflicts:
#	src/ui/i18n/en.rs
#	src/ui/i18n/ja.rs
#	src/ui/i18n/mod.rs
#	src/ui/i18n/zh.rs
2026-08-08 03:48:42 +08:00
l0ng-ai 7e441921a4 Merge branch 'fix/destructive-confirms' into integration/polish 2026-08-08 03:48:41 +08:00
l0ng-ai f605a21e76 Merge branch 'fix/copy-consistency' into integration/polish 2026-08-08 03:48:41 +08:00
l0ng-ai 4e5d711e5d Merge branch 'fix/agent-status-affordance' into integration/polish
# Conflicts:
#	src/ui/presets.rs
2026-08-08 03:48:41 +08:00
l0ng-ai 82947a2510 Merge branch 'fix/tree-states' into integration/polish 2026-08-08 03:48:41 +08:00
l0ng-ai 5e171d8f6b Merge branch 'fix/palette-list' into integration/polish 2026-08-08 03:48:41 +08:00
l0ng-ai 1ad76c153a Merge branch 'fix/icon-hit-targets' into integration/polish 2026-08-08 03:48:40 +08:00
l0ng-ai be228d22cf Merge branch 'fix/settings-search' into integration/polish 2026-08-08 03:48:40 +08:00
l0ng-ai 3caf45108d Merge branch 'fix/cursor-opacity' into integration/polish 2026-08-08 03:48:40 +08:00
l0ng-ai 08ad86756a Merge branch 'fix/terminal-context-menu' into integration/polish 2026-08-08 03:48:40 +08:00
l0ng-ai f12a837fd3 fix(app): say why the window opened with no terminal in it
A first terminal that fails to spawn logged the reason and showed the
home screen — the same screen you get after closing everything. On a
first launch with a bad shell configured, tty7 opened, said nothing, and
every ⌘T after it failed the same way.

The home screen now carries the reason, and a failed New Tab leaves it
there rather than only in a toast that goes away.

The reason itself was worth fixing too. A missing shell arrived wrapped
four deep:

  Could not open a terminal: daemon refused Spawn: spawn failed: Unable
  to spawn /usr/local/bin/x because it doesn't exist on the filesystem or
  is not executable (ENOENT: No such file or directory)

The daemon now checks a shell given as a path before anything tries to
run it, and the two internal prefixes are gone:

  Could not open a terminal: no such shell on this machine:
  /usr/local/bin/x

A bare program name is left alone — the OS resolves it through PATH, and
guessing there would be worse than saying nothing.
2026-08-08 03:47:20 +08:00
l0ng-ai 96f6dc88c5 fix(settings): break the SSH Advanced list into groups you can scan
Nineteen rows in one undivided run, from identity files to warn-on-close,
in the order the form happens to build them.

They now carry six quiet headings — Authentication, Proxies, Algorithms,
Connection, Session, Security — of two to four rows each. No row moved:
the existing order already grouped this way, it just never said so.
2026-08-08 03:34:29 +08:00
l0ng-ai d24f16a1b3 fix(sftp): stop transfers from writing over files without a word
Downloading the same file twice overwrote the first copy, and uploading
into the folder on screen overwrote whatever was already there. Neither
said anything, and neither is recoverable.

| | Before | After |
|---|---|---|
| Download to a name already in ~/Downloads | overwrites it | saved as `name (2).ext`, the way a browser does — still one click, and the first copy survives |
| Upload a name already in the remote folder | overwrites it | asks once, naming what it would replace (up to three, then `…`) |

The upload check reads the listing already on screen rather than making
a round trip to the far side to ask.
2026-08-08 03:28:11 +08:00
l0ng-ai f372971ccc fix(settings): show the pause a keybinding is waiting on
Recording a shortcut commits after a 650ms pause, and the hint asked
people to time something they could not see: "pause to save · Esc", with
no countdown behind it.

A two-pixel bar under the hint now runs the same clock the commit does,
and restarts with every extra chord — so a key sequence reads as one
gesture with a visible deadline instead of a guess.
2026-08-08 03:19:57 +08:00
l0ng-ai 0743ac6118 fix(switcher): say that Tab is the way across once a query is typed
With text in the search box, ← and → belong to the caret and Tab becomes
the way between the two columns. That remap is deliberate — the code says
so — and it was also silent: the arrows simply stopped crossing, with
nothing on screen to explain it.

The footer hint, which already changes while the switcher is held open,
now says "Tab to cross columns" whenever a query is present.
2026-08-08 03:10:50 +08:00
l0ng-ai 507cbbd88f feat(tabs): ask before a close ends work that is still running
Closing a tab is the highest-frequency destructive key in any terminal,
and tty7's headline claim is that shells outlive the app — yet ⌘W was
the one action that permanently ended one, silently. A mid-turn coding
agent, a forty-minute build and an idle prompt all died identically, and
Reopen Closed Tab restores the layout with a *fresh* shell, so the undo
was never an undo.

The SSH close guard already had the shape for this. It is now a general
close guard: a pane reports whether it is busy, and closing asks once,
naming what it would end.

| | Before | After |
|---|---|---|
| ⌘W at an idle prompt | closes | closes, unchanged |
| ⌘W with `cargo build` running | kills it silently | "Close this tab? — cargo build is still running. Closing ends it." Keep is the default button |
| ⌘W with an agent mid-turn | kills it silently | names the agent and its turn |
| Close Other Tabs / Tabs to the Right | skipped live-SSH tabs | skips anything that would raise a question, rather than stacking one dialog per tab |

A pane counts as busy only when the shell is actually reporting: without
shell integration `at_prompt` is permanently false, so a terminal that
trusted it would ask this question on every single close. Agents report
independently and are trusted either way.
2026-08-08 02:57:31 +08:00
l0ng-ai 6fa353f8ca fix(ui): one confirmation idiom, and copy that names what it is looking at
- The SSH close confirmation was the app's only bespoke in-app dialog:
  no scrim, no Escape, no click-outside, no focus — the two buttons were
  the only way out, and an on_key_down there could never have fired
  because nothing inside the card is focusable. It now asks through the
  same platform prompt as the other eleven confirmations, which gets
  Escape and dismissal from the OS.
- The switcher's tab column reported "No workspace or machine matches."
  That is the other column's noun.
- ja left "bind" and "to" in English inside a form whose other labels are
  Japanese; zh had already translated both.
- The bind and target host fields in an SSH forward rule were pinned at
  104px, which does not hold an EC2 internal hostname. Same floor, but
  they now take a share of the row's slack instead of handing all of it
  to the free-text description beside them.
- docs/features.md still described ⌃⇥ as "next tab"; since the two-column
  switcher landed it holds a panel open and commits on release. Also
  drops two more names for actions the app calls Zoom Pane and Clear
  Scrollback.
2026-08-08 02:57:18 +08:00
l0ng-ai 89d838dd15 fix(remote): ask about one mismatched server at a time
Reconnecting to several machines whose servers are all behind raised one
native modal per machine in a single pass, stacked on each other, each
about a machine the one in front of it did not name.

The queue is now walked one question at a time: the next is asked only
after the previous is answered. If the window goes away with a question
open, the rest go back on the queue instead of being swallowed.
2026-08-08 02:49:41 +08:00
l0ng-ai 10e2bae2fe test(i18n): check every key that exists, not a copy of the list
The parity guard walked a hand-maintained list of 900 keys. A key added
without being added there was checked by nobody, and the list had drifted
— that is exactly how eleven values ended up shipping as their English
originals in zh and ja.

The enum is now emitted by a macro that also emits L10nKey::ALL, so there
is one list and the compiler reads it. The guard sweeps ALL and, on top
of the non-empty checks it already made, fails on a translation that is
byte-identical to the English. What that must not catch is spelled out
in KEPT_IN_ENGLISH with a reason each: protocol names, pure templates,
product names, a language named in its own language, and the
shell / agent / ssh set that would read worse half-translated.

Also translates what the new check found and should not have kept:
ja "bind" / "to" in a form whose other labels are Japanese (zh had
already done both), and Stable / Nightly in zh and ja.

Net −850 lines.
2026-08-08 02:46:41 +08:00
l0ng-ai 78e5dabb14 fix(settings): stop Escape from throwing away an unsaved connection
Editing an SSH profile and pressing Escape closed Settings and discarded
the form. `dirty` was already computed — it is what enables Save — but
only to grey out a button, never to ask.

Escape, the close button and ⌘, now ask when the form holds unsaved
edits, with Keep Editing as the default. Every other caller of
close_settings runs as the tail of something the user explicitly chose
(Connect, Save & Connect, quick connect) and closes as before.
2026-08-08 02:41:44 +08:00
l0ng-ai 4ab1d80ecc fix(ssh): stop answering "authentication failed" when nothing was tried
The auth loop seeded its reason with "authentication failed" and only
replaced it when some method actually reported one. So a round where
every method was skipped — no key on disk, no agent, or a connection
pinned to a method this server does not offer — came out as a failure,
which sends people looking for a wrong password that was never sent.

That case now says what it is and names what the server would accept:
"no authentication method could be tried; the server offers publickey".

The disconnected strip also gained the two things it was missing. It
reported only that the connection had ended, so a rejected key and a
dropped network read identically and the reason scrolled away with the
pane's own output; it now shows the reason in the danger ink. And for a
pane spawned from a saved connection it offers "Edit connection…"
beside Reconnect — until now the only button was Try Again, on the one
error class where trying again unchanged never helps.

Tested: unit cover for the no-attempt message (names the offered
methods, never says "failed", and does not trail an empty list when the
server offered nothing). The strip itself was not seen on screen — this
machine has no sshd to fail against, and I did not want to authenticate
against someone else's.
2026-08-08 02:35:55 +08:00
l0ng-ai c8f13faee0 fix(settings): let the Keybindings page speak the app's own words
The page the docs call *the* shortcut reference rendered 89 undivided
rows of humanize_action's CamelCase split — English in a three-locale
app, and a fourth vocabulary on top of the menu bar, the palette and the
docs: "Toggle Maximize Pane" for what everything else calls Zoom Pane,
"Close Active Tab" for Close Pane / Tab, "Toggle Palette" for Command
Palette.

Adds keymap::action_entry, which routes every action's name and section
through the strings the rest of the app already uses. The list is now
read in the same seven sections the command palette groups by, and the
displaced-binding notice uses the same names as the rows it is talking
about.

Seven new keys across en/zh/ja; the nine tab and nine workspace jumps
are templated rather than eighteen hand-written strings per locale.

every_action_has_an_authored_name fails if a new action would fall back
to the CamelCase split, so the page cannot drift back one action at a
time.
2026-08-08 02:26:17 +08:00
l0ng-ai 9ae1a8dbd9 fix(ui): ask before the deletes that have no way back
The local file tree asks before it deletes a file. SFTP delete is the
same red Delete in the same shape of menu, on a machine you cannot walk
over to, and it went straight through — the asymmetry was the tell.

- SFTP delete now confirms, naming the file and the host, and says
  plainly that there is no trash on the far side.
- Deleting an SSH profile confirms. It also stops orphaning the profile's
  keychain entry: "Forget password" lives on the menu that is about to
  stop existing, so the secret had no UI left to remove it. The secret is
  only let go when no other profile still points at the same endpoint,
  and the copy says so.
- "Restore all defaults" wipes every rebound key with no undo, and now
  asks. It is a no-op when nothing has been rebound, so it does not ask
  a question with no stakes.

Deliberately left alone: Clear Scrollback (every terminal clears without
asking) and killing a port forward (cheap to recreate).
2026-08-08 02:11:16 +08:00
l0ng-ai 3301f74a8d fix(tabs): stop agent state from being carried by hue alone
Working, Needs input and Done were a hue on a nine-pixel dot with no
shape, label or tooltip anywhere. The pair that decides whether you go
and look — Needs input vs Done — is amber vs green, which is the pair
red-green colour vision separates worst.

- Needs input now draws as a ring rather than a filled dot, so it
  differs in shape and not only in colour.
- The tab avatar carries a tooltip naming the agent and its state, in
  all three locales. It had no tooltip at all before, on any of its
  three call sites.
- Codex and Grok both ship a pure black disc, which is the window fill
  on a dark theme: the disc dissolved and left a white glyph floating.
  A disc whose brand colour lands within 1.25:1 of the window now gets a
  hairline, which also covers user themes rather than one palette.

Tested: unit cover for the status labels (every state that paints a dot
has words, and Needs input never reads as Done) and for the disc edge
rule. The avatar itself was smoke-checked in a dev instance on a shell
tab; the agent branch was not exercised on screen — no agent was run.
2026-08-08 01:33:54 +08:00
l0ng-ai a9db3a8cbb fix(file-tree): say why a directory has no children instead of drawing nothing
read_dir was unwrap_or_default()ed, so a folder the OS refused produced
an empty listing and rendered byte-identically to one that really is
empty — and to one still being listed. The file tree had no error field
at all, and its InFlight set was never read by render.

An expanded directory with no visible children now draws a placeholder
row saying which of the four it is: Reading…, Empty, Only hidden files
(entries landed but the hidden filter took them all — "empty" there is a
lie one keystroke disproves), or Could not be read, in the danger ink.
Placeholders are not files: arrow-key navigation skips them.

Also gives the tree column itself a state — a search that matches
nothing, and a tab with no directory behind it, both used to render as
the same empty column — and the tab sidebar a note when a filter drops
every group.
2026-08-08 01:25:36 +08:00
l0ng-ai c1b5cc45ab fix(palette): stop showing recent commands twice, and keep frecency once you type
The zero-query list pushed a Recent section and then pushed every group
that contained those same commands, so the five rows you use most were
the five rows the list showed twice. Promoting to Recent now moves a
command rather than cloning it, and a group left empty by that drops its
header with it.

Ranking also threw frecency away the moment a character was typed —
ordered by usage at zero query, ordered purely by fuzzy score after
that, so the command someone runs every day stopped floating exactly
when they started reaching for it. A bounded log bonus (0..=24, against
16 points per matched character) now rides along with the match score:
enough to separate two comparable matches, not enough to let a
well-worn command outrank a plainly better one.
2026-08-08 01:12:16 +08:00
l0ng-ai 158195e2a6 fix(ui): give icon-only buttons a 24x24 target without changing how they look
gpui-component renders an icon-only .xsmall() button as a 20x20 box, and
two sites overrode a 32px chrome tile down to 18x18. WCAG 2.2 SC 2.5.8
puts the desktop floor at 24x24, and the tab close button — the one that
ends a shell — was among the smallest.

Adds tab_strip::hit_target, which grows only the box. The glyph keeps
its size, so the chrome is visually unchanged; the tab chip's close
button moves from top: 5 to top: 3 so it stays centred in the 30px chip.

Applied to: tab chip close, sidebar row close, managed forward delete,
SFTP tray dismiss, SFTP reveal-download, SSH forward-rule delete, and
both switcher overflow menus.
2026-08-08 01:07:02 +08:00
l0ng-ai a83b64935e fix(settings): make search mark what it counted, and write the page it promised
The search field computed per-section match counts and rendered them as
"Appearance (2)" badges, then left you to find those two rows among
sixteen. Nothing was ever filtered, highlighted or scrolled to.

- A row whose label or indexed keywords match the query now renders at
  full strength on a tinted fill; the rest of the section dims. Marking
  only happens when the section actually holds a match, so a query that
  landed elsewhere never greys out a page being read.
- A query that matches nothing anywhere says so, instead of leaving the
  page looking as if the search did nothing.
- Adds the "How shells work" block the index has pointed at since it was
  written. The entry routed to About, and About never mentioned
  persistence — the one thing that separates tty7 from any other
  terminal was not stated anywhere in the app. It now sits directly
  under the app identity, above Updates.
2026-08-08 01:01:36 +08:00
l0ng-ai 58256cffc1 fix(terminal): draw the block cursor as reverse video, not as a tint
Both cursor renderers painted a block at ~50% alpha over the finished
cell, so a caret the theme conditioned to 3:1 landed on screen at
roughly 1.9:1. Bar and underline carets were translucent too, for no
reason at all — they are thin marks that cannot occlude a glyph.

Every other terminal (Alacritty, WezTerm, iTerm2, Ghostty,
Terminal.app) paints an opaque block and redraws the character on top
of it. Do the same:

- element.rs inverts the cursor cell in the render buffer before the
  backgrounds go down, so the caret rides the normal
  background-then-glyph path; paint_cursor's focused block arm is now
  empty, and the unfocused outline is opaque.
- view.rs does the same for the shell input editor's own caret, which
  is the one users actually see at a prompt.
- The glyph is redrawn in whichever of the terminal's background or
  foreground reads better on the caret, via a shared presets::caret_ink.
2026-08-08 00:49:48 +08:00
l0ng-ai 23ee6fa99c fix(theme): floor the hairline on every neutral fill it divides
Review catch on #400: border is handed to sidebar_border and drawn on
popover chrome, but was floored against the window background alone —
a narrower guarantee than the one this PR gives sidebar_fg and the
semantic inks.
2026-08-08 00:35:37 +08:00
l0ng-ai 2a40b265d7 fix(terminal): localize the right-click menu and the search bar
The terminal's own context menu was the last surface still speaking
hardcoded English in a three-locale app — 14 literals, while the tab and
switcher menus route every item through t(). The search bar had three
more, and its Previous / Next / Close buttons were icon-only with no
tooltip at all.

It also spoke a fourth vocabulary for one action: "Maximize Pane" for
ToggleMaximizePane, which the menu bar, the palette and Keybindings all
call "Zoom Pane". "Close Pane" is likewise "Close Pane / Tab"
everywhere else, and it is the accurate name — the action closes the
tab when the pane is the last one.

Adds AppMenuSplitLeft / AppMenuSplitUp and SearchFind / SearchMatchCase
/ SearchUseRegex across en, zh and ja. The fork submenu now takes its
title from the locale table instead of tty7-core's fork_label(), which
is a capability probe in a crate that has no locale table.
2026-08-08 00:33:56 +08:00
l0ng-ai 5d46751dd0 fix(theme): floor the tokens that bypassed the contrast machinery
Sidebar text, the caret and hairlines were flat blends with no floor,
sitting one line away from tokens that are bisected to hit a target
exactly. Semantic inks were floored against the window background but
painted on popovers and sidebar rows, which sit a step toward the
foreground.

- sidebar_fg is now floored at 4.5:1 on the sidebar fill it is painted
  on, not on the background. Four builtins landed it at 3.35-3.92:1.
- caret is conditioned to 3:1. The default Light theme shipped an
  orange caret on pure white at 2.07:1.
- border keeps its blend but gets a 1.5:1 floor so a divider is worth
  the same in every theme, instead of ranging 1.26-1.64:1.
- semantic inks and fills clear their floor on background, sidebar and
  popover, not just the background.

Themes that already cleared a floor are untouched.
2026-08-08 00:27:01 +08:00
l0ng-ai 1f91719da9 fix(terminal): paste images into remote panes on macOS, and into WSL by its own path (#399)
Staging a pasted screenshot to a file and handing the agent its path was
built off macOS only. A local macOS agent reads the system clipboard itself
when it sees SYN, and that carries the image at full fidelity, so the
platform kept SYN and skipped staging entirely.

That reasoning stops at the pane boundary. An agent in an SSH pane or a
remote workspace reads the clipboard of the host it runs on, which never
holds this machine's screenshot, so SYN was a no-op and pasting did nothing
at all. Remote panes now stage and upload on every platform, decided by one
predicate instead of a platform cfg over the whole feature; a local macOS
pane still forwards SYN untouched. macOS screenshots reach the pasteboard as
TIFF, which agent vision rejects the same way it rejects a Windows BMP, so
those transcode to PNG on the way out.

A WSL pane was broken for a different reason: it shares this machine's disk
but not its path syntax, so it needs no upload, only a rewrite -- and there
was none. The agent got C:\Users\...\paste-1.png and found nothing there.
The paste now carries the automount view, /mnt/c/Users/... A path with no
mapping, such as a UNC temp directory, keeps the Windows name, which at
least says where the file went.

Seven pre-existing tests for this feature were compiled out on macOS by the
same cfg and now build and pass there. Three were added: the staging
predicate, the WSL rewrite (including UNC and drive-relative refusals), and
the TIFF-to-PNG transcode, whose failure mode is otherwise a silent no-op.
2026-08-08 00:05:56 +08:00
l0ng-aiandl0ng-ai e4fff64833 fix(daemon): stop offering to carry on beside a mismatched server (#389)
Both handshakes compare their version for equality and hang up on
anything else — the pane protocol in `ensure_running`, the control
dialect in `host::server`'s hello. So a server whose number disagrees
cannot be talked round, and the "Keep Shells" button offered a state
that does not work: panes still spawn while every machine-tree call is
refused, which is how a window opens with no tabs and saves none of the
ones you make. Taking that button was indistinguishable from the bug it
sat next to.

Restart or quit, then. Quitting is the half that destroys nothing — the
server and every shell under it keep running — which makes it a real
answer for someone who would rather go install the matching build than
lose a session mid-flight. Quit goes first, the way every other
destructive prompt here is ordered: NSAlert and TaskDialog give the
first button Return, and this prompt arrives unasked at launch, the
moment a stray Return is likeliest, so the reflex key has to be the one
that destroys nothing. Restarting stays a click or an arrow away.

A prompt dismissed without an answer re-arms instead of falling through:
`take_mismatched_daemon` takes, so without this a closed window is a
back door into the state this prompt exists to prevent.

The copy is rewritten to one line of diagnosis and one line per button,
in all three locales, listed in button order. It was a paragraph that
had to be read twice to find out what the buttons did.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-07 23:41:37 +08:00
ayamir 43a22855a2 perf(graphics): move kitty frame pixels instead of copying them (#388)
A re-transmitting sender like terminal-browser sends a fresh full-window
frame per rendered frame — ~26 MiB of RGBA at Retina resolution. On the
client that buffer was copied twice for no reason on the way to the atlas:
`decode_frame` allocated a new Vec for the payload tail behind the 30-byte
header, and the uncompressed path of `to_rgba8` then cloned it again before
the in-place BGRA swap.

Thread ownership through instead:

- `Image::decode_frame_owned` consumes the frame Vec the reader already owns
  off the socket and drains the header off the front, reusing that allocation
  as the pixel buffer rather than allocating and copying a fresh one.
- `Image::take_rgba8` moves the pixel buffer out on the uncompressed fast
  path (the shm/file transport hands us pixels already in `f=32` layout), so
  `decode` swaps R<->B in place with no clone. The compressed inflate, the
  PNG guard, and the declared-dimension inflate bound are unchanged; `f=24`
  still repacks because RGB->RGBA changes the length.

Removes two ~26 MiB per-frame touches on the client hot path. On a 3216x2160
frame the decode+normalize step drops from ~1.78 ms to ~0.89 ms — ~0.9 ms
saved per frame, ~53 ms/s at 60fps. This does not touch the wire frame layout
or the daemon-side transfer; it is a pure client-side allocation cut.
2026-08-07 23:30:11 +08:00
l0ng-aiandl0ng-ai bc03e59b6f ci: keep the fork review to forks (#398)
The fork job never checked where the head branch lived. It was written
as the fork path and reads like one, but `pull_request_target` fires on
every pull request, so labelling one of ours landed there too --
silently, and with the weaker review: no plugin, no whole-repo context,
read-only tools. #389 got that instead of the review it should have had.

Also corrects the `labeled` comment in claude-code-review.yml, which
promised exactly the case that cannot work. A `pull_request` workflow is
read from the PR's merge ref, and GitHub recomputes that on a push and
not otherwise, so a PR whose last push predates the file has a merge ref
without it and no label can summon it. Pushing fixes it, and also
triggers synchronize by itself -- which is why the empty commit worked
and the label looked broken.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-07 23:26:29 +08:00
l0ng-aiandl0ng-ai 51ea055d2a ci: anchor fork review findings to lines, and fence the reads (#397)
Two changes to the same job.

Findings now go inline. The action ships an inline-comment MCP tool
that buffers rather than posts: the queue is sent after the session
ends, by the action's own code, past a classifier. That keeps the
property review.md was chosen for -- the reviewer writes the words, a
step that cannot be argued with performs the act -- while putting a
finding next to the line it is about. review.md stays for the summary,
which belongs to the change as a whole.

Reads are fenced. A comment body is posted verbatim, so any file the
reviewer can read it can publish, and the token is in this process's
environment; /proc is the short path between the two. Reads are already
confined to the working directory in the default permission mode, but
this job should not rest on a default. Deny is evaluated before allow,
so the rules hold whatever --allowedTools says.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-07 23:13:12 +08:00
l0ng-aiandl0ng-ai ea0a0f078c ci: keep reviewing a fork PR after the label goes on (#395)
The label was a one-shot trigger, so a contributor who addressed the
findings and pushed got no second look unless someone removed and
reapplied it. It now reads as a subscription: `synchronize` re-reviews
while the label is present, and taking it off stops that.

Two clauses rather than one label test, because `labeled` carries the
label that was applied and `synchronize` carries none -- testing only
the list would spend a review every time an unrelated label landed on a
subscribed PR.

This does not gate the code in the next push, and the header comment now
says so. The gate is spend and attention; the safety is the base-branch
workspace root, the absent Bash, and executing nothing.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-07 23:04:11 +08:00
l0ng-aiandl0ng-ai 59b26ce283 ci: stop the fork review prompt from reading as a checklist (#396)
The general instruction was one line -- logic errors, edge cases,
regressions -- followed by a numbered list of four, three of them
tty7-specific. A list anchors, and that one named none of what actually
breaks a terminal emulator: panic paths, unsafe, ordering, resources on
the failure path, an API contract that no longer holds for its new
callers.

Replacing it with a longer list would only move the boundary. So the
prompt now states the aim, offers examples while saying outright they
are not a checklist, and notes that the finding nobody listed is
usually the one worth having. The repo rules stay, demoted to what an
outside reader cannot know and explicitly skippable.

claude-code-review.yml is left alone: its rules are appended to the
code-review plugin, which brings its own methodology, so a general
clause there would compete rather than add.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-07 23:02:41 +08:00
l0ng-aiandl0ng-ai fb4e8f0860 ci: post the fork review from a step, not the action (#394)
track_progress only fires on opened, synchronize, ready_for_review and
reopened. The approval gate depends on `labeled`, which is none of
them, so the action refused the run outright rather than falling back
to the log.

The review is written to review.md and posted by a final step that runs
no model and reads one file. That adds Write to the allowlist, which
costs nothing: there is still no Bash, so still no curl, and the runner
is discarded after the comment goes out.

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