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
2 changes: 1 addition & 1 deletion templates/clips/app/components/library/library-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
<>
<div className="shrink-0 space-y-1.5 px-2 py-1.5">
{shouldShowSidebarLink && (
<CaptureInstallInlineLink className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-foreground hover:bg-accent/60">
<CaptureInstallInlineLink className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs text-foreground hover:bg-accent/60">
<IconAppWindow className="h-4 w-4" />
{t("navigation.desktopCta")}
</CaptureInstallInlineLink>
Expand Down
18 changes: 6 additions & 12 deletions templates/clips/app/hooks/use-desktop-promo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useCallback, useEffect, useState } from "react";

import { useIsMobile } from "@/hooks/use-mobile";

const DISMISSED_KEY = "clips.desktop-promo.dismissed";
import {
hasDownloadedDesktopApp,
markDesktopAppDownloaded,
} from "@/lib/capture-install-options";

function detectDesktopApp(): boolean {
if (typeof navigator === "undefined") return false;
Expand All @@ -25,20 +27,12 @@ export function useDesktopPromo() {

useEffect(() => {
setIsDesktopApp(detectDesktopApp());
setDismissed(
typeof window !== "undefined" &&
window.localStorage?.getItem(DISMISSED_KEY) === "1",
);
setDismissed(hasDownloadedDesktopApp());
Comment thread
shomix marked this conversation as resolved.
}, []);

const dismiss = useCallback(() => {
setDismissed(true);
try {
window.localStorage?.setItem(DISMISSED_KEY, "1");
} catch {
// localStorage can throw in private browsing — ignore, dismissal
// still holds for the session via React state.
}
markDesktopAppDownloaded();
}, []);

return {
Expand Down
22 changes: 21 additions & 1 deletion templates/clips/app/lib/capture-install-options.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

import {
hasDownloadedDesktopApp,
markDesktopAppDownloaded,
resolveClipsChromeExtensionEnabled,
supportsPublishedClipsChromeExtensionHost,
} from "./capture-install-options";

describe("capture install options", () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it("remembers when the desktop installer is downloaded", () => {
const values = new Map<string, string>();
vi.stubGlobal("window", {
localStorage: {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => values.set(key, value),
},
});

expect(hasDownloadedDesktopApp()).toBe(false);
markDesktopAppDownloaded();
expect(hasDownloadedDesktopApp()).toBe(true);
});

it("enables the published Chrome extension on supported first-party/local hosts", () => {
expect(
supportsPublishedClipsChromeExtensionHost("clips.agent-native.com"),
Expand Down
21 changes: 21 additions & 0 deletions templates/clips/app/lib/capture-install-options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
const DESKTOP_DOWNLOAD_STORAGE_KEY = "clips.desktop-promo.dismissed";

export function hasDownloadedDesktopApp(): boolean {
try {
return (
typeof window !== "undefined" &&
window.localStorage?.getItem(DESKTOP_DOWNLOAD_STORAGE_KEY) === "1"
);
} catch {
return false;
}
}

export function markDesktopAppDownloaded(): void {
try {
window.localStorage?.setItem(DESKTOP_DOWNLOAD_STORAGE_KEY, "1");
} catch {
// Download tracking is best-effort and must not block the installer.
}
}

function isFalsy(value: unknown): boolean {
if (typeof value !== "string") return false;
return ["0", "false", "no", "off"].includes(value.trim().toLowerCase());
Expand Down
4 changes: 0 additions & 4 deletions templates/clips/app/routes/_app.dictate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -679,10 +679,6 @@ function DownloadDesktopAppCard() {
})}
</p>
</div>
<CaptureInstallButton size="sm" className="gap-1.5">
Comment thread
shomix marked this conversation as resolved.
<IconDownload className="h-3.5 w-3.5" />
{t("dictateRoute.downloadDesktopApp")}
</CaptureInstallButton>
</div>
);
}
Expand Down
18 changes: 5 additions & 13 deletions templates/clips/app/routes/_app.meetings.$meetingId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -797,19 +797,11 @@ export default function MeetingDetailRoute() {
</PageHeader>

{showDesktopRecordHint && (
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-md border border-border bg-accent/20 px-3 py-2.5">
<IconDeviceDesktop className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="text-sm">{t("meetingDetail.desktopHint")}</span>
{!isDesktopApp && (
<CaptureInstallButton
size="sm"
variant="secondary"
className="ml-auto h-8 gap-1.5 cursor-pointer"
>
<IconExternalLink className="h-3.5 w-3.5" />
{t("meetingDetail.getDesktopApp")}
</CaptureInstallButton>
)}
<div className="mb-4 flex items-start gap-3 rounded-md border border-border bg-accent/20 px-3 py-2.5">
<IconDeviceDesktop className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 text-sm">
{t("meetingDetail.desktopHint")}
</span>
</div>
)}

Expand Down
39 changes: 2 additions & 37 deletions templates/clips/app/routes/_app.meetings._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
} from "@agent-native/core/client";
import {
IconAlertTriangle,
IconAppWindow,
IconBellRinging,
IconCalendar,
IconCheck,
Expand All @@ -24,7 +23,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { NavLink, useSearchParams } from "react-router";
import { toast } from "sonner";

import { CaptureInstallButton } from "@/components/capture-install-options";
import { PageHeader } from "@/components/library/page-header";
import type { AttendeeStackParticipant } from "@/components/meetings/attendee-stack";
import { DayHeader, formatDayLabel } from "@/components/meetings/day-header";
Expand Down Expand Up @@ -52,7 +50,6 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { useDesktopPromo } from "@/hooks/use-desktop-promo";
import enMessages from "@/i18n/en-US";

export function meta() {
Expand Down Expand Up @@ -465,7 +462,7 @@ function MeetingNotesSteps() {
);
}

function MeetingNotesGuide({ showDesktopCta }: { showDesktopCta: boolean }) {
function MeetingNotesGuide() {
const t = useT();
return (
<section className="mb-6 rounded-lg border border-border bg-accent/20 p-4">
Expand All @@ -478,16 +475,6 @@ function MeetingNotesGuide({ showDesktopCta }: { showDesktopCta: boolean }) {
{t("meetingsRoute.howToTriggerDescription")}
</p>
</div>
{showDesktopCta && (
<CaptureInstallButton
size="sm"
variant="secondary"
className="h-8 w-fit shrink-0 gap-1.5 cursor-pointer"
>
<IconAppWindow className="h-4 w-4" />
{t("meetingsRoute.getDesktopApp")}
</CaptureInstallButton>
)}
</div>
<MeetingNotesSteps />
</section>
Expand Down Expand Up @@ -696,14 +683,12 @@ function CalendarAccountMenu({
function MeetingsHeader({
query,
onQueryChange,
showDesktopCta,
calendarAccounts,
onConnected,
onDisconnected,
}: {
query: string;
onQueryChange: (next: string) => void;
showDesktopCta: boolean;
calendarAccounts: CalendarAccount[];
onConnected?: () => void | Promise<void>;
onDisconnected?: () => void;
Expand Down Expand Up @@ -748,21 +733,6 @@ function MeetingsHeader({
)}
</div>
</div>
{showDesktopCta && (
<div className="flex w-fit shrink-0 flex-col items-start gap-1 sm:items-end">
<CaptureInstallButton
size="sm"
variant="secondary"
className="h-8 w-fit gap-1.5 cursor-pointer"
>
<IconAppWindow className="h-4 w-4" />
{t("meetingsRoute.getDesktopApp")}
</CaptureInstallButton>
<p className="max-w-56 text-[11px] leading-snug text-muted-foreground">
{t("meetingsRoute.requiredForReminders")}
</p>
</div>
)}
</div>
</>
);
Expand Down Expand Up @@ -807,7 +777,6 @@ export default function MeetingsIndexRoute() {
}, [query]);

const queryClient = useQueryClient();
const { shouldShowSidebarLink: showDesktopCta } = useDesktopPromo();

const accounts = useActionQuery<{ accounts: CalendarAccount[] } | undefined>(
"list-calendar-accounts",
Expand Down Expand Up @@ -1011,7 +980,6 @@ export default function MeetingsIndexRoute() {
<MeetingsHeader
query={query}
onQueryChange={setQuery}
showDesktopCta={showDesktopCta}
calendarAccounts={calendarAccounts}
onConnected={handleCalendarConnected}
onDisconnected={handleCalendarDisconnected}
Expand All @@ -1033,7 +1001,6 @@ export default function MeetingsIndexRoute() {
<MeetingsHeader
query={query}
onQueryChange={setQuery}
showDesktopCta={showDesktopCta}
calendarAccounts={calendarAccounts}
onConnected={handleCalendarConnected}
onDisconnected={handleCalendarDisconnected}
Expand All @@ -1043,9 +1010,7 @@ export default function MeetingsIndexRoute() {
<CalendarReauthBanner onReconnect={handleReconnectCalendar} />
)}

{hasCalendar && meetings.length === 0 && (
<MeetingNotesGuide showDesktopCta={showDesktopCta} />
)}
{hasCalendar && meetings.length === 0 && <MeetingNotesGuide />}

{nothingAtAll ? (
<div className="rounded-lg border border-dashed border-border bg-accent/20 px-6 py-16 text-center">
Expand Down
5 changes: 3 additions & 2 deletions templates/clips/app/routes/download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import enMessages from "@/i18n/en-US";
import {
clipsChromeExtensionEnabled,
clipsChromeExtensionUrl,
markDesktopAppDownloaded,
} from "@/lib/capture-install-options";

export function meta() {
Expand Down Expand Up @@ -118,7 +119,7 @@ function primaryDownloadButton(
if (asset) {
return (
<Button asChild size="lg" className="h-12 gap-2 px-6 text-base">
<a href={asset.url} download>
<a href={asset.url} download onClick={markDesktopAppDownloaded}>
<Icon className="h-5 w-5" />
{downloadLabel}
</a>
Expand Down Expand Up @@ -156,7 +157,7 @@ function secondaryDownloadButton(
if (asset) {
return (
<Button asChild variant="ghost" className={className}>
<a href={asset.url} download>
<a href={asset.url} download onClick={markDesktopAppDownloaded}>
<Icon className="h-4 w-4" />
{downloadLabel}
</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
type: improved
date: 2026-07-14
---

Desktop app prompts stay hidden after you download the installer
Loading