Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"shadcn": "^4.11.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"ts-pattern": "^5.9.0",
"tw-animate-css": "^1.4.0",
"vercel-minimax-ai-provider": "^0.0.2",
"zod": "^4.4.3"
Expand Down
7 changes: 5 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/entrypoints/action-collections.content/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { kebabCase } from "case-anything"
import { APP_NAME } from "@/utils/constants/app"

export const ACTION_COLLECTION_PAGE_SOURCE = "read-frog-page" as const
export const ACTION_COLLECTION_EXTENSION_SOURCE = `${kebabCase(APP_NAME)}-ext` as const

// Web page → content script: request to open the extension and install a collection.
export const OPEN_ACTION_COLLECTION_INSTALL_REQUEST_TYPE = "openActionCollectionInstall" as const
// Content script → web page: acknowledge the request (used only to detect the extension).
export const OPEN_ACTION_COLLECTION_INSTALL_ACK_TYPE = "openActionCollectionInstallAck" as const
45 changes: 45 additions & 0 deletions src/entrypoints/action-collections.content/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { defineContentScript } from "#imports"
import { env } from "@/env"
import { sendMessage } from "@/utils/message"
import {
ACTION_COLLECTION_EXTENSION_SOURCE,
ACTION_COLLECTION_PAGE_SOURCE,
OPEN_ACTION_COLLECTION_INSTALL_ACK_TYPE,
OPEN_ACTION_COLLECTION_INSTALL_REQUEST_TYPE,
} from "./bridge"

export default defineContentScript({
matches: env.WXT_OFFICIAL_SITE_ORIGINS.map((origin: string) => `${origin}/*`),
main() {
window.addEventListener("message", async (event) => {
// postMessage can come from anyone, so validate the source and origin.
if (event.source !== window || event.origin !== window.location.origin) {
return
}

const { source, type } = event.data || {}
if (
source !== ACTION_COLLECTION_PAGE_SOURCE
|| type !== OPEN_ACTION_COLLECTION_INSTALL_REQUEST_TYPE
) {
return
}

const requestId = typeof event.data.requestId === "string" ? event.data.requestId : ""
const collectionId = typeof event.data.id === "number" ? event.data.id : null
if (!requestId || collectionId == null) {
return
}

// Acknowledge so the page knows the extension is installed.
window.postMessage({
source: ACTION_COLLECTION_EXTENSION_SOURCE,
type: OPEN_ACTION_COLLECTION_INSTALL_ACK_TYPE,
requestId,
}, window.location.origin)

// Hand off to the background to open the options page and start the install flow.
await sendMessage("requestActionCollectionInstall", { collectionId })
})
},
})
4 changes: 4 additions & 0 deletions src/entrypoints/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export default defineBackground({
await openOptionsPage(message.data)
})

onMessage("requestActionCollectionInstall", async (message) => {
await openOptionsPage({ route: `/custom-actions?installActionCollection=${message.data.collectionId}` })
})

