feat(git,accounts,ui,settings): in-app forge sign-in, reconnect & session health
Brought to you by:
thebguy
Originally created by: theBGuy
Originally owned by: theBGuy
This change introduces a complete in-app forge sign-in and session health experience, allowing users to sign in to GitHub and GitLab directly from the app (no terminal required), receive proactive expiry warnings, and seamlessly reconnect expired or revoked sessions. The motivation is to eliminate context-switching to the terminal for auth flows, improve trust in session state (robust anti-flap detection), and ensure users are aware of expiring credentials before disruptions occur.
src-tauri/src/forge/session.rs, with robust anti-flap rules (double-probe before declaring broken, never alarm on transient outages).SessionHealth and related types (SessionState, etc.) across backend and frontend (src-tauri/src/forge/session.rs, src/lib/git/types.ts).src-tauri/src/lib.rs for session health and reconnect control: forge_session_health, forge_accounts_health, forge_reconnect, forge_reconnect_cancel.src-tauri/src/github/pr.rs and src-tauri/src/forge/mod.rs for integration.src/features/accounts/ReconnectDialog.tsx with device-code flow (GitHub), browser flow (GitLab), and status streaming via Tauri/Channels.src/App.tsx and exposes open/close actions via store changes in src/lib/stores/ui.ts.src/lib/git/api.ts and manage UI state diligently.src/features/accounts/SessionExpiryNotice.tsx, called in src/features/pulls/PullRequestsPanel.tsx and src/features/issues/IssuesPanel.tsx.src/lib/settings/api.ts, src/features/settings/AccountsSection.tsx.src/features/repository/ForgeNotReady.tsx) to distinguish new session, expired session, and network blips, surfacing the ReconnectDialog and affording both CLI and terminal fallback.src/features/settings/AccountsSection.tsx) to surface session health state badges per account, show "Reconnect" on expired/logged-out sessions, and merge expiry data for Bitbucket and other providers.src/lib/hotkeys/registry.ts) opens the in-app reconnect experience, context-aware by provider/session state (src/features/repository/RepositoryView.tsx).README.md, src/features/help/content.ts, site/src/data/capabilities.ts).changelog.d/added-forge-session-health.md summarizing the features at user level.
Originally posted by: cloudflare-workers-and-pages[bot]
Deploying gitdesktop with
Cloudflare Pages
9c35142View logs
Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedI'll review this PR. Since large portions of the diff are truncated (the 1611-line
session.rs, and parts ofReconnectDialog.tsx,AccountsSection.tsx, the panels), let me read the actual files to ground my review.Let me read the remaining truncated portions to ground the review.Let me verify a couple of things: the layout wrapper in the panels (doesConversationListPanelfill correctly?), andproviderLabel.Confirmed the layout wrapper matchesConversationListPanel's own root (flex min-h-0 flex-1 flex-col), so no layout regression there, andproviderLabelhandles all three providers. Here's the review.Summary
Adds in-app
gh/glabsign-in + reconnect with anti-flap session-health probing, expiry warnings, and a cancellable child-process driver. The design is sound and unusually well-defended (input validation, token redaction, RAII cleanup, the cancel-before-register race handled, solid Rust unit tests). Not blocking, but there's a real (if slow) registry leak and a cross-surface day-count inconsistency worth fixing before merge.Correctness / resource management
src-tauri/src/forge/session.rs,cancel_reconnect/ theRECONNECT_REGISTRY. The module comment promises "Entries are removed on EVERY exit path," butcancel_reconnectusesentry(...).or_insert_with(...), so a cancel that arrives after the flow already finished re-creates an orphan tombstone that nothing ever removes. Concrete path that fires on every successful reconnect: the child exits →forge_reconnectreturns →ReconnectGuarddrops →unregister_reconnectremoves the entry → the frontend'sonEventschedulessetTimeout(onClose, 1200)→ 1.2s later the dialog unmounts →ReconnectDialog's cleanup callsforgeReconnectCancel(id)→cancel_reconnectinserts a freshNotifytombstone for that dead id that is never adopted or removed. The "Try again" path (start()cancels the prior, already-finished id) leaks the same way. It's small and bounded per attempt, but it grows unbounded over the app's lifetime and contradicts the stated invariant.finishedRefset when a terminalfinishedevent arrives, and have both the unmount cleanup andstart()'s prior-cancel skipforgeReconnectCancelonce the session has finished. Belt-and-suspenders (backend): havecancel_reconnectnotify-if-present, and when it creates a tombstone, spawn a short delayedunregister_reconnectso an unadopted tombstone can't linger past the spawn window.Consistency
YYYY-MM-DD. Insrc/features/accounts/SessionExpiryNotice.tsx,daysUntildoesMath.floor((Date.parse(iso) - now)/86_400_000)whereDate.parse("2026-07-20")is parsed as UTC midnight. Insrc/features/settings/AccountsSection.tsx,daysUntilDatedoesMath.ceil((Date.parse(${date}T23:59:59) - now)/86_400_000)— local end-of-day withceil. For a token expiring 2026-07-20, the PR/Issues banner can say "expires in 3 days" while the Accounts badge says "expires in 5 days" for the identical token. Fix: extract one shared helper (same anchor time and rounding) and use it in both surfaces.SessionExpiryNoticerenders "expires today" for a negative (already-lapsed) Bitbucket count, whereasAccountsSectionclamps to0. Folding both into the shared helper resolves this too.Nits
src/features/repository/RepositoryView.tsx, thereconnect-forge-sessionhotkey.modeissessionHealth.data?.state === "notConnected" ? "login" : "refresh"; the action is gated only onforgeProvider !== null, not on health having loaded. For a never-signed-in GitHub repo whereuseForgeSessionHealthhasn't resolved yet,dataisundefined→mode: "refresh"→gh auth refresherrors ("no account"). It degrades to a failed-finish with fallbacks rather than a crash, but first-run from the palette can open the wrong mode. Consider defaulting to"login"whensessionHealth.datais undefined (or gating the action until health resolves).Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI security audit ·
opus· automatedI'll ground my review by reading the actual source that the diff truncated — especially the Rust reconnect driver and how attacker-influenceable values (host, session output) flow into subprocess spawns and streamed events.Let me quickly verify the truncated frontend files introduce no unsafe sinks (HTML injection, URL opening from untrusted data).No high-confidence, exploitable security vulnerabilities are introduced by this diff.
I traced the security-relevant flows in the new code:
forge_reconnectsubprocess spawn (session.rs): every input is validated before spawn —session_idagainst[A-Za-z0-9-]{8,64},hostrestricted to[A-Za-z0-9.-],modetologin|refresh,providermatched to a fixed set; the binary names (gh/glab) are hardcoded and resolved viaresolve_named. Args are passed as discrete argv elements totokio::process::Command(no shell), andhostis always bound as the value of--hostname, so no command- or argument-injection path exists.redact_tokens(gh/glab token prefixes) + 300-char truncation before reaching the frontend;detail/message/loginare all derived from sanitized lines. No token/secret is forwarded or logged.openUrlsinks: inAccountsSectionthey target hardcoded constant URLs; the reconnect dialog opens only the device-flow URL for the already-configured forge host (https, GHES-support behavior gated behind explicit user action on a clearly-labeled host) — not an attacker-controlled protocol.bitbucketTokenExpiresAtis a non-secret user-entered date;SessionExpiryNotice/AccountsSectionrender via React (nodangerouslySetInnerHTML, no eval).The one prior comment flagged a registry-entry leak in
cancel_reconnect— that's a resource-cleanup bug, not an exploitable security issue, and falls under the excluded resource-exhaustion category.Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
Triage — round 1 dispositions (all findings verified against the code; fixes land in the next push)
forgeReconnectCancel, whoseentry().or_insert_with()re-creates an orphan tombstone; Try-again leaks the same way. Fix: (1) frontend clearssessionIdRefon the terminalfinishedevent so completed flows are never cancelled; (2) backend validates the id inforge_reconnect_cancel(same grammar asforge_reconnect) and self-cleans unadopted tombstones via a delayed sweep (adoption is observable through the Arc refcount — a running flow holds a clone). The module's "removed on every exit path" invariant becomes true for cancel-created entries too. (This also covers the security audit's relay of the same finding.)daysUntilfloors from a UTC-midnight parse;daysUntilDateceils to local end-of-day — same token, different counts, plus the today-vs-0 clamp drift. Fix: one shared local-calendar-day helper (pure date-to-date diff) used by the notice and the Accounts card, unified expired/today wording.loginwhen health is unresolved would silently narrow granted scopes for the common signed-in case:gh auth loginrequests the default scope set, whilegh auth refresh"maintains previously added scopes" (its own help) — a user withworkflow/extra scopes would lose them. Fix instead: gate the palette action'senabledon session health having loaded, so the mode decision is never made blind. The failure described (refresh on a never-signed-in host) already degrades to the failed-finish fallbacks, and can no longer be reached from the palette.Copilot's six inline findings are all confirmed too (dispositions on each thread): mode-aware terminal fallback command,
finished-clears-ref, UTC-midnight date parse (folds into the shared helper), unknown-flag-only--jsonfallback with other non-zero →offline, a 300-char bound insanitize_detail, and id validation inforge_reconnect_cancel. A genuinely clean 7/7-real round.Posted by GitDesktop — automated agent comment, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedI'll ground my review by reading the truncated files — especially
session.rs(the reconnect driver and registry), and the truncated frontend portions.Let me read the rest ofsession.rs(tests and remaining driver code) and the truncatedAccountsSection.tsxportion.Let me check the Bitbucket expiry-date input handling and clear-on-disconnect wiring inAccountsSection, plus the GitLab sign-in block, since those parts weren't shown.This is a well-executed change, and I've verified the current state against both the previous review and Copilot's findings by readingsession.rs(full, incl. tests),ReconnectDialog.tsx, and theAccountsSectionBitbucket/GitHub blocks.Summary
Adds in-app
gh/glabsign-in + reconnect with anti-flap session-health classification, expiry warnings, and a cancellable child-process driver. The design is sound and unusually well-defended (input validation, token redaction, RAII cleanup, the cancel-before-register race, and a good spread of Rust unit tests including the new classifier/sweep/id-grammar cases). Every issue from the prior review and every Copilot item is addressed in the current diff — I found no new blocking or should-fix problems. Ship it.Resolved since last review
session.rscancel_reconnect+ReconnectDialog.tsx) — fixed on both ends. The frontend now nullssessionIdRef.currenton the terminalfinishedevent (line 122), so the unmount cleanup andstart()'s prior-cancel no longer cancel an already-finished flow. As belt-and-suspenders,cancel_reconnectnow distinguishes anOccupiedentry (adopt, let the guard clean up) from aVacantone (insert a tombstone), and only for the created-tombstone case schedulessweep_unadopted_tombstoneafter 60s, which removes the entry iffArc::strong_count == 1under the lock. A running flow (which holds a guard clone → count ≥ 2, and tolerates the full 900sRECONNECT_TIMEOUT) is correctly spared. Covered bysweep_removes_unadopted_tombstone/sweep_spares_adopted_entry.daysUntilfloor-from-UTC-midnight,daysUntilDateceil-to-local-EOD) are gone; bothSessionExpiryNoticeandAccountsSection.BitbucketAccountnow compute from the sharedcalendarDaysUntil(pure local date-to-date diff), and the expired/today/N-days wording is unified across both surfaces. The clear-on-disconnect path (setBitbucketTokenExpiresAt(null)at line 721) is wired too.reconnect-forge-sessionaction is now gatedforgeProvider === "bitbucket" || (forgeProvider !== null && sessionHealth.data !== undefined), so github/gitlab never derivemodefrom undefined health; the comment documents the deliberate choice not to default to"login"(scope-narrowing) either.Copilot findings — all resolved in the current diff
fallbackCommandfor refresh → nowgh auth refresh --hostnamewhenisGitHub && mode === "refresh"(line 101–104).sessionIdRefleft set after finish → nulled at line 122.daysUntilUTC date-only parse → replaced bycalendarDaysUntil.gh_status_jsontreating any non-zero as unknown-flag → nowclassify_gh_json_nonzerosplits unknown-flag (→ text fallback) from other failures (→Inconclusive→Offline), tested.sanitize_detaillength bound → now redacts, collapses newlines, and caps at 300 chars (tested bysanitize_detail_caps_at_300), and the gherror-field call sites route through it.forge_reconnect_cancelid validation → now rejects malformed ids viavalid_session_idbefore touching the registry (line 900–902).No further findings.
Posted by GitDesktop — AI output, verify before acting on it.
Ticket changed by: theBGuy