···11import { ArrowRight, Webhook } from "../../icons.ts";
22import { nsidToDomain } from "../../../lib/lexicons/resolver.ts";
33-import { isRecordProducingAction, type Action } from "../../../lib/db/schema.ts";
33+import { type Action } from "../../../lib/db/schema.ts";
44+import { isRecordProducingAction } from "../../../lib/actions/registry.ts";
45import { FOLLOW_TARGETS } from "../../../lib/automations/follow-targets.ts";
56import { Favicon, NsidCode } from "../NsidCode/index.tsx";
67import * as s from "./styles.css.ts";
+2-6
app/islands/AutomationForm.tsx
···11import { useState, useCallback, useRef, useMemo, useEffect } from "hono/jsx";
22import type { RecordSchema, SchemaNode } from "../../lib/lexicons/schema-types.js";
33import { nsidRequiresWantedDids } from "../../lib/lexicons/match.js";
44-import {
55- isRecordProducingAction,
66- type Action,
77- type FetchStep,
88- type FollowTarget,
99-} from "../../lib/db/schema.js";
44+import { type Action, type FetchStep, type FollowTarget } from "../../lib/db/schema.js";
55+import { isRecordProducingAction } from "../../lib/actions/registry.js";
106import {
117 ACTION_CATALOGUE,
128 actionTypeKey,
···11+import type { FC } from "hono/jsx";
12import type { Action } from "../db/schema.js";
23import type { PdsAction } from "../automations/pds.js";
34import type { ColorKey } from "../automations/follow-targets.js";
55+66+/** Common prop signature for the lucide-style icon components in `app/icons.ts`. */
77+type IconProps = { size?: number; class?: string; color?: string; "stroke-width"?: number };
88+export type ActionIcon = FC<IconProps>;
49import type {
510 ActionHandler,
611 DryRunContext,
···1722 * ACTION_CATALOGUE array in action-catalogue.ts becomes a derived view in
1823 * Phase 4. */
1924export type CatalogueTile = {
2525+ /** Imperative phrase shown in the "add action" picker (e.g. "Post to Bluesky"). */
2026 label: string;
2127 description: string;
2228 category: "webhook" | "bluesky" | "apps" | "pds";
2323- /** Lucide icon ref. Type left loose because the icon module is itself
2424- * loosely typed and we don't want to force a pre-Phase-4 import here. */
2525- icon: unknown;
2929+ /** Lucide-style icon component (one of the exports from `app/icons.ts`). */
3030+ icon: ActionIcon;
2631 available: boolean;
2732 /** Override for the tile's data-cat color (e.g. follow-sifa is sifa-blue
2833 * while still living under "Bluesky"). */
···5055> = {
5156 type: TAction["$type"];
5257 pdsType: TPdsAction["$type"];
5858+5959+ /** Short noun-phrase label used in the dashboard automation list and
6060+ * delivery-log filters (e.g. "Bluesky Post", "Webhook"). Distinct from
6161+ * `catalogue.label`, which is the imperative phrase in the picker. */
6262+ displayLabel: string;
53635464 /** Replaces `RECORD_PRODUCING_TYPES` and the `actionResultNames.push` calls
5565 * in API routes. */
···120130 record: recordDefinition,
121131 webhook: webhookDefinition,
122132};
133133+134134+/** Action types that produce a record result (uri, cid, rkey) for chaining.
135135+ * Lookup goes through the registry — adding a new action with
136136+ * `recordProducing: true` is enough; no parallel set to maintain. */
137137+export function isRecordProducingAction(type: string): boolean {
138138+ return ACTION_REGISTRY[type as ActionType]?.recordProducing ?? false;
139139+}
···11-import {
22- Bookmark,
33- BookmarkPlus,
44- FilePlus2,
55- Heart,
66- MessageSquare,
77- Pencil,
88- Trash2,
99- UserPlus,
1010- Webhook,
1111-} from "../../app/icons.js";
11+import { Heart, Trash2, UserPlus } from "../../app/icons.js";
22+import { ACTION_REGISTRY, type ActionIcon } from "../actions/registry.js";
123import { FOLLOW_TARGETS, type ColorKey, type FollowTarget } from "./follow-targets.js";
134145export type AddableActionId =
···2011 | "semble-save"
2112 | `follow-${FollowTarget}`;
22131414+type Tile = {
1515+ id: string;
1616+ label: string;
1717+ description: string;
1818+ icon: ActionIcon;
1919+ available: boolean;
2020+ colorKey?: ColorKey;
2121+ faviconDomain?: string;
2222+};
2323+2324type ActionInfo = {
2425 label: string;
2525- icon: (typeof ACTION_CATALOGUE)[number]["actions"][number]["icon"];
2626- catId: (typeof ACTION_CATALOGUE)[number]["id"];
2626+ icon: Tile["icon"];
2727+ catId: "webhook" | "bluesky" | "apps" | "pds";
2728 /** Optional override for the icon-tile color key (data-cat). Lets follow-sifa
2829 * use a sifa-blue and follow-tangled a grey while still grouping under the
2930 * Bluesky/Apps categories. */
3031 colorKey?: ColorKey;
3131- /** Domain used to render the per-app favicon next to the icon (margin-bookmark, follow). */
3232+ /** Domain used to render the per-app favicon next to the icon. */
3233 faviconDomain?: string;
3334};
34353636+/** Tile entry derived from a registered action's `catalogue` metadata. */
3737+function tileFromRegistry(type: keyof typeof ACTION_REGISTRY): Tile {
3838+ const def = ACTION_REGISTRY[type]!;
3939+ const cat = def.catalogue!;
4040+ return {
4141+ id: type,
4242+ label: cat.label,
4343+ description: cat.description,
4444+ icon: cat.icon,
4545+ available: cat.available,
4646+ ...(cat.colorKey ? { colorKey: cat.colorKey } : {}),
4747+ ...(cat.faviconDomain ? { faviconDomain: cat.faviconDomain } : {}),
4848+ };
4949+}
5050+3551/** Build the catalogue tile for a given follow target. Insertion order in
3652 * `FOLLOW_TARGETS` controls the order tiles appear within their category. */
3737-function followTileFor(target: FollowTarget) {
5353+function followTileFor(target: FollowTarget): Tile {
3854 const t = FOLLOW_TARGETS[target];
3955 return {
4040- id: `follow-${target}` as const,
5656+ id: `follow-${target}`,
4157 label: t.label,
4258 description: t.description,
4359 icon: UserPlus,
···4763 };
4864}
49655050-const followTilesByCat: Record<"bluesky" | "apps", ReturnType<typeof followTileFor>[]> = {
5151- bluesky: [],
5252- apps: [],
5353-};
6666+const followTilesByCat: Record<"bluesky" | "apps", Tile[]> = { bluesky: [], apps: [] };
5467for (const target of Object.keys(FOLLOW_TARGETS) as FollowTarget[]) {
5568 followTilesByCat[FOLLOW_TARGETS[target].catId].push(followTileFor(target));
5669}
57707171+/** "Coming soon" placeholders that have no executor yet. Kept here so the
7272+ * picker can preview the planned shape of each category. */
7373+const COMING_SOON: Record<"bluesky" | "pds", Tile[]> = {
7474+ bluesky: [
7575+ {
7676+ id: "bsky-like",
7777+ label: "Like a post",
7878+ description: "Like a Bluesky post on your behalf",
7979+ icon: Heart,
8080+ available: false,
8181+ },
8282+ ],
8383+ pds: [
8484+ {
8585+ id: "delete-record",
8686+ label: "Delete a record",
8787+ description: "Remove a record from a collection",
8888+ icon: Trash2,
8989+ available: false,
9090+ },
9191+ ],
9292+};
9393+5894export const ACTION_CATALOGUE = [
5995 {
6060- id: "webhook",
9696+ id: "webhook" as const,
6197 label: "Webhooks",
6298 description: "Send event data to your own server",
6363- actions: [
6464- {
6565- id: "webhook",
6666- label: "Send a webhook",
6767- description: "POST event data to an external URL",
6868- icon: Webhook,
6969- available: true,
7070- },
7171- ],
9999+ actions: [tileFromRegistry("webhook")],
72100 },
73101 {
7474- id: "bluesky",
102102+ id: "bluesky" as const,
75103 label: "Bluesky",
76104 description: "High-level Bluesky interactions",
7777- actions: [
7878- {
7979- id: "bsky-post",
8080- label: "Post to Bluesky",
8181- description: "Publish a post to your Bluesky account",
8282- icon: MessageSquare,
8383- available: true,
8484- },
8585- ...followTilesByCat.bluesky,
8686- {
8787- id: "bsky-like",
8888- label: "Like a post",
8989- description: "Like a Bluesky post on your behalf",
9090- icon: Heart,
9191- available: false,
9292- },
9393- ],
105105+ actions: [tileFromRegistry("bsky-post"), ...followTilesByCat.bluesky, ...COMING_SOON.bluesky],
94106 },
95107 {
9696- id: "apps",
108108+ id: "apps" as const,
97109 label: "Apps",
98110 description: "Quick actions for specific AT Protocol apps",
99111 actions: [
100100- {
101101- id: "margin-bookmark",
102102- label: "Bookmark on Margin",
103103- description: "Create a bookmark note in Margin.at",
104104- icon: Bookmark,
105105- available: true,
106106- faviconDomain: "margin.at",
107107- },
108108- {
109109- id: "semble-save",
110110- label: "Save on Semble",
111111- description: "Save a URL as a card on Semble",
112112- icon: BookmarkPlus,
113113- available: true,
114114- colorKey: "cosmik" as ColorKey,
115115- faviconDomain: "semble.so",
116116- },
112112+ tileFromRegistry("margin-bookmark"),
113113+ tileFromRegistry("semble-save"),
117114 ...followTilesByCat.apps,
118115 ],
119116 },
120117 {
121121- id: "pds",
118118+ id: "pds" as const,
122119 label: "PDS records",
123120 description: "Low-level lexicon record operations",
124124- actions: [
125125- {
126126- id: "record",
127127- label: "Create a record",
128128- description: "Create a new record in any collection",
129129- icon: FilePlus2,
130130- available: true,
131131- },
132132- {
133133- id: "patch-record",
134134- label: "Update a record",
135135- description: "Modify fields of an existing record",
136136- icon: Pencil,
137137- available: true,
138138- },
139139- {
140140- id: "delete-record",
141141- label: "Delete a record",
142142- description: "Remove a record from a collection",
143143- icon: Trash2,
144144- available: false,
145145- },
146146- ],
121121+ actions: [tileFromRegistry("record"), tileFromRegistry("patch-record"), ...COMING_SOON.pds],
147122 },
148123];
149124
+7-8
lib/automations/labels.ts
···1616 "not-exists": "is missing",
1717};
18181919-export const actionTypeLabels: Record<string, string> = {
2020- webhook: "Webhook",
2121- record: "Create Record",
2222- "bsky-post": "Bluesky Post",
2323- "patch-record": "Update Record",
2424- "margin-bookmark": "Bookmark on Margin",
2525- follow: "Follow",
2626-};
1919+import { ACTION_REGISTRY } from "../actions/registry.js";
2020+2121+/** Short noun-phrase label shown next to existing automations and in delivery
2222+ * logs. Pulled from each action's registry definition. */
2323+export const actionTypeLabels: Record<string, string> = Object.fromEntries(
2424+ Object.entries(ACTION_REGISTRY).map(([t, def]) => [t, def.displayLabel]),
2525+);
27262827export const operationLabels: Record<string, string> = {
2928 create: "Record created",
-13
lib/db/schema.ts
···8989 | FollowAction
9090 | SembleSaveAction;
91919292-/** Action types that produce a record result (uri, cid, rkey) for chaining. */
9393-const RECORD_PRODUCING_TYPES = new Set([
9494- "record",
9595- "bsky-post",
9696- "patch-record",
9797- "margin-bookmark",
9898- "follow",
9999- "semble-save",
100100-]);
101101-export function isRecordProducingAction(type: string): boolean {
102102- return RECORD_PRODUCING_TYPES.has(type);
103103-}
104104-10592export type Condition = {
10693 field: string;
10794 operator: string;
+2-2
lib/jetstream/handler.ts
···11import { db } from "../db/index.js";
22-import { deliveryLogs, type Action, isRecordProducingAction } from "../db/schema.js";
22+import { deliveryLogs, type Action } from "../db/schema.js";
33import { type ActionResult } from "../actions/executor.js";
44-import { ACTION_REGISTRY } from "../actions/registry.js";
44+import { ACTION_REGISTRY, isRecordProducingAction } from "../actions/registry.js";
55import { resolveFetches } from "../actions/fetcher.js";
66import { isSuccess } from "../actions/delivery.js";
77import { type FetchContext } from "../actions/template.js";