Skip to content
← Public packages

@kentcdodds/raycast

Raycast launcher: stored execute commands via webhooks (canonical; do not use raycast-shortcuts).

extension/src/commands.tsx

127 lines · 3.3 KB · TypeScript
import {
  Action,
  ActionPanel,
  Color,
  Detail,
  Icon,
  List,
  showToast,
  Toast,
  useNavigation,
} from "@raycast/api";
import { useCallback, useEffect, useRef, useState } from "react";
import {
  getErrorMessage,
  listCommands,
  RaycastCommandSummary,
  runCommand,
  stringifyResult,
} from "./kody-client";

function resolveIcon(icon: string): Icon | { source: string } {
  if (!icon) return Icon.Terminal;
  if ([...icon].length <= 2) return { source: icon };
  const key = icon as keyof typeof Icon;
  if (Icon[key]) return Icon[key];
  return Icon.Terminal;
}

export default function Command() {
  const { push } = useNavigation();
  const [commands, setCommands] = useState<RaycastCommandSummary[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [runningId, setRunningId] = useState<string | null>(null);
  const commandsRef = useRef(commands);
  commandsRef.current = commands;

  const load = useCallback(async (forceRefresh = false) => {
    // Only block the UI when we have nothing to show yet, or the user forced refresh.
    if (forceRefresh || commandsRef.current.length === 0) {
      setIsLoading(true);
    }
    try {
      const next = await listCommands({
        forceRefresh,
        onRevalidated: setCommands,
      });
      setCommands(next);
    } catch (error) {
      await showToast({
        style: Toast.Style.Failure,
        title: "Failed to load Kody commands",
        message: getErrorMessage(error),
      });
    } finally {
      setIsLoading(false);
    }
  }, []);

  useEffect(() => {
    void load(false);
  }, [load]);

  async function run(command: RaycastCommandSummary) {
    try {
      setRunningId(command.id);
      const result = await runCommand({ commandId: command.id });
      await showToast({
        style: Toast.Style.Success,
        title: "Kody finished",
        message: command.title,
      });
      push(
        <Detail
          markdown={[
            `# ${command.title}`,
            "```json",
            stringifyResult(result),
            "```",
          ].join("\n\n")}
        />,
      );
    } catch (error) {
      await showToast({
        style: Toast.Style.Failure,
        title: "Kody command failed",
        message: getErrorMessage(error),
      });
    } finally {
      setRunningId(null);
    }
  }

  return (
    <List
      isLoading={isLoading || runningId !== null}
      searchBarPlaceholder="Search Kody commands"
    >
      {commands.map((command) => (
        <List.Item
          key={command.id}
          title={command.title}
          subtitle={command.description || command.id}
          icon={resolveIcon(command.icon)}
          accessories={[
            { tag: { value: command.id, color: Color.SecondaryText } },
          ]}
          actions={
            <ActionPanel>
              <Action
                title="Run Command"
                icon={Icon.Play}
                onAction={() => void run(command)}
              />
              <Action
                title="Refresh Commands"
                icon={Icon.ArrowClockwise}
                shortcut={{ modifiers: ["cmd"], key: "r" }}
                onAction={() => void load(true)}
              />
              <Action.CopyToClipboard title="Copy Command Id" content={command.id} />
            </ActionPanel>
          }
        />
      ))}
    </List>
  );
}