Skip to content

Commit d3760d2

Browse files
author
Laurent Guitton
committed
feat(desktop): release v2.16.0 — PR activity notifications
1 parent 531fb8c commit d3760d2

41 files changed

Lines changed: 836 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.16.0] - 2026-05-29
11+
12+
v2.16 adds native OS notifications for pull-request activity — review requests, new comments, CI pass/fail flips, and merge/close — surfaced while GitWand is in the background, with zero extra network requests beyond the existing Launchpad poll.
13+
14+
### Added
15+
16+
- **Background Launchpad poller** (`useLaunchpadPoller`) — a dedicated ~60 s poll loop, independent of `useRepoPoller` (which pauses when the window is hidden). It keeps refreshing PR state in the background so notifications fire without GitWand in the foreground. Gated on workspace presence, notification settings, and connectivity.
17+
- **Snapshot diff layer** (`useLaunchpadNotifications`) — a module-level singleton that compares successive PR snapshots and emits typed events: `new-pr`, `ci-flip`, `review-requested`, `review-decided`, `new-comment`, `closed` (merged/closed). Zero additional network requests — it consumes the data the poller already fetched. The first pass only seeds the snapshot, so reopening the app never fires a burst of stale notifications.
18+
- **Native OS notifications** via `tauri-plugin-notification` (macOS Notification Center, Linux libnotify, Windows toast). Emitted only when the window is in the background (`document.hidden`); in the foreground the Launchpad updates visually instead. Every event is also pushed to `useLogs` (traceable in the Logs tab).
19+
- **Notification settings** (Settings → Notifications) — granularity selector: All activity · Reviews & comments · CI failures only · None; plus a "by people" toggle that suppresses events authored by bots (GitHub Actions, Dependabot, Renovate).
20+
21+
### Changed
22+
23+
- **`workspace_prs_all` re-enriched** — the Launchpad PR list again carries `statusCheckRollup`, `reviewDecision`, `reviewRequests` and a new `comment_count`, which the notification diff needs. The light sidebar list (`gh_list_prs`) stays unenriched for boot performance; only the background Launchpad path pays the heavier `gh pr list --json` cost.
24+
25+
### Technical
26+
27+
- New `PullRequest.comment_count` field threaded across the three layers (`types.rs`/`gh.rs`/`gitlab.rs`/`bitbucket.rs`, `dev-server.mjs`, `backend.ts` + `backend-pr.ts`).
28+
- `tauri-plugin-notification` added to Cargo + initialised in `lib.rs`; `notification:*` permissions declared in `capabilities/default.json`. The TS wrapper (`useOsNotification`) calls the plugin's IPC commands directly so the type-check and `dev:web` builds don't depend on the native JS package.
29+
- Unit tests for the diff layer (every transition → correct event, terminal-only CI flips, bot detection, no boot burst).
30+
- i18n: `settings.notification*` and `notify.*` keys across all five locales.
31+
1032
## [2.15.1] - 2026-05-29
1133

1234
v2.15.1 closes the "Git Tree polish & quick actions" lot — the daily-friction follow-ups to PR #23 (Force push, Quick Stash, Submodules in the Git Tree).

