• State inspector for viewing and mutating registered application state snapshots. Displays state sources grouped by category with JSON editors and apply functionality. Read-only sources are displayed without edit capabilities.

    Returns ReactElement

    JSX element rendering the state inspector panel

    export function StateInspector(): React.ReactElement {
    const { stateInspectorSources } = useDebugState();
    const { applyStateInspectorUpdate, refreshStateInspectorSource } =
    useDebugActions();

    const [editorState, setEditorState] = useState<Record<string, EditorState>>(
    {},
    );

    useEffect(() => {
    setEditorState((prev) => {
    const nextState: Record<string, EditorState> = {};

    for (const source of stateInspectorSources) {
    const previous = prev[source.id];
    const formatted = safeStringify(source.value);
    // Preserve dirty editor state to avoid losing unsaved changes
    if (previous?.isDirty) {
    nextState[source.id] = previous;
    } else {
    // Update with fresh snapshot when not being edited
    nextState[source.id] = {
    value: formatted,
    isDirty: false,
    error: null,
    };
    }
    }

    return nextState;
    });
    }, [stateInspectorSources]);

    const groups = useMemo(() => {
    // Group sources by category for organized display
    return stateInspectorSources.reduce(
    (acc, source) => {
    if (!acc[source.group]) {
    acc[source.group] = [];
    }
    acc[source.group].push(source);
    return acc;
    },
    {} as Record<string, typeof stateInspectorSources>,
    );
    }, [stateInspectorSources]);

    const handleEditorChange = (id: string, value: string) => {
    setEditorState((prev) => ({
    ...prev,
    [id]: {
    value,
    isDirty: true,
    error: null,
    },
    }));
    };

    const handleRefresh = (id: string) => {
    refreshStateInspectorSource(id);
    setEditorState((prev) => ({
    ...prev,
    [id]: prev[id]
    ? {
    ...prev[id],
    isDirty: false,
    error: null,
    }
    : prev[id],
    }));
    };

    const handleCopy = async (id: string) => {
    const current = editorState[id];
    if (!current) return;
    try {
    await navigator.clipboard.writeText(current.value);
    toast.success("State snapshot copied to clipboard");
    } catch (error) {
    toast.error("Failed to copy state snapshot");
    console.error("Failed to copy state inspector snapshot", error);
    }
    };

    const handleApply = (id: string, label: string) => {
    const current = editorState[id];
    if (!current) return;
    try {
    const parsed = JSON.parse(current.value);
    applyStateInspectorUpdate(id, parsed);
    toast.success(`${label} state updated`);
    setEditorState((prev) => ({
    ...prev,
    [id]: {
    value: current.value,
    isDirty: false,
    error: null,
    },
    }));
    } catch (error) {
    const message =
    error instanceof SyntaxError
    ? "Invalid JSON payload"
    : "Failed to apply state mutation";
    toast.error(message);
    console.error("State inspector apply error", error);
    setEditorState((prev) => ({
    ...prev,
    [id]: {
    ...current,
    error: message,
    },
    }));
    }
    };

    if (stateInspectorSources.length === 0) {
    return (
    <div className="border-border/60 bg-muted/20 flex h-full flex-col items-center justify-center gap-4 rounded-2xl border border-dashed p-8 text-center">
    <FlaskConical className="text-muted-foreground h-10 w-10" />
    <div className="space-y-2">
    <h2 className="text-lg font-semibold">No state sources registered</h2>
    <p className="text-muted-foreground text-sm">
    Enable debug instrumentation in providers or hooks to inspect and
    mutate runtime state from this panel.
    </p>
    </div>
    </div>
    );
    }

    return (
    <div className="flex h-full flex-col gap-6">
    <div className="flex flex-col gap-3">
    <h2 className="text-lg font-semibold">State inspector</h2>
    <p className="text-muted-foreground text-sm">
    Review live snapshots of registered application state. When a source
    allows mutations, you can edit the JSON payload and apply it to the
    live runtime for testing.
    </p>
    <div className="flex flex-wrap items-center gap-2 text-xs">
    <Badge variant="outline" className="border-border/60 bg-muted/40">
    {stateInspectorSources.length} source
    {stateInspectorSources.length === 1 ? "" : "s"}
    </Badge>
    <Badge variant="outline" className="border-border/60 bg-muted/40">
    {Object.keys(groups).length} group
    {Object.keys(groups).length === 1 ? "" : "s"}
    </Badge>
    </div>
    </div>

    <Separator />

    <ScrollArea className="flex-1">
    <div className="space-y-8 pr-4">
    {Object.entries(groups).map(([group, sources]) => (
    <div key={group} className="space-y-4">
    <div className="flex items-center gap-3">
    <Badge className="bg-primary/10 text-primary border-primary/20 border">
    {group}
    </Badge>
    <span className="text-muted-foreground text-xs">
    {sources.length} source
    {sources.length === 1 ? "" : "s"}
    </span>
    </div>

    <div className="space-y-6 pb-4">
    {sources.map((source) => {
    const editor = editorState[source.id] ?? {
    value: safeStringify(source.value),
    isDirty: false,
    error: null,
    };
    const isReadOnly = !source.canEdit;

    let statusBadge: React.ReactNode;
    if (isReadOnly) {
    statusBadge = (
    <Badge
    variant="outline"
    className="border-border/60 text-muted-foreground"
    >
    Read only
    </Badge>
    );
    } else if (editor.isDirty) {
    statusBadge = (
    <Badge className="bg-amber-500/15 text-amber-600 dark:text-amber-300">
    Pending update
    </Badge>
    );
    } else {
    statusBadge = (
    <Badge
    variant="outline"
    className="border-border/60 text-muted-foreground"
    >
    Live
    </Badge>
    );
    }

    return (
    <div
    key={source.id}
    className="border-border/60 bg-background/90 rounded-2xl border p-4 shadow-sm"
    >
    <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-1">
    <div className="flex items-center gap-2">
    <h3 className="text-base font-semibold">
    {source.label}
    </h3>
    {statusBadge}
    </div>
    {source.description ? (
    <p className="text-muted-foreground text-sm">
    {source.description}
    </p>
    ) : null}
    </div>
    <div className="text-muted-foreground flex flex-col items-start gap-1 text-xs sm:items-end">
    <span className="flex items-center gap-1">
    <RefreshCcw className="h-3.5 w-3.5" />
    Last updated {formatTimestamp(source.lastUpdated)}
    </span>
    {editor.error ? (
    <span className="flex items-center gap-1 text-red-500 dark:text-red-400">
    <AlertCircle className="h-3.5 w-3.5" />
    {editor.error}
    </span>
    ) : null}
    </div>
    </div>

    <div className="mt-4 space-y-4">
    <Textarea
    value={editor.value}
    onChange={(event) =>
    handleEditorChange(source.id, event.target.value)
    }
    spellCheck={false}
    className={cn(
    "min-h-[220px] w-full resize-y whitespace-pre-wrap font-mono text-xs leading-relaxed",
    isReadOnly && "opacity-70",
    editor.error &&
    "border-red-400 focus-visible:ring-red-400",
    )}
    wrap="soft"
    style={{ overflowWrap: "anywhere" }}
    readOnly={isReadOnly}
    />

    <div className="flex flex-wrap items-center gap-2">
    <Button
    type="button"
    variant="outline"
    size="sm"
    onClick={() => handleRefresh(source.id)}
    >
    <RotateCcw className="mr-2 h-4 w-4" />
    Refresh snapshot
    </Button>
    <Button
    type="button"
    variant="outline"
    size="sm"
    onClick={() => handleCopy(source.id)}
    >
    <ClipboardCopy className="mr-2 h-4 w-4" />
    Copy JSON
    </Button>
    <div className="ml-auto flex items-center gap-2">
    <Button
    type="button"
    variant="default"
    size="sm"
    onClick={() =>
    handleApply(source.id, source.label)
    }
    disabled={isReadOnly || !editor.isDirty}
    >
    <Check className="mr-2 h-4 w-4" />
    Apply changes
    </Button>
    </div>
    </div>

    {isReadOnly ? (
    <div className="border-border/60 bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2 text-xs">
    <ShieldAlert className="text-muted-foreground h-3.5 w-3.5" />
    <span>
    This source is exposed for observation only.
    Mutations are disabled by the provider.
    </span>
    </div>
    ) : (
    <div className="border-border/60 bg-muted/30 flex items-start gap-2 rounded-md border px-3 py-2 text-xs">
    <FileWarning className="text-muted-foreground h-3.5 w-3.5" />
    <span>
    Applying malformed state can destabilise the
    application. Export snapshots before mutating, and
    restore them if issues occur.
    </span>
    </div>
    )}
    </div>
    </div>
    );
    })}
    </div>
    </div>
    ))}
    </div>
    </ScrollArea>
    </div>
    );
    }