///
import { type Cell, handler, NAME } from "commontools";
import { MentionableCharm } from "./backlinks-index.tsx";
/**
* Parse a path like "CharmName/result/field" or "CharmName/input/field"
*/
function parsePath(path: string): {
charmName: string;
cellType?: "result" | "input";
path: (string | number)[];
} {
const segments = path.split("/").filter((s) => s.length > 0);
if (segments.length === 0) {
throw new Error(`Invalid path: "${path}"`);
}
const charmName = segments[0];
const rest = segments.slice(1);
// Check if second segment is "result" or "input"
if (rest.length > 0 && (rest[0] === "result" || rest[0] === "input")) {
return {
charmName,
cellType: rest[0],
path: rest.slice(1),
};
}
return { charmName, path: rest };
}
/**
* Find a charm by name from the mentionable list
*/
function findCharmByName(
mentionable: Cell,
name: string,
): Cell | undefined {
for (let i = 0; i < mentionable.get().length; i++) {
const c = mentionable.key(i);
if (c.get()[NAME] === name) {
return c;
}
}
return undefined;
}
/**
* Navigate through a path of keys/indices on a cell
*/
function navigateToCell(
cell: Cell,
path: readonly (string | number)[],
): Cell {
let current = cell;
for (const segment of path) {
current = current.key(segment);
}
return current;
}
/**
* Handler for creating links between charm cells.
* Used by chatbot.tsx to enable LLM-driven cell linking.
*
* Supports paths like:
* - "CharmName/result/field" - link from charm result
* - "CharmName/input/field" - link to/from charm input
* - "CharmName/field" - defaults to result
*/
export const linkTool = handler<
{ source: string; target: string },
{ mentionable: Cell }
>(({ source, target }, { mentionable }) => {
const sourceParsed = parsePath(source);
const targetParsed = parsePath(target);
// Find source and target charms
const sourceCharm = findCharmByName(mentionable, sourceParsed.charmName);
if (!sourceCharm) {
const names = mentionable
.map((c) => c[NAME])
.filter(Boolean)
.join(", ");
throw new Error(
`Source charm "${sourceParsed.charmName}" not found. Available: ${
names || "none"
}`,
);
}
const targetCharm = findCharmByName(mentionable, targetParsed.charmName);
if (!targetCharm) {
const names = mentionable
.map((c) => c[NAME])
.filter(Boolean)
.join(", ");
throw new Error(
`Target charm "${targetParsed.charmName}" not found. Available: ${
names || "none"
}`,
);
}
// Navigate to source cell
let sourceCell: Cell = sourceCharm;
if (sourceParsed.cellType === "input") {
const argCell = sourceCharm.resolveAsCell().getArgumentCell();
if (!argCell) throw new Error("Source charm has no argument cell");
sourceCell = argCell;
}
sourceCell = navigateToCell(sourceCell, sourceParsed.path);
// Navigate to target cell
let targetCell: Cell = targetCharm;
if (targetParsed.cellType === "input" || targetParsed.path.length > 0) {
// For any path or explicit "input", navigate to argument cell
const argCell = targetCharm.resolveAsCell().getArgumentCell();
if (!argCell) throw new Error("Target charm has no argument cell");
targetCell = argCell;
}
// Pop last segment as the key to set
const targetPath = [...targetParsed.path];
const targetKey = targetPath.pop();
if (targetKey === undefined) {
throw new Error("Target path cannot be empty");
}
// Navigate to parent and set link
const targetParent = navigateToCell(targetCell, targetPath);
targetParent.key(targetKey).set(sourceCell);
});