PLAN-v2.16-pr-notifications.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PLAN v2.16.0 — PR Activity Notifications
2+
3+
> Notifications OS natives pour les évènements de PR (review request, nouveau commentaire, flip CI, merge/close) sans quitter GitWand.
4+
5+
**Rappels transverses** (identiques v2.15.1) : tout ajout backend = 3 couches (Rust `ops.rs`/`gh.rs` + `dev-server.mjs` + wrapper `backend.ts`) ; i18n × 5 locales ; versioning via `./scripts/bump-version.sh` ; `packages/core` zéro-Node ; tests de parité pour toute nouvelle commande.
6+
7+
---
8+
9+
## Ce que l'exploration a confirmé (et corrigé par rapport à la ROADMAP)
10+
11+
La ROADMAP annonçait l'infra « presque prête (`useRepoPoller`, `useLaunchpadPrs`, `useConnectivity`) ; seul manque le diff-snapshot + l'émission OS ». En réalité **deux écarts** changent l'ampleur :
12+
13+
1. **Pas de polling Launchpad de fond.** `useRepoPoller` ne suit que le repo actif et **se met en pause quand la fenêtre est cachée** (`document.hidden`). Or on veut notifier *quand l'app est en arrière-plan*. Le Launchpad (`LaunchpadView.vue`) ne rafraîchit qu'au montage et au clic. → Il faut un **poller dédié Launchpad** qui continue de tourner en arrière-plan.
14+
15+
2. **Les données de PR « légères » ne portent ni CI ni reviews ni commentaires.** Le chemin liste (`workspace_prs_all``gh.rs`) renvoie `checks_rollup` et `review_decision` **vides** (allégés en v2.8.5 pour la perf de boot du sidebar), et `PullRequest` n'expose pas de `commentCount`. → Pour notifier sur flip CI / review request / nouveau commentaire, il faut **enrichir `workspace_prs_all`** (`gh pr list --json …,statusCheckRollup,reviewDecision,reviewRequests,comments`). Sinon on ne peut détecter que : nouvelle PR, changement d'état (merged/closed), bump `updatedAt`.
16+
17+
Acquis utilisables tels quels : `useConnectivity` (`isOnline` pour ne pas notifier hors-ligne), `useLogs` (singleton `pushLog(level,msg,ctx)` → onglet Logs), `useSettings` (champ `notifications: boolean` existant), capabilities dans `src-tauri/capabilities/default.json`.
18+
19+
---
20+
21+
## Chantier 1 — Poller Launchpad de fond (front)
22+
23+
- `useLaunchpadPoller.ts` : `setInterval` dédié (~60 s), indépendant de `useRepoPoller`. **Ne se met PAS en pause** quand la fenêtre est cachée (c'est justement là qu'on veut notifier), mais respecte `isOnline` (skip si hors-ligne) et l'activation des notifications.
24+
- À chaque tick : `useLaunchpadPrs.refresh(repos)` → met à jour le snapshot courant (et rafraîchit le Launchpad au passage si ouvert).
25+
- Discipline polling (mémoire `feedback_gitwand_polling_discipline`) : un seul interval, gaté sur `workspace non vide` + `notifications activées` ; cleanup en `onUnmounted`.
26+
27+
## Chantier 2 — Couche diff-snapshot (front)
28+
29+
- `useLaunchpadNotifications.ts` : singleton niveau module. Garde le snapshot précédent (Map `url → {state, updatedAt, checksRollup, reviewDecision, reviewRequested, commentCount}`).
30+
- À chaque refresh, compare ancien/nouveau et produit des évènements typés :
31+
- `ci-flip` (SUCCESS↔FAILURE), `review-requested` (mon login ajouté à `reviewRequested`), `new-comment` (`commentCount` ↑), `review-decided` (APPROVED / CHANGES_REQUESTED), `merged` / `closed`, `new-pr`.
32+
- Zéro requête réseau supplémentaire (réutilise les données du poller).
33+
- Filtre « by people » : ignore les évènements dont l'auteur est un bot (`github-actions`, `dependabot`, `renovate`, suffixe `[bot]`).
34+
- Première passe après boot = établissement du snapshot **sans** notifier (pas de rafale au démarrage).
35+
36+
## Chantier 3 — Notifications OS natives (Rust + capabilities)
37+
38+
- Ajouter `tauri-plugin-notification = "2"` (Cargo.toml) + init du plugin (`lib.rs`), permission `notification:default` dans `capabilities/default.json`.
39+
- Wrapper TS `notify(title, body, prUrl)` (3 couches : Tauri `plugin-notification` ; dev-server = no-op/log ; fallback Web Notification en `dev:web`).
40+
- Action au clic → ouvrir le Launchpad sur la PR concernée (réutilise la navigation Launchpad + deep-link par `url`).
41+
- **Gating** : émettre seulement quand `document.hidden` (app en arrière-plan) ; en avant-plan, le Launchpad se met à jour visuellement, pas de notif OS.
42+
- Chaque notif est aussi poussée dans `useLogs` (traçable, onglet Logs).
43+
44+
## Chantier 4 — Réglages de granularité (front + i18n)
45+
46+
- `useSettings` : ajouter `notificationLevel: "all" | "reviews" | "ci" | "none"` (le `notifications: boolean` existant reste le maître on/off, ou est migré). Ajouter `notificationsByPeople: boolean`.
47+
- ⚠️ Mémoire `feedback_gitwand_duplicate_settings_interfaces` : tout nouveau champ doit être ajouté **à la fois** dans `useSettings.ts` (AppSettings) **et** `SettingsPanel.vue` (Settings).
48+
- Section « Notifications » dans `SettingsPanel.vue` : radio All / Reviews & comments / CI failures only / None + toggle « by people ».
49+
- i18n × 5 : `settings.notifications.*`, libellés des évènements pour les titres/corps de notif.
50+
51+
## Chantier 5 — Backend enrichi (conditionnel — voir question de cadrage)
52+
53+
- Si on veut CI/review/comment : enrichir `workspace_prs_all` pour peupler `checks_rollup`, `review_decision`, `review_requested`, et un `comment_count` (nouveau champ `PullRequest` + 3 couches + GitLab/Bitbucket en best-effort).
54+
- Coût : `gh pr list --json` plus lourd, mais sur le poll Launchpad de fond (60 s), acceptable.
55+
- Tests de parité pour le champ enrichi.
56+
57+
## Chantier 6 — Finition release
58+
59+
- Tests : unitaires sur la couche diff (`useLaunchpadNotifications` : chaque transition → bon évènement ; bots filtrés ; pas de rafale au boot) ; parité si backend enrichi.
60+
- Docs : ROADMAP v2.16 → Shipped + CHANGELOG + website/changelog.
61+
- `./scripts/bump-version.sh 2.16.0`.
62+
63+
---
64+
65+
## Ordre conseillé
66+
67+
Backend enrichi (5, si retenu) → Poller (1) → Diff (2) → Notif OS (3) → Settings (4) → Finition (6).

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<p align="center">
2222
<img alt="License" src="https://img.shields.io/badge/license-MIT-8B5CF6">
2323
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-100%25-3178C6">
24-
<img alt="Version" src="https://img.shields.io/badge/version-2.15.1-22c55e">
24+
<img alt="Version" src="https://img.shields.io/badge/version-2.16.0-22c55e">
2525
</p>
2626

