Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
import { AppShell } from "./components/layout/app-shell";
import { UpdateDialog } from "./components/layout/update-dialog";
import { WebUpdateDialog } from "./components/layout/web-update-dialog";
import { Confetti } from "./components/onboarding/confetti";
import { Onboarding, useOnboarding } from "./components/onboarding/onboarding";
import { ErrorBoundary } from "./components/shared/error-boundary";
Expand All @@ -19,6 +20,7 @@ import { useAuditStore } from "./stores/audit-store";
import { useExtensionStore } from "./stores/extension-store";
import { resolveMode, useUIStore } from "./stores/ui-store";
import { useUpdateStore } from "./stores/update-store";
import { useWebUpdateStore } from "./stores/web-update-store";

/** Minimum interval (ms) between consecutive scan_and_sync calls */
const SCAN_DEBOUNCE_MS = 5_000;
Expand Down Expand Up @@ -50,10 +52,13 @@ export default function App() {
return () => mq.removeEventListener("change", onChange);
}, [mode]);

// Check for updates on startup (non-blocking, silent failure) — desktop only
// Check for updates on startup (non-blocking, silent failure).
// Desktop uses Tauri's native updater; web mode polls GitHub releases.
useEffect(() => {
if (isDesktop()) {
useUpdateStore.getState().checkForUpdate();
} else {
useWebUpdateStore.getState().checkForUpdate();
}
}, []);

