/** * MI Action Protocol — types and parser for [MI_ACTION:{...}] markers * embedded in LLM responses. * * The LLM outputs prose interleaved with action markers. The parser * extracts actions and returns clean display text for the user. */ export type MiAction = // Canvas shape actions (existing) | { type: "create-shape"; tagName: string; props: Record; ref?: string } | { type: "update-shape"; shapeId: string; fields: Record } | { type: "delete-shape"; shapeId: string } | { type: "connect"; sourceId: string; targetId: string; color?: string } | { type: "move-shape"; shapeId: string; x: number; y: number } | { type: "navigate"; path: string } | { type: "transform"; transform: string; shapeIds: string[] } // Module content actions | { type: "create-content"; module: string; contentType: string; body: Record; ref?: string } | { type: "update-content"; module: string; contentType: string; id: string; body: Record } | { type: "delete-content"; module: string; contentType: string; id: string } // Admin actions | { type: "enable-module"; moduleId: string } | { type: "disable-module"; moduleId: string } | { type: "configure-module"; moduleId: string; settings: Record } // Composite actions | { type: "scaffold"; name: string; steps: MiAction[] } | { type: "batch"; actions: MiAction[]; requireConfirm?: boolean }; export interface ParsedMiResponse { displayText: string; actions: MiAction[]; } const ACTION_PATTERN = /\[MI_ACTION:([\s\S]*?)\]/g; /** * Parse [MI_ACTION:{...}] markers from streamed text. * Returns the clean display text (markers stripped) and an array of actions. */ export function parseMiActions(text: string): ParsedMiResponse { const actions: MiAction[] = []; const displayText = text.replace(ACTION_PATTERN, (_, json) => { try { const action = JSON.parse(json.trim()) as MiAction; if (action && action.type) { actions.push(action); } } catch { // Malformed action — skip silently } return ""; }); return { displayText: displayText.replace(/\n{3,}/g, "\n\n").trim(), actions, }; } /** Check if an action requires user confirmation before executing. */ export function isDestructiveAction(action: MiAction): boolean { switch (action.type) { case "delete-shape": case "delete-content": case "disable-module": return true; case "batch": return action.requireConfirm || action.actions.some(isDestructiveAction); case "scaffold": return action.steps.some(isDestructiveAction); default: return false; } } /** Summarise executed actions for a confirmation chip. */ export function summariseActions(actions: MiAction[]): string { const counts: Record = {}; for (const a of actions) { counts[a.type] = (counts[a.type] || 0) + 1; } const parts: string[] = []; if (counts["create-shape"]) parts.push(`Created ${counts["create-shape"]} shape(s)`); if (counts["update-shape"]) parts.push(`Updated ${counts["update-shape"]} shape(s)`); if (counts["delete-shape"]) parts.push(`Deleted ${counts["delete-shape"]} shape(s)`); if (counts["connect"]) parts.push(`Connected ${counts["connect"]} pair(s)`); if (counts["move-shape"]) parts.push(`Moved ${counts["move-shape"]} shape(s)`); if (counts["transform"]) parts.push(`Applied ${counts["transform"]} transform(s)`); if (counts["navigate"]) parts.push(`Navigating`); if (counts["create-content"]) parts.push(`Created ${counts["create-content"]} item(s)`); if (counts["update-content"]) parts.push(`Updated ${counts["update-content"]} item(s)`); if (counts["delete-content"]) parts.push(`Deleted ${counts["delete-content"]} item(s)`); if (counts["enable-module"]) parts.push(`Enabled ${counts["enable-module"]} module(s)`); if (counts["disable-module"]) parts.push(`Disabled ${counts["disable-module"]} module(s)`); if (counts["configure-module"]) parts.push(`Configured ${counts["configure-module"]} module(s)`); if (counts["scaffold"]) parts.push(`Scaffolded ${counts["scaffold"]} setup(s)`); if (counts["batch"]) parts.push(`Batch: ${counts["batch"]} group(s)`); return parts.join(", ") || ""; } /** Produce a detailed list of actions for the expandable summary. */ export function detailedActionSummary(actions: MiAction[]): string[] { const details: string[] = []; for (const a of actions) { switch (a.type) { case "create-shape": details.push(`Created ${a.tagName}${a.props?.title ? `: "${a.props.title}"` : ""}`); break; case "create-content": details.push(`Created ${a.contentType} in ${a.module}${a.body?.title ? `: "${a.body.title}"` : ""}`); break; case "delete-shape": details.push(`Deleted shape ${a.shapeId}`); break; case "delete-content": details.push(`Deleted ${a.contentType} ${a.id} from ${a.module}`); break; case "connect": details.push(`Connected ${a.sourceId} → ${a.targetId}`); break; case "scaffold": details.push(`Scaffold "${a.name}": ${a.steps.length} steps`); break; case "batch": details.push(`Batch: ${a.actions.length} actions`); break; case "navigate": details.push(`Navigate to ${a.path}`); break; default: details.push(`${a.type}`); } } return details; }