Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
27a4a0a
Add GitHub auth process
Jun 5, 2026
c5da6d1
feat(pr): ajouter le support des pull requests cross-fork
Jun 5, 2026
3b87cab
feat: Add PR AI language setting and standardize external link handling
Jun 5, 2026
5932fd9
feat(pr): surface upstream PRs and actions for forked repos
Jun 5, 2026
3698d41
refactor(pr): Consolidate fork information handling
Jun 5, 2026
81e6a32
feat(pr): Guide users to GitHub account settings from gh CLI error
Jun 5, 2026
c77b7e1
feat(accounts): Default account form to GitHub and hide manual username
Jun 5, 2026
50f03aa
feat: Add comprehensive Azure DevOps forge support
Jun 5, 2026
fe8d7f4
feat(pr): Generalize 'Open in Browser' labels and icons
Jun 5, 2026
9e75953
feat(pr): Open external markdown links in OS browser
Jun 5, 2026
c5a3250
feat(azure): Implement automatic access token refresh
Jun 5, 2026
7e4f971
feat(azure): Implement PR comments and local file stats
Jun 5, 2026
62a2841
feat(azure): Implement CI checks and PR reviews
Jun 5, 2026
ea3e94d
feat(azure): Improve PR merge readiness with explicit policy and revi…
Jun 5, 2026
a62c667
feat(azure): Display PR waiting status and file change statistics
Jun 5, 2026
714d78a
feat(pr): Refine PR data, optimize loading, and enhance merge readiness
Jun 5, 2026
ac4f42c
feat(pr): Integrate GitHub PR CI status into waiting dot
Jun 5, 2026
37ba25f
feat(pr): Display all GitHub PR comments
Jun 5, 2026
be76d67
feat(pr): Add "Go to" button for comments and filter GitLab system notes
Jun 5, 2026
c43e592
feat(pr): Standardize avatar disk style across all PR views
Jun 5, 2026
12d0f51
refactor(avatar): centraliser le style des avatars dans useAvatar
Jun 5, 2026
fcb021e
This change introduces a stale-while-revalidate (SWR) cache for Pull …
Jun 5, 2026
e24f473
perf(tauri): Offload blocking commands to background threads
Jun 5, 2026
e99047d
perf(tauri): Convert commands to async functions
Jun 5, 2026
394abca
feat(pr): Replace refresh button with spinner during PR list loading
Jun 5, 2026
f8b39e0
feat(pr): Align PR listing and count with base repository for forks
Jun 5, 2026
8a8ff46
fix(pr): Correct comment counts in PR details and panel
Jun 5, 2026
4b9b0da
fix(pr): Correct PR tab URLs for Azure DevOps
Jun 5, 2026
7b0b4c6
feat(pr): Relocate and restyle external PR links for improved access
Jun 5, 2026
8eb604e
feat(pr): Add full Markdown support to comments and descriptions
Jun 5, 2026
2ca841c
fix(pr): Correct quoting for CSS custom property in comment thread
Jun 5, 2026
fa7ee51
feat(pr): Display aggregated CI status for PRs across all forges
Jun 5, 2026
b4cffb7
fix(pr): Suppress "no approval" waiting reason for merge-ready PRs
Jun 5, 2026
921a717
feat(pr): Deduplicate Azure DevOps CI checks by best status
Jun 5, 2026
7d679a1
feat(pr): Improve Azure build check accuracy and surface merge conflicts
Jun 5, 2026
6dd18c2
feat(pr): Enhance Azure check accuracy for builds and expired policies
Jun 5, 2026
c8eb640
feat(pr): Evaluate CI status for draft pull requests
Jun 5, 2026
447d7b2
feat(pr): Improve merge status display by considering CI state
Jun 5, 2026
87dfc15
feat(pr, auth): Refactor OAuth device flow and optimize PR UI
Jun 5, 2026
4815219
feat(pr): Display and enforce user merge permissions
Jun 5, 2026
a6061a1
fix(pr): Correct GitHub merge permission logic for forks
Jun 5, 2026
0ab86de
feat(pr): Enhance 'Open in Browser' button prominence
Jun 5, 2026
6afdc08
feat(pr): Implement visibility-gated background revalidation
Jun 5, 2026
3b2b806
feat(pr): Refine PR list sidebar header layout
Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ research/
.release-notes-v1.5.0.md
website/.vitepress/.temp
.claude/worktrees/

