Skip to content

release: 0.2.2 - #22

Merged
lorem-dev merged 14 commits into
mainfrom
develop
Jul 27, 2026
Merged

release: 0.2.2#22
lorem-dev merged 14 commits into
mainfrom
develop

Conversation

@lorem-dev

Copy link
Copy Markdown
Owner

Promotes develop to main for the 0.2.2 release.

The bulk of this release is the Windows terminal: repository Git ran in a
pseudo-terminal of its own there, where an SSH key passphrase prompt never
surfaced, so a clone that needed one waited forever and -- because the wait was
unbounded -- took every later Git operation down with it. Git now runs in the
interactive terminal on Windows as it already did on macOS and Linux, the
terminal is raised and focused for any prompt that needs an answer, and both
waits are bounded so one stuck command can no longer wedge the session.

Alongside it: a right-click Copy/Paste/Select all menu in the terminal, a
documentation section on setting up an ssh-agent (linked from the app when none
is running), and the release workflow moved off the deprecated Node 20 runtime.

Full gate green: cargo fmt --check, cargo clippy --all-targets, cargo test,
pnpm lint, pnpm typecheck, pnpm test:cov, the renderer build, and
pnpm test:e2e against the fixture submodule.

Caveat worth recording: the Windows passphrase path itself was never
exercised on a real Windows machine. Every layer around it was confirmed there
from screenshots -- the command reaches the shell, runs, reports its exit code,
and the queue no longer stalls -- but no run ever got as far as ssh actually
asking for a passphrase.

lorem-dev and others added 14 commits July 25, 2026 18:27
chore: back-merge main into develop
A shell that failed to spawn produced no error anywhere: the renderer
issued the start as a floating promise, so the view stayed blank. The
same dead session also made repository git silently fall back to the
headless port, which prints nothing -- so a clone looked like it never
ran at all.

Report it at every layer instead. The renderer retries the start up to
three times, then shows the reason in the terminal and logs a warning
saying git still runs but without visible output. A new terminal_status
command exposes whether a session is live and why not, re-read whenever
the shell exits so a failed auto-restart is caught too.

Also stop losing the reason on the way out: a pseudo-terminal backend
that panics rather than erroring -- Windows before 10 1809, which does
not export CreatePseudoConsole -- is caught so its message survives
instead of unwinding into an opaque join error, git launch failures on
the standalone-process path are printed rather than reduced to an exit
code, and shell/git spawn errors name the shell, the working directory,
and the PATH they tried.
The overlay body could grow but never shrink. A flex item's automatic
minimum size is its content size, and the content here is the xterm
grid, so a smaller window left the host element at its old height: the
view's ResizeObserver never fired, the grid was never refitted, and it
overflowed the visible slot. Give the body min-height: 0 so it can
actually shrink, and pin the header with flex: none so the body stays
the only flexible row.

Also switch the body from overflow: hidden to overflow: clip. A hidden
box is still a scroll container, and the browser scrolls it on its own
to reveal a focused descendant -- xterm's absolutely positioned helper
textarea -- which left the first rows hidden behind the header with no
scrollbar to bring them back. A clip box is not scrollable, so nothing
can shift the grid.
GitHub now annotates every run that uses an action targeting Node 20,
forcing it onto Node 24 and warning once per job. Move each one to the
first major that declares node24 itself.

actions/upload-artifact v4 -> v6 and actions/download-artifact v4 -> v7:
the intervening majors (upload v5, download v6) still declare node20, so
a single-step bump would not have cleared the warning. download v5 did
change the output path for single downloads BY ID, which does not apply
here -- the release job downloads every artifact at once. Also move
softprops/action-gh-release to v3, astral-sh/setup-uv to v7 (used with
no inputs, so none of its v6/v7 input removals apply), and the one
remaining actions/checkout@v4 in backmerge to v5, matching every other
workflow.
The closed terminal overlay was hidden with `display: none`, which left its
xterm host without a layout box. The fit addon sizes the grid from
`parseInt(getComputedStyle(host).width)`, and a box-less element reports the
computed string "100%" -- parsed as 100 -- so the PTY started inside a 100x100
phantom box, a dozen columns wide. The shell hard-wrapped its banner to that
width (the wrap is baked into the emitted bytes, so no later resize reflows
it), and opening the overlay changed the column count, which tripped the
`term.clear()` reflow guard and dropped every row above the cursor. What
remained on screen was the tail of a wrapped banner line.

