Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions jumble/src/components/CharmRunner.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useRef } from "react";
import { render } from "@commontools/html";
import React, { useMemo, useRef } from "react";
import { render, type VNode } from "@commontools/html";
import { UI } from "@commontools/builder";
import { charmSchema, fixItCharm } from "@commontools/charm";
import { useCharmManager } from "@/contexts/CharmManagerContext.tsx";
import { useNavigate } from "react-router-dom";
import { LuX } from "react-icons/lu";
import { DitheredCube } from "@/components/DitherCube.tsx";
import { createPath } from "@/routes.ts";
import { charmId } from "@/utils/charms.ts";
import { Cell, Charm, charmId } from "@/utils/charms.ts";

interface CharmLoaderProps {
charmImport: () => Promise<any>;
argument?: any;
Expand All @@ -16,7 +17,7 @@ interface CharmLoaderProps {
}

interface CharmRendererProps {
charm: any;
charm: Cell<Charm>;
argument?: any;
className?: string;
}
Expand Down Expand Up @@ -81,18 +82,19 @@ function RawCharmRenderer({ charm, className = "" }: CharmRendererProps) {
const [runtimeError, setRuntimeError] = React.useState<Error | null>(null);
const [isFixing, setIsFixing] = React.useState(false);
const { charmManager, currentReplica } = useCharmManager();
const id = useMemo(() => charmId(charm), [charm]);
const navigate = useNavigate();

// Store a reference to the current charm to detect changes
const prevCharmRef = useRef(charm);
const prevCharmRef = useRef<Charm | null>(null);
Copy link

Copilot AI Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initializing prevCharmRef to null means the runtime error state will reset on the initial render; if preserving the initial charm state is required, consider initializing with the current charm value.

Suggested change
const prevCharmRef = useRef<Charm | null>(null);
const prevCharmRef = useRef<Charm | null>(charm);

Copilot uses AI. Check for mistakes.

// Clear error when charm changes
React.useEffect(() => {
if (prevCharmRef.current !== charm) {
setRuntimeError(null);
prevCharmRef.current = charm;
}
}, [charm]);
}, [id]);

const handleFixIt = React.useCallback(async () => {
if (!runtimeError || isFixing) return;
Expand All @@ -111,7 +113,7 @@ function RawCharmRenderer({ charm, className = "" }: CharmRendererProps) {
} finally {
setIsFixing(false);
}
}, [runtimeError, isFixing, charmManager, charm, currentReplica, navigate]);
}, [runtimeError, isFixing, charmManager, id, currentReplica, navigate]);

React.useEffect(() => {
const container = containerRef.current;
Expand All @@ -127,7 +129,10 @@ function RawCharmRenderer({ charm, className = "" }: CharmRendererProps) {

container.addEventListener("common-iframe-error", handleIframeError);

const cleanup = render(container, charm.asSchema(charmSchema).key(UI));
const cleanup = render(
container,
charm.asSchema(charmSchema).key(UI) as Cell<VNode>,
);

return () => {
cleanup();
Expand All @@ -136,7 +141,7 @@ function RawCharmRenderer({ charm, className = "" }: CharmRendererProps) {
container.innerHTML = "";
}
};
}, [charm]);
}, [id]);

return (
<>
Expand Down
34 changes: 21 additions & 13 deletions jumble/src/utils/charms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { type Charm, CharmManager } from "@commontools/charm";
import { Cell, getEntityId } from "@commontools/runner";
import { NAME } from "@commontools/builder";

export type { Cell, Charm };

export function charmId(charm: Charm): string | undefined {
const id = getEntityId(charm);
return id ? id["/"] : undefined;
Expand All @@ -12,50 +14,56 @@ export function charmId(charm: Charm): string | undefined {
* @param charmManager The charm manager instance
* @returns Promise that resolves to an array of mentionable charms (filtered out trash and pinned first)
*/
export async function getMentionableCharms(charmManager: CharmManager): Promise<Cell<Charm>[]> {
export async function getMentionableCharms(
charmManager: CharmManager,
): Promise<Cell<Charm>[]> {
// Sync all collections to ensure we have the latest data
await Promise.all([
charmManager.sync(charmManager.getCharms()),
charmManager.sync(charmManager.getPinned()),
charmManager.sync(charmManager.getTrash())
charmManager.sync(charmManager.getTrash()),
]);

// Get all collections
const allCharms = charmManager.getCharms().get();
const pinnedCharms = charmManager.getPinned().get();
const trashedCharms = charmManager.getTrash().get();

// Create a set of trashed charm IDs for quick lookup
const trashedIds = new Set<string>(
trashedCharms.map(charm => charmId(charm)).filter((id): id is string => id !== undefined)
trashedCharms.map((charm) => charmId(charm)).filter((id): id is string =>
id !== undefined
),
);

// Create a set of pinned charm IDs for quick lookup
const pinnedIds = new Set<string>(
pinnedCharms.map(charm => charmId(charm)).filter((id): id is string => id !== undefined)
pinnedCharms.map((charm) => charmId(charm)).filter((id): id is string =>
id !== undefined
),
);

// Filter out trashed charms and those without IDs
const mentionableCharms = allCharms.filter(charm => {
const mentionableCharms = allCharms.filter((charm) => {
const id = charmId(charm);
return id !== undefined && !trashedIds.has(id);
});

// Sort charms with pinned first, then by name
return mentionableCharms.sort((a, b) => {
const aId = charmId(a);
const bId = charmId(b);

// By this point both aId and bId should be defined, but check just in case
if (!aId || !bId) {
console.warn("Unexpected undefined ID in sort function");
return 0;
}

// Sort pinned first
if (pinnedIds.has(aId) && !pinnedIds.has(bId)) return -1;
if (!pinnedIds.has(aId) && pinnedIds.has(bId)) return 1;

// Then sort by name
const aName = a.get()?.[NAME] ?? "Untitled";
const bName = b.get()?.[NAME] ?? "Untitled";
Expand Down