.gitwandrc
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Cross-fork pull requests** β€” when the repo's `origin` is a fork, the PR create view shows a target-repository selector (upstream parent vs your fork), defaulting to upstream. Works on both the REST (token) path β€” head is qualified as `fork-owner:branch` β€” and the `gh` path (`--repo`). New `gh_fork_info` command detects the relationship.
- **Fork PRs in the list** β€” on the REST (token) path, the PR list for a fork now also includes the PRs you opened on the upstream repo (head repo == your fork), merged and sorted with origin's own PRs. PR detail/diff/checks/merge transparently resolve to the upstream repo for those entries.
- **Sign in with GitHub (no `gh` CLI required)** β€” Settings β†’ Accounts now offers a "Sign in with GitHub" button using the OAuth device flow. The resulting token is stored in the OS keychain; once present, the GitHub PR workflow (list, count, detail, diff, checks, files, create, merge, checkout, mark-ready) routes through the GitHub REST API instead of shelling out to `gh`. The `gh` CLI still works as before when no Settings token is configured β€” the ambient `GH_TOKEN`/`GITHUB_TOKEN` env path is unchanged.
- **Azure DevOps support (new forge)** — Settings → Accounts now offers "Sign in with Azure" using the Entra ID OAuth device flow (same UX as GitHub), with automatic access-token refresh via the stored refresh token. The tokens are stored in the OS keychain (`gitwand:azure/oauth` + `oauth-refresh`). Azure DevOps remotes (`dev.azure.com`, `*.visualstudio.com`) are auto-detected and routed to a new `AzureProvider` backed by the Azure DevOps REST API (api-version 7.1): PR list/count/detail/diff/files/create/merge/checkout, draft→ready, comments (threads), CI checks (branch-policy evaluations) and reviewer-vote reviews — so the merge-readiness chip reflects real build + approval state. Diff, file lists and change stats are produced from local git (Azure has no unified-patch endpoint). Comment edit/delete, line-anchored comment creation, reviewer pickers and submitting reviews are not wired yet and degrade gracefully.

### Notes

- Ships with GitWand's registered GitHub OAuth App `client_id` (`Ov23licwiCpPiRPRodWN`, public β€” device flow enabled) baked into `github_api.rs`. Override at runtime or build time via `GITWAND_GH_CLIENT_ID` if needed.
- Azure DevOps sign-in ships with a **temporary** default Entra ID client id β€” the well-known Azure CLI public client (`04b07795-8ddb-461a-bbee-02f9e1bf7b46`, device flow enabled) β€” so it works without registering an app. Stop-gap only: the consent screen reads "Microsoft Azure CLI" and Microsoft may restrict reuse. Override with a dedicated GitWand Entra app via `GITWAND_AZURE_CLIENT_ID` (runtime or build time) before shipping.

## [2.17.0] - 2026-06-04

v2.17 rounds out the agent-CLI lineup with **opencode** as a first-class AI provider, and gives every CLI agent its own model picker β€” a second select under the provider dropdown, scoped per provider so switching back restores the previous choice.
Expand Down
74 changes: 74 additions & 0 deletions apps/desktop/dev-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ function gitSpawn(args, cwd) {
});
}

// ── Mock state: GitHub OAuth device flow (dev:web only) ──────────────────────
// The real flow lives in Rust (`github_api.rs`). In the browser mock we fake it
// so the Settings > Accounts UI can be exercised without Tauri. It does NOT log
// you in β€” it just returns a static code then "succeeds" after a couple polls.
let _mockGithubPolls = 0;