Hide the overlay with `visibility` instead: the box (and the correct size)
survives while the overlay is closed, and hit-testing is still off, so the
draggable header projects no drag region over the app -- the reason plain
`opacity` was not enough. Guard the initial fit against a zero-sized host as
well, so a box-less host falls back to xterm's 80x24 default rather than the
phantom geometry.

Also make the overlay body a proper flex slot: without `min-height: 0` its
automatic minimum size is the grid's own height, so the terminal could grow
but never shrink -- the host never changed size on a smaller window, so the
view's ResizeObserver never fired to refit it. Clip instead of hiding its
overflow, since an overflow:hidden box is still a scroll container the browser
may scroll to reveal xterm's off-screen helper textarea.
On Windows the first git command run through the terminal never
returned, and every later one silently did nothing at all.

After git exited, run_git_process joined the reader thread while still
holding the command's master in active_cmd_master. The reader ends at
end-of-file, and end-of-file needs every writer on the pty closed --
but the Windows pseudo-console keeps its own duplicate of that write
handle until the master is dropped, which is what we were holding. So
the join waited on an event that could not happen: run_git never
returned, its GitQueue slot was never freed, and each following clone,
sync or update check blocked before printing even its command header.
That is why the terminal accepted typing yet showed no git, an update
check sat unfinished, and adding a repository looked inert.

Drop the master (and clear the input routing) before waiting, so the
console closes and the reader reaches end-of-file. POSIX is unaffected:
its reader holds an independent dup and takes end-of-file from the
child's slave closing. Bound the wait too, so a reader that still
cannot finish stalls one thread instead of the whole queue.

Cover it with a regression test that fails, rather than skips, when the
standalone path does not return and hand input routing back. Also picks
up the Cargo.lock version bump the release commit did not include.
Git runs in the terminal through its own pseudo-terminal, and the app
waited for it to exit with no bound at all. A clone that blocks on an
SSH key passphrase -- a prompt that never reaches that pseudo-terminal
on Windows -- therefore held its GitQueue slot for the rest of the
session. Every later clone, sync and update check then blocked inside
the queue, before the code that prints the command header, which is why
the terminal showed nothing whatsoever and a repository could not be
added until the app was restarted.

Bound the wait on SILENCE rather than on runtime. A large clone reports
progress continuously, so its activity clock keeps moving and it is
never affected however long it takes; a command that produces no output
at all while still running is the signature of a prompt nobody can
answer. That one is killed, the queue is released, and the terminal
explains the likely cause instead of going quiet.

This is containment, not a cure: the passphrase prompt still does not
reach git's pseudo-terminal on Windows, so such a clone now fails with a
message rather than hanging the app.
Cloning a repository whose SSH key has a passphrase could not work on
Windows. Git ran there in a pseudo-terminal of its own, and the
passphrase prompt never surfaced in it: git printed nothing and waited
forever, holding the git queue so every later operation went silent too.
Running the same command by hand works, because there the prompt appears
at the console the user is typing into.

Do that instead. Windows now takes the in-shell path POSIX already used,
so git shares the terminal the user is already looking at and the prompt
can be answered. A private pseudo-terminal is left only for POSIX shells
we could not hook, where nobody could answer a prompt anyway.

