a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add sonner toast notifications for jj operations

- Add sonner package and Toaster component with macOS-native styling
- Show success toasts for edit, new, and abandon operations
- Style toasts with translucent background, backdrop blur, themed colors

+58 -7
+1
apps/desktop/package.json
··· 42 42 "react-resizable-panels": "^4.0.10", 43 43 "scheduler": "^0.27.0", 44 44 "shadcn": "^3.6.2", 45 + "sonner": "^2.0.7", 45 46 "tailwind-merge": "^3.4.0", 46 47 "tw-animate-css": "^1.4.0" 47 48 },
+33
apps/desktop/src/components/ui/sonner.tsx
··· 1 + import { Toaster as Sonner, type ToasterProps, toast } from "sonner"; 2 + 3 + function Toaster({ ...props }: ToasterProps) { 4 + return ( 5 + <Sonner 6 + className="toaster group font-sans" 7 + position="bottom-right" 8 + gap={8} 9 + toastOptions={{ 10 + classNames: { 11 + toast: 12 + "group toast group-[.toaster]:bg-popover/80 group-[.toaster]:backdrop-blur-xl group-[.toaster]:text-popover-foreground group-[.toaster]:border group-[.toaster]:border-border/50 group-[.toaster]:shadow-lg group-[.toaster]:rounded-[10px] group-[.toaster]:text-sm group-[.toaster]:px-3 group-[.toaster]:py-2.5", 13 + title: "group-[.toast]:font-medium group-[.toast]:text-[13px]", 14 + description: "group-[.toast]:text-muted-foreground group-[.toast]:text-[12px]", 15 + actionButton: 16 + "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground group-[.toast]:text-xs group-[.toast]:font-medium group-[.toast]:rounded-md", 17 + cancelButton: 18 + "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground group-[.toast]:text-xs group-[.toast]:rounded-md", 19 + success: 20 + "group-[.toaster]:border-primary/30 [&_[data-icon]]:text-primary", 21 + error: 22 + "group-[.toaster]:border-destructive/40 [&_[data-icon]]:text-destructive", 23 + warning: 24 + "group-[.toaster]:border-chart-3/40 [&_[data-icon]]:text-chart-3", 25 + info: "group-[.toaster]:border-primary/30 [&_[data-icon]]:text-primary", 26 + }, 27 + }} 28 + {...props} 29 + /> 30 + ); 31 + } 32 + 33 + export { Toaster, toast };
+14 -4
apps/desktop/src/db.ts
··· 3 3 import { queryCollectionOptions } from "@tanstack/query-db-collection"; 4 4 import { listen } from "@tauri-apps/api/event"; 5 5 import { Effect } from "effect"; 6 + import { toast } from "@/components/ui/sonner"; 6 7 import type { ChangedFile, Repository, Revision } from "@/tauri-commands"; 7 8 import { 8 9 generateChangeIds, ··· 284 285 .then(() => { 285 286 // Invalidate to get fresh data from backend 286 287 queryClient.invalidateQueries({ queryKey: ["revisions", repoPath] }); 288 + toast.success(`Working copy is now ${targetRevision.change_id_short}`); 287 289 }) 288 - .catch(() => { 290 + .catch((error) => { 289 291 // Revert optimistic update 290 292 const revertUpdates: Revision[] = []; 291 293 if ( ··· 296 298 } 297 299 revertUpdates.push({ ...targetRevision, is_working_copy: false }); 298 300 collection.utils.writeUpsert(revertUpdates); 301 + toast.error(`Failed to edit revision: ${error}`, { duration: Number.POSITIVE_INFINITY }); 299 302 }); 300 303 } 301 304 ··· 346 349 }).pipe(Effect.tapError((error) => Effect.logError("jjNew failed", error))); 347 350 348 351 trackMutation(mutationId, Effect.runPromise(program)) 349 - .then(() => { 352 + .then((newChangeId) => { 350 353 // Invalidate to get authoritative data (correct commit_id, short_id, etc.) 351 354 queryClient.invalidateQueries({ queryKey: ["revisions", repoPath] }); 355 + const shortId = newChangeId.slice(0, 8); 356 + toast.success(`Working copy is now ${shortId}`, { 357 + description: "Created new revision", 358 + }); 352 359 }) 353 - .catch(() => { 360 + .catch((error) => { 354 361 // Revert optimistic update 355 362 if (optimisticRevision) { 356 363 collection.utils.writeDelete(getRevisionKey(optimisticRevision)); ··· 358 365 collection.utils.writeUpsert([{ ...currentWcRevision, is_working_copy: true }]); 359 366 } 360 367 } 368 + toast.error(`Failed to create revision: ${error}`, { duration: Number.POSITIVE_INFINITY }); 361 369 }); 362 370 } 363 371 ··· 379 387 .then(() => { 380 388 // Invalidate to get fresh data (especially for WC abandon which creates new WC) 381 389 queryClient.invalidateQueries({ queryKey: ["revisions", repoPath] }); 390 + toast.success(`Abandoned revision ${revision.change_id_short}`); 382 391 }) 383 - .catch(() => { 392 + .catch((error) => { 384 393 // Re-add on failure (only if we deleted it) 385 394 if (!revision.is_working_copy) { 386 395 collection.utils.writeUpsert([revision]); 387 396 } 397 + toast.error(`Failed to abandon revision: ${error}`, { duration: Number.POSITIVE_INFINITY }); 388 398 }); 389 399 } 390 400
+7 -1
apps/desktop/src/routes/__root.tsx
··· 1 1 import { createRootRoute, Outlet } from "@tanstack/react-router"; 2 + import { Toaster } from "../components/ui/sonner"; 2 3 3 4 export const Route = createRootRoute({ 4 5 component: RootComponent, 5 6 }); 6 7 7 8 function RootComponent() { 8 - return <Outlet />; 9 + return ( 10 + <> 11 + <Outlet /> 12 + <Toaster /> 13 + </> 14 + ); 9 15 }
+3 -2
bun.lock
··· 46 46 "react-resizable-panels": "^4.0.10", 47 47 "scheduler": "^0.27.0", 48 48 "shadcn": "^3.6.2", 49 + "sonner": "^2.0.7", 49 50 "tailwind-merge": "^3.4.0", 50 51 "tw-animate-css": "^1.4.0", 51 52 }, ··· 1191 1192 1192 1193 "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], 1193 1194 1195 + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], 1196 + 1194 1197 "sorted-btree": ["sorted-btree@1.8.1", "", {}, "sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ=="], 1195 1198 1196 1199 "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], ··· 1390 1393 "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 1391 1394 1392 1395 "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1393 - 1394 - "desktop/agentation": ["agentation@1.1.0", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-hr6gyxw8lT7mbzIZWFGg3rzuVJnkWMxeo7Y3TyHN5Fa/QZ38vGwVm8wFrv/1ZbbSRnaikbEzUcqKli0AOujP+A=="], 1395 1396 1396 1397 "desktop/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], 1397 1398