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
6 changes: 5 additions & 1 deletion apps/desktop/src/components/pro-gate-modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ export function ProGateModal({ isOpen, onClose, type }: ProGateModalProps) {
description:
"You've reached the custom template limit for free users. Please upgrade your account to continue.",
};
} else {
} else if (type === "chat") {
return {
description: "4 messages are allowed per conversation for free users. Upgrade to pro for unlimited chat.",
};
} else {
return {
description: "Template duplication is only available for pro users. Please upgrade your account to continue.",
};
}
Comment on lines +20 to 28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make branching exhaustive and explicit for "template_duplicate".

Replace the if/else chain with a switch that enumerates all ProGateType members to prevent future silent defaults.

Apply the diff suggested in types.ts review comment.

🤖 Prompt for AI Agents
In apps/desktop/src/components/pro-gate-modal/index.tsx around lines 20 to 28,
replace the current if/else chain for handling ProGateType values with a switch
statement that explicitly enumerates every ProGateType member (including "chat",
"template_duplicate" and any other members defined in types.ts), returning the
corresponding description for each case; add an explicit default that either
throws an error or uses a never-exhaustive-check helper so new enum values cause
a compile/runtime failure rather than silently falling back, and align wording
with the types.ts suggested diff.

};

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/components/pro-gate-modal/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export interface ProGateModalProps {
isOpen: boolean;
onClose: () => void;
type: "template" | "chat";
type: "template" | "chat" | "template_duplicate";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure exhaustive handling of the new ProGate type across call sites.

Adding "template_duplicate" is fine. Prefer explicit handling (no implicit default) wherever the type is branched on to avoid future silent fallthrough.

You can make getContent() in index.tsx exhaustive:

-  const getContent = () => {
-    if (type === "template") {
-      return { description: "You've reached the custom template limit for free users. Please upgrade your account to continue." };
-    } else if (type === "chat") {
-      return { description: "4 messages are allowed per conversation for free users. Upgrade to pro for unlimited chat." };
-    } else {
-      return { description: "Template duplication is only available for pro users. Please upgrade your account to continue." };
-    }
-  };
+  const getContent = () => {
+    switch (type) {
+      case "template":
+        return { description: "You've reached the custom template limit for free users. Please upgrade your account to continue." };
+      case "chat":
+        return { description: "4 messages are allowed per conversation for free users. Upgrade to pro for unlimited chat." };
+      case "template_duplicate":
+        return { description: "Template duplication is only available for pro users. Please upgrade your account to continue." };
+      default: {
+        const _exhaustive: never = type;
+        return { description: "" };
+      }
+    }
+  };

Run to find any non-exhaustive branches on ProGate type:

Also applies to: 7-7


🏁 Script executed:

#!/bin/bash
rg -nP --type=ts --type=tsx -C2 '\btype\s*===\s*"(template|chat|template_duplicate)"|switch\s*\(\s*type\s*\)' apps/desktop/src

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Search for direct comparisons to ProGate type values and switch statements across .ts and .tsx files
rg -nP -C2 'type\s*===\s*"(template|chat|template_duplicate)"' -g '*.ts' -g '*.tsx' apps/desktop/src
rg -nP -C2 'switch\s*\(\s*type\s*\)' -g '*.ts' -g '*.tsx' apps/desktop/src

Length of output: 1120


Use an exhaustive switch for getContent in pro-gate-modal/index.tsx
Replace the current if/else if/else chain with a switch (type) that has explicit cases for "template", "chat", and "template_duplicate", and a default branch assigning never to enforce exhaustiveness.

🤖 Prompt for AI Agents
In apps/desktop/src/components/pro-gate-modal/types.ts line 4 and
apps/desktop/src/components/pro-gate-modal/index.tsx around the getContent
implementation, replace the existing if/else if/else chain with a switch(type)
that has explicit cases for "template", "chat", and "template_duplicate"; in
each case return the appropriate content narrowing the props to that branch, and
add a default branch that assigns the variable to never (e.g., const
_exhaustiveCheck: never = type) to enforce exhaustiveness so the compiler fails
if the union changes.

}