Windows shells accept no precmd/PROMPT_COMMAND hook, so there is no
invisible marker to read the exit code from. The typed line ends with an
echo of a `__skk_done_<exit>__` sentinel instead, which the output
scanner strips exactly as it strips a marker. cmd needs `call echo
%^ERRORLEVEL%`: an interactive line is expanded in full before any of it
runs, so a bare `%ERRORLEVEL%` would report the PREVIOUS command's code.
Requiring digits is what keeps the shell's echo of that line from being
mistaken for the sentinel it will later print. Readiness comes from a
lull in output, since there is no bracketed paste to key off.

Arguments are quoted per shell -- doubled quotes inside double quotes for
cmd, single-quoted literals for PowerShell -- so a remote URL or path
cannot carry a metacharacter into command position.

The wait is bounded by inactivity rather than runtime, counting typing as
activity: a user reading and answering a passphrase prompt keeps it
alive, while a command with no output and no input for two minutes is
interrupted and reported instead of holding the queue.

An ssh-agent is still used whenever one holds the key, in which case
nothing is asked at all; this only makes the no-agent case usable.
The update sweep fetched every tracked repository, including ones whose
clone does not exist -- because it failed, or the directory was removed.
Such a repository has no local commit to compare against, so the check
could only ever answer "no update"; it just answered it by running git
in a missing directory first, which reported

  fatal: cannot change to '...': No such file or directory

into the terminal on every startup, once per uncloned repository.

Check the clone exists before fetching, as list_branches already does.
Getting text out of the terminal was reachable only through a keyboard
shortcut, and not an obvious one: it differs per platform, and in a
terminal a bare Ctrl+C sends an interrupt rather than copying, so the
combination is Ctrl+Shift+C. Nothing on screen said so.

Add the three actions to a right-click menu built on the shared Menu,
anchored to the click point by a zero-size marker (Menu positions against
an element's rect). Copy is disabled when nothing is selected, checked
once when the menu opens so the entry cannot go stale while it is up.

Select all earns its place beyond discoverability: when a full-screen
program has taken over the mouse, dragging cannot select, and this is
then the only way to reach the output.
Surfacing the terminal when git blocks on a passphrase already existed,
but three things kept it from working when it matters.

The signal is debounced to once per prompt and was re-armed only by
bracketed paste, which a Windows shell never sends. On the in-shell path
that shell now runs git on, the debounce therefore latched after the
first prompt of the session and every later sync or clone waited
silently. Re-arm it wherever the shell becomes ready again -- for a
hooked shell that is still the ready prompt, for an unhooked one its
completion sentinel.

Git over HTTPS asks `Username for '...'` and `Password for '...'`,
neither of which contains a bare `password:`, so a credential prompt
never counted as one at all. Add both.

Finally, opening the overlay left the focus on the container, so the
answer went nowhere until the user clicked into the terminal -- which is
a strange thing to have to discover while staring at a passphrase
prompt. Focus the terminal itself whenever the overlay opens.
Without an ssh-agent, Git asks for the key passphrase on every single
operation. That is correct behaviour, but with no explanation it reads
as the app being broken rather than as a machine that needs setting up.

Add a documentation section covering what an agent does and how to set
one up on macOS, Linux and Windows, plus when pinentry is involved (only
when SSH is routed through gpg-agent, e.g. for a smartcard). The Windows
instructions call out the trap behind this whole thread: Git for Windows
uses its own bundled ssh, which does not talk to the Windows agent
service, so the key sits in the agent while Git keeps asking -- until
core.sshCommand points at the system client.

Link to it from the app. When the terminal is raised for a prompt, check
whether an agent exists (SSH_AUTH_SOCK anywhere, or the OpenSSH named
pipe on Windows) and, if not, log one entry per session pointing at the
section. Checked at that moment rather than at startup, because an agent
can be started while the app runs.

Notification entries gain an optional documentation URL: the log renders
it as an inline link, and a toast carrying one opens it when clicked,
since the toast is gone in five seconds and the log entry behind it is
easy to miss.
@lorem-dev
lorem-dev merged commit c501c0c into main Jul 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant