feat(terminal,automation): add custom terminal command and reclaim stale...
Brought to you by:
thebguy
Originally created by: theBGuy
Originally owned by: theBGuy
Bundles two independent improvements: a new Custom command… terminal mode that lets you launch any shell-free command with a {path} placeholder (for multiplexers, wrappers, and terminals auto-detection doesn't know), and a fix for automation review claims that were starving PRs for up to 30 days when an app instance died before releasing them. The claim fix also makes missed-review catch-up work per review mode, so a failed general review is retried even after the security audit ran.
launch_custom_command in src-tauri/src/fsops.rs, plus helpers tokenize_command, substitute_path, is_batch_file, and first_token_is_pathlike: it tokenizes the template (double-quote grouping, no shell), substitutes {path} per token so spaced paths stay a single argv entry, resolves the first token to an absolute executable via agent::resolve_named, and spawns it rooted at the repo. Batch files (.cmd/.bat) are rejected to avoid re-introducing a shell through cmd.exe.open_in_terminal in src-tauri/src/fsops.rs with a command parameter and dispatches the new custom-command kind ahead of the per-OS matchers.launch_terminal_unix so a plain (non-.app) executable is spawned directly rooted at the repo instead of being mis-launched through open -a.src-tauri/src/fsops.rs for tokenizing, path substitution, batch-file detection, and path-like first-token detection.src/features/settings/TerminalSection.tsx: a new mode with a monospace command Input, per-platform placeholder, a non-blocking warning when {path} is missing, and mode-switching that preserves the other mode's stored value.terminalCommand field (and default) to AppSettings in src/lib/settings/api.ts, and threads command through openInTerminal in src/lib/git/api.ts.terminalCommand at every "Open in terminal" call site: ReconnectDialog.tsx, ChangesEmptyState.tsx, ForgeNotReady.tsx, RepoList.tsx, RepositoryMenu.tsx, and SessionOpenMenu.tsx.STALE_CLAIM_AGE (30 minutes) in src-tauri/src/automation_claims.rs and splits the exclusive-create into create_new_claim, so claim_in_dir now reclaims a claim whose mtime is older than the threshold (best-effort delete + one retry, yielding on a lost race) instead of waiting for the 30-day sweep. Adds tests covering reclaim, fresh-claim denial, and that the reclaimed file holds the new key.pr-open first-review per mode in src/lib/automations/runner.ts: it skips any mode that already has a review record or a matching dismissed head, so synthesized pr-open events only fire the mode(s) still missing a review.prOpenEligible in src/lib/automations/sync.ts to return eligible when at least one mode still needs a review (previously required both modes missing), so a single failed or stolen mode can be caught up after the other mode ran.src/features/pulls/RemotePrView.tsx to reflect the per-mode eligibility.README.md and the External editor / Terminal section in src/features/help/content.ts to describe the custom-command mode and {path} placeholder.changelog.d/added-custom-terminal-command.md and changelog.d/fixed-automation-claim-starvation.md.
Originally posted by: cloudflare-workers-and-pages[bot]
Deploying gitdesktop with
Cloudflare Pages
0b76de2View logs
Originally posted by: theBGuy
Context for reviewers — deliberate calls and evidence, one claim per item:
STALE_CLAIM_AGEdoc comment. A DELIVERED review's dedupe does not depend on its claim: the pr-reviews record is written at delivery, and BOTH re-review gates (the new per-mode pr-open gate and pr-sync's same-sha skip) consult that record BEFORE the claim is ever taken. Reclaiming an old delivered claim therefore cannot cause a re-review. The accepted residual: a legitimately still-running review that outlasts 30 minutes can be double-claimed by a concurrently polling second instance — bounded to one duplicate review, strictly better than the 30-day starvation it replaces.prOpenEligibleflipped from "no prior in either mode" to "some mode missing" deliberately, paired with the runner's new per-mode pr-open gate — a synthesized catch-up pr-open runs ONLY the missing mode(s), never re-running a mode with a prior. The other caller (fireReadyReviewin RemotePrView) was verified under the new semantics and its comment updated; the gate, not claim dedupe, is what prevents double reviews there (the PR [#91] invariant, preserved).resolve_named(names, Some(bin_path))is exists-check-ONLY — it never falls through to the PATH search. That contract was live-confirmed the hard way (a bare-name template failed with "terminal command not found: cmd" in the dev app) and is whylaunch_custom_commandroutes by token shape: path-like first tokens (containing a separator) get the exists-check, bare names passNoneso the resolver runs its real PATH/PATHEXT + Unix login-shell lookup. Please don't propose collapsing this back to an unconditionalSome(first).{path}substituted per-token (a repo path with spaces/;/$()stays ONE argv token), the first token resolved to an ABSOLUTE path BEFOREcurrent_diris set (Windows resolves bare names against the child's cwd ahead of PATH — a repo committingwt.exemust never execute on a trusted template), resolved.cmd/.batrejected (Rust ≥1.77 routes batch files through cmd.exe — BatBadBut — silently reintroducing a shell),CREATE_NO_WINDOWon Windows, and noopen -aanywhere in command mode (it does not propagate cwd into a .app).cmd /c echo {path} > marker & cd >> markerproduced a marker whose two lines both equal the repository path — proving bare-name PATH resolution, tokenization, per-token substitution, argv passthrough, and the child's working directory in one shot. The Settings UI affordances (the new dropdown mode, the shell-free helper text, the non-blocking missing-{path}warning) were verified live as well.terminalCommandis a SEPARATE field fromterminalPath, and the dropdown value IS the mode — deliberately. Reusing one field would carry a stale value into the wrong launch arm when flipping modes; mode round-trips preserve BOTH stored values..appbundle now spawns directly withcurrent_dirinstead of mislaunching viaopen -a; the.appand missing-path fallbacks are unchanged.Verification on this head: full cargo suite 828 passed / 0 failed (3 new claims tests, 8 new fsops tests), clippy
-D warningsclean,pnpm buildgreen, scoped biome clean. The batch was adversarially spec-reviewed pre-PR; the one blocker found (item 5) was fixed and re-verified live.Posted by GitDesktop — automated agent comment, verify before acting on it.
Related
Tickets:
#91Tickets:
#99Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedSummary
Two independent changes: a shell-free "Custom command…" terminal mode (with
{path}substitution) threaded through every call site, and a stale-claim reclaim (30 min) for automation review claims plus a per-modepr-opengate so a single missing review mode can be caught up. Overall the design is sound — the claim-reclaim safety argument holds up against the diff (both re-review gates consult thepr-reviewsrecord before the claim is taken, so reclaiming a delivered claim can't re-fire a review), and the widenedprOpenEligibleis correctly bounded by the per-tickcatchUpAttemptedguard. One should-fix on the terminal-launch security invariant; the rest is solid.Correctness / Security
src-tauri/src/fsops.rs,launch_custom_command(viafirst_token_is_pathlike→resolve_named). The comment asserts "Either branch yields an absolute path," but that invariant does not hold for a relative path-like first token. Trace: a token like./bin/wtorbin\wtmakesfirst_token_is_pathliketrue →bin_path = Some(first)→crate::agent::resolve_named(&[first], Some(first)), andresolve_named(agent.rs:414-416) returnsPathBuf::from(p)unchanged whenpb.is_file()— it never canonicalizes.is_file()is evaluated against the app process's cwd, butCommand::new(resolved).current_dir(path)then resolves/executes that relative path against the repo dir. So the two disagree: on Unix (chdir-then-exec) the existence check validates the app-cwd copy while exec runs the repo's copy — a confused-deputy corner that the "resolve to ABSOLUTE beforecurrent_dir(repo)" comment is specifically claiming to prevent. Real-world likelihood is low (users are steered to bare names / absolute paths by the placeholders, and the check is against app cwd not the repo), but the stated security invariant is provably false. Fix: canonicalize the resolved path (std::fs::canonicalize) — or reject a non-absoluteresolved— before building theCommand, so the path that's checked is the path that's executed.Tests
tokenize_command,substitute_path,is_batch_file,first_token_is_pathlike) and for the reclaim state machine (stale reclaim, fresh-denies, rewrite-with-new-key). No test exercises a relative path-like first token — worth adding once the canonicalize fix above lands, since that's the branch where the invariant currently breaks.Accessibility (nit)
src/features/settings/TerminalSection.tsx,missingPathTokenwarning. The hint appears/disappears as the user edits the command but is a plain<p>, so a screen-reader user editing the field won't be notified. Considerrole="status"/aria-live="polite"on the warning paragraph so it's announced when it toggles. Optional — it's a soft, non-blocking hint, and it correctly pairs an icon with text (not color alone).Docs-sync looks handled deliberately: README Integrations, the in-app help section, and both changelog fragments were updated; the marketing site has no dedicated "open in terminal" capability line to extend, so leaving it untouched for this sub-mode is reasonable.
Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI security audit ·
opus· automatedNo security issues found. The
terminalCommandinput is sourced exclusively from the local user's settings store (never from a repo-shared config or any remote/attacker channel), so it's a trusted local setting; andlaunch_custom_commandspawns shell-free with single-token{path}substitution (no re-tokenization, no shell — a malicious repo path can't inject), resolves the program to an absolute path before settingcurrent_dir(repo)(defeating the Windows CWD-precedence hijack), and rejects resolved.cmd/.batfiles (BatBadBut). Theautomation_claimsstale-reclaim and therunner.ts/sync.tsper-mode gating change dedup behavior only, with no untrusted-data-to-sink or privilege-boundary crossing.Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
Round-1 dispositions (all four findings accepted and fixed; ready for the next push):
resolve_namedreturns a relative path-like token verbatim after exists-checking it against the APP's cwd, while Unix chdir-then-exec would resolve it against the REPO — checked file ≠ executed file. Fix is a pureensure_absolute(p, base)helper joining a relative resolution onto the same app cwd the exists-check used, applied before the Command is built. Chose join overcanonicalizedeliberately: the executed bytes stay identical to the checked bytes, and it avoids Windows\\?\verbatim-path quirks. Security comment rewritten to state the invariant honestly; pure unit test added (relative+base → joined absolute; absolute unchanged, platform-branched for the 3-OS matrix).{path}hint now carriesrole="status"(implicit polite live region); icon+text unchanged.Also: the 3-OS Rust matrix came back green on the reviewed head (
8247448); the fix delta re-ran fsops (21 passed) + clippy clean. Delta since review:fsops.rs+TerminalSection.tsxonly.Posted by GitDesktop — automated agent comment, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedSummary
Two independent changes: a shell-free "Custom command…" terminal mode (
{path}substitution, threaded through everyopenInTerminalcall site) and a 30-minute stale-claim reclaim for automation review claims plus a per-modepr-opencatch-up gate. The change is sound and I found nothing blocking. The previous review's one should-fix and its nits are all fixed in the current diff, and the widened catch-up eligibility — which I initially suspected of introducing a starvation regression — checks out once you follow the surrounding code.Resolved since last review
launch_custom_commandnow appliesensure_absolute(resolved, &std::env::current_dir()?)right afterresolve_named, pinning a relative resolution (./bin/wt) to the same app cwd the exists-check used, beforecurrent_dir(repo)is applied to the child. The security comment was also rewritten to state the invariant honestly, andensure_absolute_joins_relative_and_passes_absolutecovers both branches. Join-over-canonicalize is a reasonable call (byte-identical to the checked path, avoids Windows\\?\quirks).{path}warning not announced — fixed.role="status"added to the warning<p>(implicit polite live region); icon+text pairing preserved.tokenize_commandnow returnsInvalidArgumentwhen the scan ends inside a quote, withtokenize_command_unbalanced_quote_is_errorpinning both unterminated cases and the balanced happy path.spellCheck={false}).Correctness (verified sound)
The widened
prOpenEligible(now returns eligible when any mode still needs a first review, previously required both) is safe in the surrounding machinery, so it is not a finding:pr-opengate (runner.ts:275-292) skips any mode with a prior review record, so an already-reviewed mode can't double-fire; a manual panel review is also visible here viagetLatestReview, which the comment credits as the real double-review guard.catchUpAttempted(sync.ts:96,160,171): a picked PR is marked synchronously before the await and excluded from later ticks regardless of outcome, so such a PR is attempted at most once per head per session (worst case one harmless no-oppr-opendispatch, sinceeffectiveActionsintypes.ts:127-128only yields enabled modes). Newer PRs are not starved.The claim-reclaim state machine (
automation_claims.rs) is also sound: reclaim fires only onmtime ≥ 30 min, a failed delete or lost retry race both resolve toOk(false), and a future mtime (duration_since→Err) is treated as not-stale. All eightopenInTerminalcall sites pass the newterminalCommand, and there is no Rust-side settings struct that would drop the newterminalCommandfield on round-trip (the frontend store is the sole writer).Nothing else worth flagging.
Posted by GitDesktop — AI output, verify before acting on it.
Ticket changed by: theBGuy