export type ProGateType = "template" | "chat";
export type ProGateType = "template" | "chat" | "template_duplicate";
57 changes: 21 additions & 36 deletions apps/desktop/src/components/settings/views/template.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { TemplateService } from "@/utils/template-service";
import { type Template } from "@hypr/plugin-db";
import { Button } from "@hypr/ui/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@hypr/ui/components/ui/dropdown-menu";
import { Input } from "@hypr/ui/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";
import { Textarea } from "@hypr/ui/components/ui/textarea";
import { Trans, useLingui } from "@lingui/react/macro";
import { CopyIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { SectionsList } from "../components/template-sections";

Expand All @@ -21,6 +13,7 @@ interface TemplateEditorProps {
template: Template;
onTemplateUpdate: (template: Template) => void;
onDelete?: () => void;
onDuplicate?: (template: Template) => void;
isCreator?: boolean;
}

Expand Down Expand Up @@ -72,6 +65,7 @@ export default function TemplateEditor({
template,
onTemplateUpdate,
onDelete,
onDuplicate,
isCreator = true,
}: TemplateEditorProps) {
const { t } = useLingui();
Expand Down Expand Up @@ -138,8 +132,8 @@ export default function TemplateEditor({
);

const handleDuplicate = useCallback(() => {
// TODO: Implement duplicate functionality
}, []);
onDuplicate?.(template);
}, [onDuplicate, template]);

const handleDelete = useCallback(() => {
onDelete?.();
Expand Down Expand Up @@ -194,33 +188,24 @@ export default function TemplateEditor({
</div>

{isCreator && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<MoreHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleDuplicate} className="cursor-pointer">
<CopyIcon className="mr-2 h-4 w-4" />
<Trans>Duplicate</Trans>
</DropdownMenuItem>

{/* Only show separator and delete option for custom templates */}
{!isBuiltinTemplate && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive hover:bg-red-100 hover:text-red-600 cursor-pointer"
>
<TrashIcon className="mr-2 h-4 w-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</>
<div className="flex gap-2">
{isBuiltinTemplate
? (
<Button variant="outline" size="sm" onClick={handleDuplicate}>
<Trans>Duplicate</Trans>
</Button>
)
: (
<Button
variant="outline"
size="sm"
onClick={handleDelete}
className="text-destructive hover:bg-red-50 hover:text-red-600 hover:border-red-200"
>
<Trans>Delete</Trans>
</Button>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
Comment on lines 190 to 209
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate button hidden for built-ins when user isn’t creator; avoid inert buttons

Showing Duplicate behind isCreator likely hides it for built-ins owned by the app. Also hide action buttons when handlers are absent to avoid no-ops.

Apply this diff:

-          {isCreator && (
-            <div className="flex gap-2">
-              {isBuiltinTemplate
-                ? (
-                  <Button variant="outline" size="sm" onClick={handleDuplicate}>
-                    <Trans>Duplicate</Trans>
-                  </Button>
-                )
-                : (
-                  <Button
-                    variant="outline"
-                    size="sm"
-                    onClick={handleDelete}
-                    className="text-destructive hover:bg-red-50 hover:text-red-600 hover:border-red-200"
-                  >
-                    <Trans>Delete</Trans>
-                  </Button>
-                )}
-            </div>
-          )}
+          {((isBuiltinTemplate && !!onDuplicate) || (!isBuiltinTemplate && isCreator && !!onDelete)) ? (
+            <div className="flex gap-2">
+              {isBuiltinTemplate && onDuplicate && (
+                <Button variant="outline" size="sm" onClick={handleDuplicate}>
+                  <Trans>Duplicate</Trans>
+                </Button>
+              )}
+              {!isBuiltinTemplate && isCreator && onDelete && (
+                <Button
+                  variant="outline"
+                  size="sm"
+                  onClick={handleDelete}
+                  className="text-destructive hover:bg-red-50 hover:text-red-600 hover:border-red-200"
+                >
+                  <Trans>Delete</Trans>
+                </Button>
+              )}
+            </div>
+          ) : null}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isCreator && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<MoreHorizontalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleDuplicate} className="cursor-pointer">
<CopyIcon className="mr-2 h-4 w-4" />
<Trans>Duplicate</Trans>
</DropdownMenuItem>
{/* Only show separator and delete option for custom templates */}
{!isBuiltinTemplate && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive hover:bg-red-100 hover:text-red-600 cursor-pointer"
>
<TrashIcon className="mr-2 h-4 w-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</>
<div className="flex gap-2">
{isBuiltinTemplate
? (
<Button variant="outline" size="sm" onClick={handleDuplicate}>
<Trans>Duplicate</Trans>
</Button>
)
: (
<Button
variant="outline"
size="sm"
onClick={handleDelete}
className="text-destructive hover:bg-red-50 hover:text-red-600 hover:border-red-200"
>
<Trans>Delete</Trans>
</Button>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{((isBuiltinTemplate && !!onDuplicate) || (!isBuiltinTemplate && isCreator && !!onDelete)) ? (
<div className="flex gap-2">
{isBuiltinTemplate && onDuplicate && (
<Button variant="outline" size="sm" onClick={handleDuplicate}>
<Trans>Duplicate</Trans>
</Button>
)}
{!isBuiltinTemplate && isCreator && onDelete && (
<Button
variant="outline"
size="sm"
onClick={handleDelete}
className="text-destructive hover:bg-red-50 hover:text-red-600 hover:border-red-200"
>
<Trans>Delete</Trans>
</Button>
)}
</div>
) : null}
🤖 Prompt for AI Agents
In apps/desktop/src/components/settings/views/template.tsx around lines 190-209,
the current conditional shows the action wrapper only when isCreator, which
hides the Duplicate button for built-in templates and can render inert buttons
when handlers are missing; change the rendering so the Duplicate button is shown
when the template is built-in and a handleDuplicate handler exists, and show the
Delete button when the user is the creator and a handleDelete handler exists;
also only render the surrounding action container if at least one visible button
will be rendered to avoid empty/no-op controls.

</div>
</div>
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop/src/components/settings/views/templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,44 @@ export default function TemplatesView() {
}
};

const handleDuplicateTemplate = async (template: Template) => {
try {
Comment on lines +199 to +200
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove local try/catch; follow repo rule “No error handling.”

Let errors bubble to the caller/UI boundary or centralize in TemplateService.

-  const handleDuplicateTemplate = async (template: Template) => {
-    try {
+  const handleDuplicateTemplate = async (template: Template) => {
@@
-    } catch (error) {
-      console.error("Failed to duplicate template:", error);
-    }
-  };
+  };

Also applies to: 232-235

🤖 Prompt for AI Agents
In apps/desktop/src/components/settings/views/templates.tsx around lines 199-200
(and also for the block at 232-235), remove the local try/catch wrappers in
handleDuplicateTemplate (and the other duplicated block) so errors are not
handled locally; instead let the async function return/throw normally so errors
bubble to the caller or the centralized TemplateService error boundary; if any
cleanup is required keep a finally block only, otherwise delete the catch and
its contents and ensure the function returns the Promise directly.

if (!getLicense.data?.valid) {
analyticsCommands.event({
event: "pro_license_required_template",
distinct_id: userId,
});
await showProGateModal("template_duplicate");
return;
}
Comment on lines +201 to +208
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align duplication gating with creation gating (respect free quota).

Creation checks !valid && customTemplates.length > 1 while duplication gates on any invalid license. Use the same threshold to keep UX consistent.

-      if (!getLicense.data?.valid) {
+      if (!getLicense.data?.valid && customTemplates.length > 1) {
         analyticsCommands.event({
           event: "pro_license_required_template",
           distinct_id: userId,
         });
         await showProGateModal("template_duplicate");
         return;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!getLicense.data?.valid) {
analyticsCommands.event({
event: "pro_license_required_template",
distinct_id: userId,
});
await showProGateModal("template_duplicate");
return;
}
if (!getLicense.data?.valid && customTemplates.length > 1) {
analyticsCommands.event({
event: "pro_license_required_template",
distinct_id: userId,
});
await showProGateModal("template_duplicate");
return;
}
🤖 Prompt for AI Agents
In apps/desktop/src/components/settings/views/templates.tsx around lines 201 to
208, the duplication gating currently blocks all users without a valid license
but creation gating only blocks when the user is over the free quota (i.e.,
!valid && customTemplates.length > 1); update the duplication check to use the
same threshold so it only triggers analytics/event and showProGateModal when
getLicense.data?.valid is false AND customTemplates.length > 1, matching the
creation logic and keeping the UX consistent.


const emojiMatch = template.title?.match(/^(\p{Emoji})\s*/u);
const originalEmoji = emojiMatch ? emojiMatch[1] : "📄";
const titleWithoutEmoji = template.title?.replace(/^(\p{Emoji})\s*/u, "") || "Untitled";
const duplicatedTemplate: Template = {
...template,
id: crypto.randomUUID(),
user_id: userId,
title: `${originalEmoji} ${titleWithoutEmoji} (Copy)`,
tags: template.tags?.filter(tag => tag !== "builtin") || [],
};

await TemplateService.saveTemplate(duplicatedTemplate);

await loadTemplates();

setSelectedTemplate(duplicatedTemplate);
setViewState("editor");

analyticsCommands.event({
event: "template_duplicated",
distinct_id: userId,
});
} catch (error) {
console.error("Failed to duplicate template:", error);
}
};

// Check if current template is being viewed (read-only)
const isViewingTemplate = selectedTemplate && !TemplateService.canEditTemplate(selectedTemplate.id);

Expand All @@ -221,6 +259,7 @@ export default function TemplatesView() {
template={selectedTemplate}
onTemplateUpdate={handleTemplateUpdate}
onDelete={handleTemplateDeleteFromEditor}
onDuplicate={handleDuplicateTemplate}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify TemplateEditor prop contract and unify duplication paths.

  • Ensure TemplateEditor accepts onDuplicate?: (t: Template) => void and wires it to its UI.
  • Route TemplateCard’s “Clone” to this same handler to avoid two divergent code paths (handleCloneTemplate vs handleDuplicateTemplate) and to keep gating consistent. Example updates (outside this hunk):
    • custom list: onClone={() => handleDuplicateTemplate(template)}
    • builtin list: onClone={() => handleDuplicateTemplate(template)}
  • Afterwards, delete handleCloneTemplate.

🏁 Script executed:

#!/bin/bash
# Verify ProGate type supports "template_duplicate"
rg -nP '"template_duplicate"|ProGateType' apps/desktop/src

# Verify TemplateEditor prop includes onDuplicate and is used
rg -nP -C3 'onDuplicate\??\s*:\s*\(template:\s*Template' apps/desktop/src
rg -nP -C3 'export default function TemplateEditor|interface .*Props|type .*Props' apps/desktop/src/components

# Ensure only one duplicate handler exists in this file
rg -nP 'handleDuplicateTemplate\s*=\s*async' apps/desktop/src/components/settings/views/templates.tsx

Length of output: 65257


results:

apps/desktop/src/components/settings/views/templates.tsx:150:  const handleCloneTemplate = async (template: Template) => {
apps/desktop/src/components/settings/views/templates.tsx:250:    onClone={() => handleCloneTemplate(template)}

The search confirms that handleCloneTemplate is still defined and used within templates.tsx at lines 150 and 250, and onClone prop is wired to handleCloneTemplate. The review comment’s suggestion to unify the duplication paths by routing onClone to handleDuplicateTemplate and then removing handleCloneTemplate remains valid.


Unify duplication handlers

  • In apps/desktop/src/components/settings/views/templates.tsx, change all onClone={() => handleCloneTemplate(template)} (usages around line 250) to onClone={() => handleDuplicateTemplate(template)} and delete the handleCloneTemplate definition (around line 150).
  • In TemplateEditor (in apps/desktop/src/components/settings/views/template.tsx), destructure and call its onDuplicate prop in the UI so the “Duplicate” action uses handleDuplicateTemplate.
🤖 Prompt for AI Agents
In apps/desktop/src/components/settings/views/templates.tsx around lines
250-262, replace all usages of onClone={() => handleCloneTemplate(template)}
with onClone={() => handleDuplicateTemplate(template)} and remove the now-unused
handleCloneTemplate function definition located around line 150; in
apps/desktop/src/components/settings/views/template.tsx (TemplateEditor) update
the component props to destructure onDuplicate and invoke onDuplicate where the
“Duplicate” action is rendered so the Duplicate UI calls the passed-in
handleDuplicateTemplate handler. Ensure imports/prop types are updated and no
references to handleCloneTemplate remain.

isCreator={true}
/>
)}
Expand Down
Loading
Loading