Expand Down Expand Up @@ -144,7 +149,7 @@ export default function App() {
return (
<>
{showConfetti && <Confetti />}
{isDesktop() && <UpdateDialog />}
{isDesktop() ? <UpdateDialog /> : <WebUpdateDialog />}
<HashRouter>
<ErrorBoundary>
<Routes>
Expand Down
39 changes: 39 additions & 0 deletions src/components/layout/changelog-markdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Markdown from "react-markdown";

const components = {
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{children}
</a>
),
h2: ({ children }: { children?: React.ReactNode }) => (
<h4 className="mb-2 text-xs font-medium text-muted-foreground">
{children}
</h4>
),
ul: ({ children }: { children?: React.ReactNode }) => (
<ul className="list-disc pl-4 space-y-1 text-sm text-foreground">
{children}
</ul>
),
p: ({ children }: { children?: React.ReactNode }) => (
<p className="text-sm text-foreground">{children}</p>
),
};

/** Renders a release-notes markdown body, or an "improvements" fallback when empty. */
export function ChangelogMarkdown({ body }: { body: string }) {
if (!body) {
return (
<p className="text-sm text-muted-foreground italic">
Bug fixes and improvements.
</p>
);
}
return <Markdown components={components}>{body}</Markdown>;
}
3 changes: 2 additions & 1 deletion src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { NavLink } from "react-router-dom";
import { isDesktop } from "@/lib/transport";
import { ScopeSwitcher } from "./scope-switcher";
import { UpdateCard } from "./update-card";
import { WebUpdateCard } from "./web-update-card";

const mainNavItems = [
{ to: "/", icon: LayoutDashboard, label: "Overview" },
Expand Down Expand Up @@ -88,7 +89,7 @@ export function Sidebar() {
{/* Settings separator */}
<div className="mt-auto mx-3 mb-1 border-t border-sidebar-border/40" />

{isDesktop() && <UpdateCard />}
{isDesktop() ? <UpdateCard /> : <WebUpdateCard />}

<ScopeSwitcher />

Expand Down
3 changes: 2 additions & 1 deletion src/components/layout/update-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useUpdateStore } from "@/stores/update-store";

export function UpdateCard() {
const available = useUpdateStore((s) => s.available);
const dismissed = useUpdateStore((s) => s.dismissed);
const installing = useUpdateStore((s) => s.installing);
const promptUpdate = useUpdateStore((s) => s.promptUpdate);

if (!available) return null;
if (!available || dismissed) return null;

return (
<div className="mb-2 rounded-xl border border-primary/20 bg-primary/5 p-3">
Expand Down
47 changes: 7 additions & 40 deletions src/components/layout/update-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Download, Loader2, X } from "lucide-react";
import Markdown from "react-markdown";
import { useUpdateStore } from "@/stores/update-store";
import { ChangelogMarkdown } from "./changelog-markdown";

export function UpdateDialog() {
const available = useUpdateStore((s) => s.available);
const showChangelog = useUpdateStore((s) => s.showChangelog);
const installing = useUpdateStore((s) => s.installing);
const dismissChangelog = useUpdateStore((s) => s.dismissChangelog);
const dismissDialog = useUpdateStore((s) => s.dismissDialog);
const dismissUpdate = useUpdateStore((s) => s.dismissUpdate);
const confirmUpdate = useUpdateStore((s) => s.confirmUpdate);

if (!showChangelog || !available) return null;
Expand All @@ -16,7 +17,7 @@ export function UpdateDialog() {
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={dismissChangelog}
onClick={dismissDialog}
/>

{/* Dialog */}
Expand All @@ -27,7 +28,7 @@ export function UpdateDialog() {
Update to v{available.version}
</h3>
<button
onClick={dismissChangelog}
onClick={dismissDialog}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<X size={16} />
Expand All @@ -36,47 +37,13 @@ export function UpdateDialog() {

{/* Changelog */}
<div className="flex-1 overflow-y-auto px-5 py-4">
{available.body ? (
<Markdown
components={{
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{children}
</a>
),
h2: ({ children }) => (
<h4 className="mb-2 text-xs font-medium text-muted-foreground">
{children}
</h4>
),
ul: ({ children }) => (
<ul className="list-disc pl-4 space-y-1 text-sm text-foreground">
{children}
</ul>
),
p: ({ children }) => (
<p className="text-sm text-foreground">{children}</p>
),
}}
>
{available.body}
</Markdown>
) : (
<p className="text-sm text-muted-foreground italic">
Bug fixes and improvements.
</p>
)}
<ChangelogMarkdown body={available.body} />
</div>

{/* Footer */}
<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
<button
onClick={dismissChangelog}
onClick={dismissUpdate}
className="rounded-lg border border-border px-4 py-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
Later
Expand Down
25 changes: 25 additions & 0 deletions src/components/layout/web-update-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Download } from "lucide-react";
import { useWebUpdateStore } from "@/stores/web-update-store";

export function WebUpdateCard() {
const available = useWebUpdateStore((s) => s.available);
const dismissed = useWebUpdateStore((s) => s.dismissed);
const promptUpdate = useWebUpdateStore((s) => s.promptUpdate);

if (!available || dismissed) return null;

return (
<div className="mb-2 rounded-xl border border-primary/20 bg-primary/5 p-3">
<p className="text-xs font-medium text-foreground">
v{available.version} available
</p>
<button
onClick={promptUpdate}
className="mt-2 flex w-full items-center justify-center gap-1.5 rounded-md bg-primary px-2.5 py-1.5 text-xs font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
<Download size={12} />
How to Update
</button>
</div>
);
}
59 changes: 59 additions & 0 deletions src/components/layout/web-update-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ExternalLink, X } from "lucide-react";
import { useWebUpdateStore } from "@/stores/web-update-store";
import { ChangelogMarkdown } from "./changelog-markdown";

const INSTRUCTIONS_URL = "https://github.com/RealZST/HarnessKit#updating";

export function WebUpdateDialog() {
const available = useWebUpdateStore((s) => s.available);
const showDialog = useWebUpdateStore((s) => s.showDialog);
const dismissDialog = useWebUpdateStore((s) => s.dismissDialog);
const dismissUpdate = useWebUpdateStore((s) => s.dismissUpdate);

if (!showDialog || !available) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={dismissDialog}
/>

<div className="relative w-[420px] max-h-[70vh] flex flex-col rounded-xl border border-border bg-background shadow-xl">
<div className="flex items-center justify-between border-b border-border px-5 py-4">
<h3 className="text-base font-semibold">
Update to v{available.version}
</h3>
<button
onClick={dismissDialog}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<X size={16} />
</button>
</div>

<div className="flex-1 overflow-y-auto px-5 py-4">
<ChangelogMarkdown body={available.body} />
</div>

<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
<button
onClick={dismissUpdate}
className="rounded-lg border border-border px-4 py-2 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
Close
</button>
<a
href={INSTRUCTIONS_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 rounded-lg bg-primary px-4 py-2 text-xs font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
<ExternalLink size={12} />
View Update Instructions
</a>
</div>
</div>
</div>
);
}
45 changes: 44 additions & 1 deletion src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { toast } from "@/stores/toast-store";
import type { AppIcon, ThemeName } from "@/stores/ui-store";
import { useUIStore } from "@/stores/ui-store";
import { useUpdateStore } from "@/stores/update-store";
import { useWebUpdateStore } from "@/stores/web-update-store";

const THEME_OPTIONS: {
value: ThemeName;
Expand Down Expand Up @@ -104,6 +105,48 @@ function UpdateSection() {
);
}

function WebUpdateSection() {
const available = useWebUpdateStore((s) => s.available);
const checking = useWebUpdateStore((s) => s.checking);
const checkForUpdate = useWebUpdateStore((s) => s.checkForUpdate);
const promptUpdate = useWebUpdateStore((s) => s.promptUpdate);

const handleCheck = async () => {
await checkForUpdate(true);
if (!useWebUpdateStore.getState().available) {
toast.success("You're up to date");
}
};

return (
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">v{__APP_VERSION__}</span>
{available ? (
<button
onClick={promptUpdate}
className="flex items-center gap-1.5 rounded-lg bg-primary px-2.5 py-1 text-xs text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
>
<Download size={12} />
Update to v{available.version}
</button>
) : (
<button
onClick={handleCheck}
disabled={checking}
className="flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50 transition-colors"
>
{checking ? (
<Loader2 size={12} className="animate-spin" />
) : (
<RefreshCw size={12} />
)}
{checking ? "Checking..." : "Check for Updates"}
</button>
)}
</div>
);
}

export default function SettingsPage() {
const {
themeName,
Expand Down Expand Up @@ -236,7 +279,7 @@ export default function SettingsPage() {
<h2 className="text-2xl font-bold tracking-tight select-none">
Settings
</h2>
{isDesktop() && <UpdateSection />}
{isDesktop() ? <UpdateSection /> : <WebUpdateSection />}
</div>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
Expand Down
Loading
Loading