/**
* Get a GitHub OAuth token β€” tries in order:
* 1. GH_TOKEN / GITHUB_TOKEN env vars
Expand Down Expand Up @@ -2179,6 +2185,39 @@ async function handleRequest(req, res) {

// ─── GitHub REST API endpoints (no gh binary needed) ──────

// POST /api/github-device-start β€” MOCK device flow (dev:web only).
// Returns a static user code + verification URL. The frontend opens the URL
// and starts polling; see /api/github-device-poll below.
if (url.pathname === "/api/github-device-start" && req.method === "POST") {
_mockGithubPolls = 0;
// NOTE: verification_uri intentionally does NOT point at the real
// github.com/login/device β€” this is a fake flow and a real code is never
// issued. Sending the user to GitHub with a bogus code only confuses.
return jsonResponse(req, res, {
device_code: "mock-device-code",
user_code: "DEV-MOCK",
verification_uri: "about:blank#gitwand-dev-mock",
verification_uri_complete: "",
expires_in: 900,
interval: 1,
});
}

// POST /api/github-device-poll β€” MOCK: "pending" twice, then "success".
// No real token is stored; this only unblocks UI iteration in dev:web.
if (url.pathname === "/api/github-device-poll" && req.method === "POST") {
_mockGithubPolls += 1;
if (_mockGithubPolls < 3) {
return jsonResponse(req, res, { status: "pending", login: "", error: "" });
}
return jsonResponse(req, res, { status: "success", login: "dev-user", error: "" });
}

// POST /api/github-token-present β€” MOCK: never "logged in" in dev:web.
if (url.pathname === "/api/github-token-present" && req.method === "POST") {
return jsonResponse(req, res, false);
}

// GET /api/gh-current-user β€” returns the authenticated GitHub login
if (url.pathname === "/api/gh-current-user" && req.method === "GET") {
try {
Expand Down Expand Up @@ -2572,6 +2611,41 @@ async function handleRequest(req, res) {
}
}

// GET /api/gh-pr-issue-comments?cwd=<path>&number=<n>
// Issue-level (conversation) comments β€” not anchored to a diff line.
if (url.pathname === "/api/gh-pr-issue-comments" && req.method === "GET") {
const cwd = url.searchParams.get("cwd");
const number = url.searchParams.get("number");
if (!cwd || !number) return jsonResponse(req, res, { error: "Missing cwd or number" }, 400);
try {
const token = getGithubToken();
if (!token) return jsonResponse(req, res, { error: "No GitHub token" }, 401);
const nwo = getRepoNwo(resolve(cwd));
if (!nwo) return jsonResponse(req, res, { error: "Could not determine GitHub repo" }, 400);
const resp = await githubFetch(`/repos/${nwo}/issues/${number}/comments?per_page=100`, token);
if (!resp.ok) return jsonResponse(req, res, { error: `GitHub API ${resp.status}` }, 500);
const raw = await resp.json();
return jsonResponse(req, res, raw.map((c) => ({
id: c.id,
body: c.body,
author: c.user?.login ?? "",
created_at: c.created_at,
updated_at: c.updated_at,
path: "",
line: null,
original_line: null,
side: "RIGHT",
start_line: null,
start_side: null,
in_reply_to_id: null,
diff_hunk: "",
url: c.html_url ?? "",
})));
} catch (err) {
return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500);
}
}

// POST /api/gh-pr-comment β€” create or reply
// Body: { cwd, number, body, path, line, side, start_line?, start_side?, in_reply_to_id? }
if (url.pathname === "/api/gh-pr-comment" && req.method === "POST") {
Expand Down
16 changes: 8 additions & 8 deletions apps/desktop/src-tauri/src/commands/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ fn resolve_codex_binary() -> Option<String> {
/// stderr surfaces a clear "please log in" message that the calling
/// command (`claude_cli_prompt`) propagates as an error.
#[tauri::command]
pub(crate) fn detect_claude_cli() -> Result<ClaudeCliInfo, String> {
pub(crate) async fn detect_claude_cli() -> Result<ClaudeCliInfo, String> {
let binary = match resolve_claude_binary() {
Some(b) => b,
None => {
Expand Down Expand Up @@ -165,7 +165,7 @@ pub(crate) fn detect_claude_cli() -> Result<ClaudeCliInfo, String> {
/// The CLI already handles auth via the user's subscription β€” we just pipe
/// text in and get text back.
#[tauri::command]
pub(crate) fn claude_cli_prompt(
pub(crate) async fn claude_cli_prompt(
prompt: String,
system_prompt: Option<String>,
cwd: Option<String>,
Expand Down Expand Up @@ -244,7 +244,7 @@ pub(crate) fn claude_cli_prompt(
/// for a prompt they never asked for. Auth verifies implicitly on the
/// first real prompt via `codex_cli_prompt`.
#[tauri::command]
pub(crate) fn detect_codex_cli() -> Result<CodexCliInfo, String> {
pub(crate) async fn detect_codex_cli() -> Result<CodexCliInfo, String> {
let binary = match resolve_codex_binary() {
Some(b) => b,
None => {
Expand Down Expand Up @@ -277,7 +277,7 @@ pub(crate) fn detect_codex_cli() -> Result<CodexCliInfo, String> {
}

#[tauri::command]
pub(crate) fn codex_cli_prompt(
pub(crate) async fn codex_cli_prompt(
prompt: String,
system_prompt: Option<String>,
cwd: Option<String>,
Expand Down Expand Up @@ -381,7 +381,7 @@ fn resolve_opencode_binary() -> Option<String> {
/// Claude / Codex detectors: no prompt is sent to verify auth β€” that is
/// confirmed implicitly on the first real `opencode_cli_prompt`.
#[tauri::command]
pub(crate) fn detect_opencode_cli() -> Result<OpencodeCliInfo, String> {
pub(crate) async fn detect_opencode_cli() -> Result<OpencodeCliInfo, String> {
let binary = match resolve_opencode_binary() {
Some(b) => b,
None => {
Expand Down Expand Up @@ -414,7 +414,7 @@ pub(crate) fn detect_opencode_cli() -> Result<OpencodeCliInfo, String> {
}

#[tauri::command]
pub(crate) fn opencode_cli_prompt(
pub(crate) async fn opencode_cli_prompt(
prompt: String,
system_prompt: Option<String>,
cwd: Option<String>,
Expand Down Expand Up @@ -471,7 +471,7 @@ pub(crate) fn opencode_cli_prompt(
/// when the binary is missing or the command fails, so the UI can fall back
/// to free-text entry gracefully.
#[tauri::command]
pub(crate) fn opencode_list_models() -> Result<Vec<String>, String> {
pub(crate) async fn opencode_list_models() -> Result<Vec<String>, String> {
let binary = match resolve_opencode_binary() {
Some(b) => b,
None => return Ok(Vec::new()),
Expand Down Expand Up @@ -501,7 +501,7 @@ pub(crate) fn opencode_list_models() -> Result<Vec<String>, String> {
/// embed a PTY because this is a one-shot setup flow: the user validates
/// in their browser and comes back to GitWand.
#[tauri::command]
pub(crate) fn claude_cli_login() -> Result<(), String> {
pub(crate) async fn claude_cli_login() -> Result<(), String> {
let binary = resolve_claude_binary()
.ok_or_else(|| "Binaire `claude` introuvable. Installez-le d'abord.".to_string())?;

Expand Down
Loading
Loading