2727
---

ROADMAP.md

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,6 @@
66

77
## What's Next
88

9-
### v2.16.0 — PR Activity Notifications
10-
11-
Native OS notifications for PR events — review request, new comment, CI flip, mention — without leaving GitWand. The infrastructure is nearly ready (`useRepoPoller`, `useLaunchpadPrs`, `useConnectivity`); only the diff-snapshot layer and OS emission are missing.
12-
13-
**Launchpad diff-snapshot**
14-
15-
- Between two poller ticks, compare the previous snapshot with the new one: new comments, CI status changes (pass/fail flip), incoming review requests, PR merge/close
16-
- `useLaunchpadNotifications.ts`: module-level singleton, compares by `updatedAt` / `commentCount` / `ciStatus` — zero additional network requests
17-
18-
**Native notifications via `tauri-plugin-notification`**
19-
20-
- OS notification (macOS Notification Center, Linux libnotify, Windows toast) with title, body, and action (open the Launchpad on the relevant PR)
21-
- Configurable granularity in Settings > Notifications: All activity · Reviews & comments · CI failures only · None
22-
- "By people" mode: filter events identified as bots (GitHub Actions, Dependabot, Renovate)
23-
- Gating: notifications only when the GitWand window is in the background (`visibilitychange`)
24-
25-
**Implementation**
26-
27-
- `tauri-plugin-notification` already in the Tauri 2 ecosystem
28-
- macOS TCC permissions declared in `tauri.conf.json` (`allow-notification` capability)
29-
- Notifications are pushed to `useLogs` — traceable in the Logs tab
30-
31-
---
32-
339
### v2.17.0 — Inline CI Check Annotations
3410

3511
Overlay check-run annotations in the diff — the exact line that failed the linter or typecheck, right where you need it in the review.
@@ -166,6 +142,7 @@ Positioning: neither "yet another Git GUI" nor an IDE. A first-class Git navigat
166142
167143
| Version | Highlights |
168144
|---------|-----------|
145+
| **v2.16.0** | PR Activity Notifications — background Launchpad poller, zero-network snapshot diff (`useLaunchpadNotifications`) for CI flips / review requests / new comments / merge-close, native OS notifications via `tauri-plugin-notification` (background-only), Settings granularity (All · Reviews & comments · CI failures only · None) + "by people" bot filter, enriched `workspace_prs_all` (CI/review/comment fields) |
169146
| **v2.15.1** | Git Tree polish & quick actions — Force push (branch context menu + protected-trunk/diverged-remote guard), Quick Stash `⌘⇧,` (instant, AI label) + pending badge in the commit area, Submodules in the Git Tree (branch-picker section, per-commit pointed-SHA badge, click-to-navigate) |
170147
| **v2.15.0** | Git Tree multi-branch — Git Tree as primary view, Log panel removed, unified context menus, stash/branch/tag management from the graph, DAG trunk-pinning, WIP node, search bar |
171148
| **v2.14.0** | Forge completeness — GitLab `updateComment`/`deleteComment`/CI checks, complete Bitbucket stubs, forge-agnostic `getConflictPreview`/`getHotspots`, multi-account provider |

