diff --git a/frontend/src/app/actor-builds-list.tsx b/frontend/src/app/actor-builds-list.tsx index 22a66b2e94..ece00a039b 100644 --- a/frontend/src/app/actor-builds-list.tsx +++ b/frontend/src/app/actor-builds-list.tsx @@ -1,13 +1,38 @@ -import { faActorsBorderless, Icon } from "@rivet-gg/icons"; +import * as allIcons from "@rivet-gg/icons"; +import { faActorsBorderless, Icon, type IconProp } from "@rivet-gg/icons"; import { useInfiniteQuery } from "@tanstack/react-query"; import { Link, useNavigate } from "@tanstack/react-router"; -import { Fragment } from "react"; +import { Fragment, useMemo } from "react"; import { match } from "ts-pattern"; import { Button, cn, Skeleton } from "@/components"; import { useDataProvider } from "@/components/actors"; import { VisibilitySensor } from "@/components/visibility-sensor"; import { RECORDS_PER_PAGE } from "./data-providers/default-data-provider"; +const emojiRegex = + /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/u; + +function isEmoji(str: string): boolean { + return emojiRegex.test(str); +} + +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function toPascalCase(str: string): string { + return str + .split("-") + .map((part) => capitalize(part)) + .join(""); +} + +function lookupFaIcon(iconName: string): IconProp | null { + const pascalName = `fa${toPascalCase(iconName)}`; + const iconDef = (allIcons as Record)[pascalName]; + return iconDef ?? null; +} + export function ActorBuildsList() { const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } = useInfiniteQuery(useDataProvider().buildsQueryOptions()); @@ -22,55 +47,79 @@ export function ActorBuildsList() { Connect RivetKit to see instances.

) : null} - {data?.map((build) => ( - - ))} + })} + > + + {displayName} + + + + ); + })} {isFetchingNextPage || isLoading ? Array(RECORDS_PER_PAGE) .fill(null) diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts index 6ecd5884dd..99f1b14b9a 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts @@ -21,17 +21,6 @@ import type { } from "./contexts"; import type { AnyDatabaseProvider } from "./database"; -/** - * Configuration object that can be returned from `workflow()` to provide - * metadata and the run handler. - */ -export interface RunConfig { - /** Icon to display in the inspector for this run handler */ - icon?: string; - /** The actual run handler function */ - run: (...args: any[]) => any; -} - export interface ActorTypes< TState, TConnParams, @@ -53,24 +42,49 @@ const zFunction = < T extends (...args: any[]) => any = (...args: unknown[]) => unknown, >() => z.custom((val) => typeof val === "function"); +// Schema for run handler with metadata +export const RunConfigSchema = z.object({ + /** Display name for the actor in the Inspector UI. */ + name: z.string().optional(), + /** Icon for the actor in the Inspector UI. Can be an emoji or FontAwesome icon name. */ + icon: z.string().optional(), + /** The run handler function. */ + run: zFunction(), +}); +export type RunConfig = z.infer; + +// Run can be either a function or an object with name/icon/run +const zRunHandler = z.union([zFunction(), RunConfigSchema]).optional(); + +/** Extract the run function from either a function or RunConfig object. */ +export function getRunFunction( + run: ((...args: any[]) => any) | RunConfig | undefined, +): ((...args: any[]) => any) | undefined { + if (!run) return undefined; + if (typeof run === "function") return run; + return run.run; +} + +/** Extract run metadata (name/icon) from RunConfig if provided. */ +export function getRunMetadata( + run: ((...args: any[]) => any) | RunConfig | undefined, +): { name?: string; icon?: string } { + if (!run || typeof run === "function") return {}; + return { name: run.name, icon: run.icon }; +} + // This schema is used to validate the input at runtime. The generic types are defined below in `ActorConfig`. // // We don't use Zod generics with `z.custom` because: // (a) there seems to be a weird bug in either Zod, tsup, or TSC that causese external packages to have different types from `z.infer` than from within the same package and // (b) it makes the type definitions incredibly difficult to read as opposed to vanilla TypeScript. -// Schema for RunConfig objects returned by workflow() -const RunConfigSchema = z.object({ - icon: z.string().optional(), - run: zFunction(), -}); - export const ActorConfigSchema = z .object({ onCreate: zFunction().optional(), onDestroy: zFunction().optional(), onWake: zFunction().optional(), onSleep: zFunction().optional(), - run: z.union([zFunction(), RunConfigSchema]).optional(), + run: zRunHandler, onStateChange: zFunction().optional(), onBeforeConnect: zFunction().optional(), onConnect: zFunction().optional(), @@ -88,6 +102,10 @@ export const ActorConfigSchema = z createVars: zFunction().optional(), options: z .object({ + /** Display name for the actor in the Inspector UI. */ + name: z.string().optional(), + /** Icon for the actor in the Inspector UI. Can be an emoji or FontAwesome icon name. */ + icon: z.string().optional(), createVarsTimeout: z.number().positive().default(5000), createConnStateTimeout: z.number().positive().default(5000), onConnectTimeout: z.number().positive().default(5000), @@ -355,7 +373,7 @@ interface BaseActorConfig< * On shutdown, the actor waits for this handler to complete with a * configurable timeout (options.runStopTimeout, default 15s). * - * Can be a function or a RunConfig object (returned by `workflow()`). + * Can be either a function or a RunConfig object with optional name/icon metadata. * * @returns Void or a Promise. If the promise exits, the actor crashes. */ @@ -688,6 +706,16 @@ export function test< export const DocActorOptionsSchema = z .object({ + name: z + .string() + .optional() + .describe("Display name for the actor in the Inspector UI."), + icon: z + .string() + .optional() + .describe( + "Icon for the actor in the Inspector UI. Can be an emoji (e.g., '🚀') or FontAwesome icon name (e.g., 'rocket').", + ), createVarsTimeout: z .number() .optional() diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts index 7ba4473ced..4bc6623487 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts @@ -18,7 +18,7 @@ import { CONN_VERSIONED, } from "@/schemas/actor-persist/versioned"; import { EXTRA_ERROR_LOG } from "@/utils"; -import type { ActorConfig } from "../config"; +import { getRunFunction, type ActorConfig } from "../config"; import type { ConnDriver } from "../conn/driver"; import { createHttpDriver } from "../conn/drivers/http"; import { @@ -1169,16 +1169,11 @@ export class ActorInstance { } #startRunHandler() { - if (!this.#config.run) return; + const runFn = getRunFunction(this.#config.run); + if (!runFn) return; this.#rLog.debug({ msg: "starting run handler" }); -// Handle both function and RunConfig object (returned by workflow()) - const runFn = - typeof this.#config.run === "function" - ? this.#config.run - : this.#config.run.run; - const runSpan = this.startTraceSpan("actor.run"); const runResult = this.#traces.withSpan(runSpan, () => runFn(this.actorContext), diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts index ddb1b3600e..1c0519755f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import type { ActorDefinition, AnyActorDefinition } from "@/actor/definition"; +import { getRunMetadata } from "@/actor/config"; import { type Logger, LogLevelSchema } from "@/common/log"; import { ENGINE_ENDPOINT } from "@/engine-process/constants"; import { InspectorConfigSchema } from "@/inspector/config"; @@ -265,9 +266,21 @@ export type RegistryConfigInput = Omit< export function buildActorNames( config: RegistryConfig, -): Record }> { +): Record }> { return Object.fromEntries( - Object.keys(config.use).map((name) => [name, { metadata: {} }]), + Object.keys(config.use).map((actorName) => { + const definition = config.use[actorName]; + const options = definition.config.options ?? {}; + const runMeta = getRunMetadata(definition.config.run); + const metadata: Record = {}; + // Actor options take precedence over run metadata + metadata.icon = options.icon ?? runMeta.icon; + metadata.name = options.name ?? runMeta.name; + // Remove undefined values + if (!metadata.icon) delete metadata.icon; + if (!metadata.name) delete metadata.name; + return [actorName, { metadata }]; + }), ); } diff --git a/website/src/content/docs/actors/appearance.mdx b/website/src/content/docs/actors/appearance.mdx new file mode 100644 index 0000000000..8ecd8c28fe --- /dev/null +++ b/website/src/content/docs/actors/appearance.mdx @@ -0,0 +1,148 @@ +# Icons & Names + +Actors can be customized with a display name and icon that appear in the Rivet inspector & dashboard. This helps identify actors at a glance when managing your application. + +## Configuration + +Set the `name` and `icon` properties in your actor's `options`: + +```typescript +import { actor } from "rivetkit"; + +const chatRoom = actor({ + options: { + name: "Chat Room", // Human-friendly display name + icon: "comments", // FontAwesome icon name + }, + state: { messages: [] }, + actions: { + // ... + } +}); +``` + +## Icon Formats + +The `icon` property accepts two formats: + +### Emoji + +Use any emoji character directly: + +```typescript +const notificationService = actor({ + options: { + name: "Notifications", + icon: "🔔", + }, + // ... +}); +``` + +### FontAwesome Icons + +Use [FontAwesome](https://fontawesome.com/search) icon names without the "fa" prefix: + +```typescript +const gameServer = actor({ + options: { + name: "Game Server", + icon: "gamepad", + }, + // ... +}); + +const analyticsWorker = actor({ + options: { + name: "Analytics", + icon: "chart-line", + }, + // ... +}); +``` + +## Default Behavior + +If no `icon` is specified, actors display the default actor icon. If no `name` is specified, the actor's registry key (e.g., `chatRoom`, `gameServer`) is displayed instead. + +## Examples + +Here are some common patterns: + +```typescript +import { actor } from "rivetkit"; + +// Chat/messaging actors +const chatRoom = actor({ + options: { name: "Chat Room", icon: "comments" }, + // ... +}); + +// Game-related actors +const matchmaker = actor({ + options: { name: "Matchmaker", icon: "users" }, + // ... +}); + +const gameSession = actor({ + options: { name: "Game Session", icon: "gamepad" }, + // ... +}); + +// Data processing actors +const dataProcessor = actor({ + options: { name: "Data Processor", icon: "microchip" }, + // ... +}); + +// Using emojis for quick identification +const alertService = actor({ + options: { name: "Alerts", icon: "🚨" }, + // ... +}); +``` + +## Advanced: Run Handler Metadata + +For library developers creating reusable run handlers, you can bundle icon and name metadata directly with the `run` property. This allows libraries to provide sensible defaults without requiring users to configure them manually. + +Instead of returning a function from your run handler factory, return an object with `name`, `icon`, and `run`: + +```typescript +import type { RunConfig } from "rivetkit"; + +function myCustomRunHandler(options: MyOptions): RunConfig { + async function run(c) { + // Your run handler logic... + } + + return { + name: "My Custom Handler", + icon: "bolt", + run, + }; +} +``` + +Users can then use this directly: + +```typescript +const myActor = actor({ + run: myCustomRunHandler({ /* options */ }), + // Automatically gets "My Custom Handler" name and "bolt" icon +}); +``` + +Actor-level `options.name` and `options.icon` always take precedence, allowing users to override library defaults: + +```typescript +const myActor = actor({ + options: { + name: "Custom Name", // Overrides "My Custom Handler" + icon: "rocket", // Overrides "bolt" + }, + run: myCustomRunHandler({ /* options */ }), +}); +``` + +The built-in `workflow()` helper uses this pattern to automatically display the workflow icon for workflow-based actors. diff --git a/website/src/content/docs/actors/index.mdx b/website/src/content/docs/actors/index.mdx index f96d9abbb4..70783fd8cc 100644 --- a/website/src/content/docs/actors/index.mdx +++ b/website/src/content/docs/actors/index.mdx @@ -870,6 +870,22 @@ Find the full client guides here: - [React Client](/docs/clients/react) - [Swift Client](/docs/clients/swift) +### Icons & Names + +Customize how actors appear in the UI with display names and icons: + +```typescript +const chatRoom = actor({ + options: { + name: "Chat Room", + icon: "💬", // or FontAwesome: "comments", "chart-line", etc. + }, + // ... +}); +``` + +[Documentation](/docs/actors/appearance). + ## Authentication & Security Validate credentials in `onBeforeConnect` or `createConnState`. Throw an error to reject the connection. Use `c.conn.id` or `c.conn.state` to identify users in actions—never trust user IDs passed as action parameters. diff --git a/website/src/sitemap/mod.ts b/website/src/sitemap/mod.ts index 731422859e..4df88a8aa1 100644 --- a/website/src/sitemap/mod.ts +++ b/website/src/sitemap/mod.ts @@ -276,6 +276,10 @@ export const sitemap = [ title: "Versions & Upgrades", href: "/docs/actors/versions", }, + { + title: "Icons & Names", + href: "/docs/actors/appearance", + }, ], }, ],