Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
developtomainfor 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, andpnpm test:e2eagainst 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.