apps/desktop/dev-server.mjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3669,13 +3669,13 @@ async function handleRequest(req, res) {
36693669
const { repos } = await readBody(req);
36703670
const results = repos.map(repo => {
36713671
try {
3672-
// v2.8.5 boot-perf: dropped heavy fields (statusCheckRollup,
3673-
// mergeStateStatus, reviewRequests, reviewDecision, additions,
3674-
// deletions) — each triggers a per-PR roundtrip in `gh` internals
3675-
// and HTTP 502s on busy repos like dendreo at high --limit.
3676-
// Mirror of the Rust change in `commands/workspace.rs`.
3672+
// v2.16: re-enriched with CI / review / comment fields so the
3673+
// Launchpad notification diff (useLaunchpadNotifications) can detect
3674+
// CI flips, review requests and new comments. Mirror of the Rust
3675+
// change in `commands/workspace.rs`. This runs on the background
3676+
// Launchpad poller (~60 s), so the extra cost is acceptable.
36773677
const raw = execSync(
3678-
"gh pr list --state open --json number,title,state,author,headRefName,baseRefName,isDraft,createdAt,updatedAt,url,labels,assignees --limit 10",
3678+
"gh pr list --state open --json number,title,state,author,headRefName,baseRefName,isDraft,createdAt,updatedAt,url,labels,assignees,reviewRequests,reviewDecision,mergeStateStatus,statusCheckRollup,comments --limit 10",
36793679
{ cwd: repo.path, encoding: "utf-8" }
36803680
);
36813681
const ghPrs = JSON.parse(raw || "[]");
@@ -3702,6 +3702,7 @@ async function handleRequest(req, res) {
37023702
checks_rollup: (pr.statusCheckRollup ?? [])
37033703
.map(c => c.conclusion)
37043704
.find(c => !!c) ?? "",
3705+
comment_count: (pr.comments ?? []).length,
37053706
}));
37063707
return { repo_path: repo.path, repo_name: repo.name, prs, error: null };
37073708
} catch (e) {

apps/desktop/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gitwand/desktop",
3-
"version": "2.15.1",
3+
"version": "2.16.0",
44
"private": true,
55
"description": "GitWand Desktop — lightweight Git client with smart conflict resolution (Tauri + Vue 3)",
66
"type": "module",
@@ -25,6 +25,7 @@
2525
"@gitwand/core": "workspace:*",
2626
"@tanstack/vue-virtual": "^3.13.24",
2727
"@tauri-apps/api": "^2.0.0",
28+
"@tauri-apps/plugin-notification": "^2.0.0",
2829
"@tauri-apps/plugin-process": "^2.0.0",
2930
"@tauri-apps/plugin-updater": "^2.0.0",
3031
"dompurify": "^3.2.0",

apps/desktop/src-tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "gitwand-desktop"
3-
version = "2.15.1"
3+
version = "2.16.0"
44
description = "GitWand Desktop — standalone merge conflict resolution"
55
authors = ["Laurent Guitton <lb.guitton@gmail.com>"]
66
license = "MIT"
@@ -38,6 +38,7 @@ tauri-build = { version = "2", features = [] }
3838
# app is built in debug mode unless we explicitly enable it in release too.
3939
tauri = { version = "2", features = ["devtools"] }
4040
tauri-plugin-dialog = "2"
41+
tauri-plugin-notification = "2"
4142
tauri-plugin-shell = "2"
4243
tauri-plugin-global-shortcut = "2"
4344
tauri-plugin-updater = "2"

apps/desktop/src-tauri/capabilities/default.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
"global-shortcut:allow-is-registered",
1414
"updater:default",
1515
"process:default",
16-
"process:allow-restart"
16+
"process:allow-restart",
17+
"notification:default",
18+
"notification:allow-notify",
19+
"notification:allow-request-permission",
20+
"notification:allow-is-permission-granted"
1721
]
1822
}

apps/desktop/src-tauri/src/commands/bitbucket.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ fn bb_pr_to_pr(pr: &serde_json::Value) -> PullRequest {
331331
review_decision: String::new(),
332332
merge_state_status: String::new(),
333333
checks_rollup: String::new(),
334+
comment_count: ji(pr, "comment_count"),
334335
}
335336
}
336337

apps/desktop/src-tauri/src/commands/gh.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ pub(crate) fn gh_create_pr(
285285
review_decision: String::new(),
286286
merge_state_status: String::new(),
287287
checks_rollup: String::new(),
288+
comment_count: 0,
288289
})
289290
}
290291

0 commit comments

Comments
 (0)