import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ArrowLeft, Braces, ChevronDown, Clock, CornerDownRight, GitBranch, Plus, Repeat, Save, Square, Terminal as TerminalIcon, Trash2, Play, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { Textarea } from "@/components/textarea"; import { Checkbox } from "@/components/checkbox"; import { EmptyState } from "@/components/empty-state"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/alert-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/dropdown-menu"; import { getUserPreferences, saveUserPreferences } from "@/api/open-tabs-api"; import { getErrorMessage } from "../lib/error-message.js"; import { parseTerminalMacros, runTerminalMacro, type MacroStep, type TerminalMacro, } from "@/lib/terminal-macros"; import type { Tab } from "@/types/ui-types"; const STEP_TYPES = ["send", "wait", "delay", "if", "repeat"] as const; const STEP_ICONS = { send: TerminalIcon, wait: CornerDownRight, delay: Clock, if: GitBranch, repeat: Repeat, } as const; function id(): string { return typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; } function createStep(type: MacroStep["type"]): MacroStep { const stepId = id(); switch (type) { case "send": return { id: stepId, type, text: "", pressEnter: true }; case "delay": return { id: stepId, type, milliseconds: 1000 }; case "wait": return { id: stepId, type, pattern: "", isRegex: false, timeoutMs: 10_000, onTimeout: "stop", }; case "if": return { id: stepId, type, pattern: "", isRegex: false, then: [], else: [], }; case "repeat": return { id: stepId, type, count: 2, steps: [] }; } } function isValidRegex(pattern: string): boolean { try { new RegExp(pattern); return true; } catch { return false; } } /** Milliseconds are an implementation detail, so the editor works in seconds. */ function SecondsInput({ valueMs, onChangeMs, minMs, label, }: { valueMs: number; onChangeMs: (ms: number) => void; minMs: number; label: string; }) { const { t } = useTranslation(); return ( ); } /** Shared text/regex matcher field used by both "wait" and "if". */ function MatchField({ pattern, isRegex, label, placeholder, onChange, }: { pattern: string; isRegex: boolean; label: string; placeholder: string; onChange: (next: { pattern?: string; isRegex?: boolean }) => void; }) { const { t } = useTranslation(); const badRegex = isRegex && pattern.length > 0 && !isValidRegex(pattern); return (
{label}
onChange({ pattern: event.target.value })} /> {badRegex && (
{t("macros.match.invalidRegex")}
)}
); } function AddStepMenu({ onAdd, compact, }: { onAdd: (type: MacroStep["type"]) => void; compact?: boolean; }) { const { t } = useTranslation(); return ( {STEP_TYPES.map((type) => { const Icon = STEP_ICONS[type]; return ( onAdd(type)} > {t(`macros.stepTypes.${type}.label`)} {t(`macros.stepTypes.${type}.help`)} ); })} ); } function StepList({ steps, onChange, depth = 0, }: { steps: MacroStep[]; onChange: (steps: MacroStep[]) => void; depth?: number; }) { const { t } = useTranslation(); const update = (index: number, step: MacroStep) => { const next = [...steps]; next[index] = step; onChange(next); }; return (
{steps.map((step, index) => { const Icon = STEP_ICONS[step.type]; return (
{t(`macros.stepTypes.${step.type}.label`)}
{step.type === "send" && (