setupSidePanelMessageHandler({
extensionBrowser: browser,
logger,
Expand Down
20 changes: 12 additions & 8 deletions src/entrypoints/options/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ComponentType } from "react"
import { lazy, Suspense } from "react"
import { Route, Routes } from "react-router"
import { ROUTE_DEFS } from "./app-sidebar/nav-items"
import { ActionCollectionInstallListener } from "./components/action-collection-install-listener"
import { GeneralPage } from "./pages/general"

type RoutePath = (typeof ROUTE_DEFS)[number]["path"]
Expand Down Expand Up @@ -43,13 +44,16 @@ function RouteLoadingFallback() {

export default function App() {
return (
<Suspense fallback={<RouteLoadingFallback />}>
<Routes>
{ROUTE_DEFS.map(({ path }) => {
const Component = ROUTE_COMPONENTS[path]
return <Route key={path} path={path} element={<Component />} />
})}
</Routes>
</Suspense>
<>
<ActionCollectionInstallListener />
<Suspense fallback={<RouteLoadingFallback />}>
<Routes>
{ROUTE_DEFS.map(({ path }) => {
const Component = ROUTE_COMPONENTS[path]
return <Route key={path} path={path} element={<Component />} />
})}
</Routes>
</Suspense>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router"
import { ActionCollectionDetailDialog } from "../pages/custom-actions/components/ai-feature-store"

const INSTALL_PARAM = "installActionCollection"

export function ActionCollectionInstallListener() {
const { search } = useLocation()
const navigate = useNavigate()
const [collectionId, setCollectionId] = useState<number | null>(() => {
const raw = new URLSearchParams(search).get(INSTALL_PARAM)
const parsed = raw == null ? Number.NaN : Number(raw)
return Number.isInteger(parsed) ? parsed : null
})

useEffect(() => {
const params = new URLSearchParams(search)
if (params.has(INSTALL_PARAM)) {
params.delete(INSTALL_PARAM)
void navigate({ search: params.toString() }, { replace: true })
}
}, [search, navigate])

return (
<ActionCollectionDetailDialog
open={collectionId != null}
collectionId={collectionId}
onOpenChange={(open) => {
if (!open) {
setCollectionId(null)
}
}}
onInstalled={() => setCollectionId(null)}
/>
)
}
13 changes: 8 additions & 5 deletions src/entrypoints/options/components/page-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import { Separator } from "@/components/ui/base-ui/separator"
import { SidebarTrigger } from "@/components/ui/base-ui/sidebar"
import { cn } from "@/utils/styles/utils"

export function PageLayout({ title, children, className, innerClassName }: { title: React.ReactNode, children: React.ReactNode, className?: string, innerClassName?: string }) {
export function PageLayout({ title, children, className, innerClassName, action }: { title: React.ReactNode, children: React.ReactNode, className?: string, innerClassName?: string, action?: React.ReactNode }) {
return (
<div className={cn("w-full pb-8", className)}>
<div className="border-b">
<Container>
<header className="flex h-14 -ml-1.5 shrink-0 items-center gap-2">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-1.5 h-4! my-auto" />
<h1>{title}</h1>
<header className="flex h-14 -ml-1.5 shrink-0 items-center justify-between gap-2">
<div className="flex items-center gap-2">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-1.5 h-4! my-auto" />
<h1>{title}</h1>
</div>
{action}
</header>
</Container>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ vi.mock("../notebase-connection-field", () => ({
),
}))

vi.mock("../../components/ai-feature-store", () => ({
PublishActionButton: () => <div>PublishActionButton</div>,
}))

function cloneConfig(config: Config): Config {
return JSON.parse(JSON.stringify(config)) as Config
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { sanitizeSelectionToolbarCustomAction } from "@/utils/notebase/connection"
import { cn } from "@/utils/styles/utils"
import { selectedCustomActionIdAtom } from "../atoms"
import { PublishActionButton } from "../components/ai-feature-store"
import { formOpts, useAppForm } from "./form"
import { IconField } from "./icon-field"
import { NameField } from "./name-field"
Expand Down Expand Up @@ -105,6 +106,10 @@ function CustomActionConfigEditor({ selectedAction }: { selectedAction: Selectio
<form.AppForm>
<div className={cn("flex-1 bg-card rounded-xl p-4 border flex flex-col justify-between")}>
<div className="flex flex-col gap-4">
<div className="flex justify-end">
<PublishActionButton action={selectedAction} />
</div>

<NameField form={form} />

<IconField form={form} />
Expand Down Expand Up @@ -137,7 +142,7 @@ function CustomActionConfigEditor({ selectedAction }: { selectedAction: Selectio

{betaExperienceConfig.enabled && <NotebaseConnectionField form={form} />}
</div>
<div className="flex justify-end mt-8">
<div className="mt-8 flex flex-wrap items-center justify-end gap-3">
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogTrigger render={<Button type="button" variant="destructive" />}>
{i18n.t("options.floatingButtonAndToolbar.selectionToolbar.customActions.form.delete")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function CustomActionCardList() {
</Button>
)}
/>
<AddActionDialog onSelect={handleTemplateSelect} />
<AddActionDialog onSelectTemplate={handleTemplateSelect} />
</Dialog>

{llmProviders.length === 0 && (
Expand Down Expand Up @@ -125,26 +125,30 @@ function CustomActionCard({ action }: { action: SelectionToolbarCustomAction })
)}
onClick={() => setSelectedCustomActionId(action.id)}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<div className="size-4">
<Icon icon={action.icon} className="size-4 text-zinc-600 dark:text-zinc-300 shrink-0" />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="size-4 shrink-0">
<Icon icon={action.icon} className="size-4 text-zinc-600 dark:text-zinc-300" />
</div>
<span className="text-sm font-medium truncate">{action.name}</span>
</div>
</div>
<span className="text-sm font-medium truncate">{action.name}</span>
<Switch
checked={action.enabled !== false}
onCheckedChange={(checked) => {
void setSelectionToolbarConfig({
...selectionToolbarConfig,
customActions: customActions.map(item =>
item.id === action.id ? { ...item, enabled: checked } : item,
),
})
}}
onPointerDown={event => event.stopPropagation()}
onClick={event => event.stopPropagation()}
/>
</div>
<Switch
checked={action.enabled !== false}
onCheckedChange={(checked) => {
void setSelectionToolbarConfig({
...selectionToolbarConfig,
customActions: customActions.map(item =>
item.id === action.id ? { ...item, enabled: checked } : item,
),
})
}}
onPointerDown={event => event.stopPropagation()}
onClick={event => event.stopPropagation()}
/>
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,57 @@ import { i18n } from "#imports"
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/base-ui/dialog"
import { CUSTOM_ACTION_TEMPLATES } from "@/utils/constants/custom-action-templates"

const T_PREFIX = "options.floatingButtonAndToolbar.selectionToolbar.customActions.templates"

type TemplateI18nKey = Parameters<typeof i18n.t>[0]

function tTemplateKey(key: string) {
return i18n.t(key as TemplateI18nKey)
}

export function AddActionDialog({ onSelect }: { onSelect: (template: CustomActionTemplate) => void }) {
function ActionRow({ icon, name, description, onClick, disabled }: {
icon: string
name: string
description: string
onClick: () => void
disabled?: boolean
}) {
return (
<button
type="button"
className="flex items-center gap-3 rounded-xl border p-3 text-left transition-colors hover:bg-muted/70 disabled:cursor-not-allowed disabled:opacity-60"
onClick={onClick}
disabled={disabled}
>
<Icon icon={icon} className="size-5 shrink-0 text-zinc-600 dark:text-zinc-300" />
<div className="min-w-0">
<div className="text-sm font-medium">{name}</div>
<div className="text-xs text-muted-foreground">{description}</div>
</div>
</button>
)
}

export function AddActionDialog({
onSelectTemplate,
}: {
onSelectTemplate: (template: CustomActionTemplate) => void
}) {
return (
<DialogContent className="sm:max-w-md">
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{i18n.t("options.floatingButtonAndToolbar.selectionToolbar.customActions.templates.dialogTitle")}
</DialogTitle>
<DialogDescription>
{i18n.t("options.floatingButtonAndToolbar.selectionToolbar.customActions.templates.dialogDescription")}
</DialogDescription>
<DialogTitle>{i18n.t(`${T_PREFIX}.dialogTitle`)}</DialogTitle>
<DialogDescription>{i18n.t(`${T_PREFIX}.dialogDescription`)}</DialogDescription>
</DialogHeader>
<div className="grid gap-2">
{CUSTOM_ACTION_TEMPLATES.map(template => (
<button
<ActionRow
key={template.id}
type="button"
className="flex items-center gap-3 rounded-xl border p-3 text-left transition-colors hover:bg-muted/70"
onClick={() => onSelect(template)}
>
<Icon icon={template.icon} className="size-5 shrink-0 text-zinc-600 dark:text-zinc-300" />
<div className="min-w-0">
<div className="text-sm font-medium">{tTemplateKey(template.nameKey)}</div>
<div className="text-xs text-muted-foreground">{tTemplateKey(template.descriptionKey)}</div>
</div>
</button>
icon={template.icon}
name={tTemplateKey(template.nameKey)}
description={tTemplateKey(template.descriptionKey)}
onClick={() => onSelectTemplate(template)}
/>
))}
</div>
</DialogContent>
Expand Down
Loading
Loading