feat: Implement dynamic plugin definition loading from remote/local sources and standardize action IDs using plugin metadata.

This commit is contained in:
2026-02-02 12:05:52 -05:00
parent 54c34b6f7d
commit 7fd0d97a67
23 changed files with 270 additions and 243 deletions

View File

@@ -318,20 +318,11 @@ export class ActionRegistry {
headers?: Record<string, string>;
};
}>;
metadata?: Record<string, any>;
};
}>,
): void {
console.log("ActionRegistry.loadPluginActions called with:", {
studyId,
pluginCount: studyPlugins?.length ?? 0,
plugins: studyPlugins?.map((sp) => ({
id: sp.plugin.id,
actionCount: Array.isArray(sp.plugin.actionDefinitions)
? sp.plugin.actionDefinitions.length
: 0,
hasActionDefs: !!sp.plugin.actionDefinitions,
})),
});
// console.log("ActionRegistry.loadPluginActions called with:", { studyId, pluginCount: studyPlugins?.length ?? 0 });
if (this.pluginActionsLoaded && this.loadedStudyId === studyId) return;
@@ -347,11 +338,7 @@ export class ActionRegistry {
? plugin.actionDefinitions
: undefined;
console.log(`Plugin ${plugin.id}:`, {
actionDefinitions: plugin.actionDefinitions,
isArray: Array.isArray(plugin.actionDefinitions),
actionCount: actionDefs?.length ?? 0,
});
// console.log(`Plugin ${plugin.id}:`, { actionCount: actionDefs?.length ?? 0 });
if (!actionDefs) return;
@@ -399,9 +386,13 @@ export class ActionRegistry {
retryable: action.retryable,
};
// Extract semantic ID from metadata if available, otherwise fall back to database IDs (which typically causes mismatch if seed uses semantic)
// Ideally, plugin.metadata.robotId should populate this.
const semanticRobotId = plugin.metadata?.robotId || plugin.robotId || plugin.id;
const actionDef: ActionDefinition = {
id: `${plugin.robotId ?? plugin.id}.${action.id}`,
type: `${plugin.robotId ?? plugin.id}.${action.id}`,
id: `${semanticRobotId}.${action.id}`,
type: `${semanticRobotId}.${action.id}`,
name: action.name,
description: action.description ?? "",
category,
@@ -412,7 +403,7 @@ export class ActionRegistry {
),
source: {
kind: "plugin",
pluginId: plugin.robotId ?? plugin.id,
pluginId: semanticRobotId, // Use semantic ID here too
robotId: plugin.robotId,
pluginVersion: plugin.version ?? undefined,
baseActionId: action.id,
@@ -439,15 +430,8 @@ export class ActionRegistry {
console.log(
`ActionRegistry: Loaded ${totalActionsLoaded} plugin actions for study ${studyId}`,
);
console.log("Current action registry state:", {
totalActions: this.actions.size,
actionsByCategory: {
wizard: this.getActionsByCategory("wizard").length,
robot: this.getActionsByCategory("robot").length,
control: this.getActionsByCategory("control").length,
observation: this.getActionsByCategory("observation").length,
},
});
// console.log("Current action registry state:", { totalActions: this.actions.size });
this.pluginActionsLoaded = true;
this.loadedStudyId = studyId;

View File

@@ -180,6 +180,8 @@ export function DesignerRoot({
robotId: sp.plugin.robotId ?? "",
name: sp.plugin.name,
version: sp.plugin.version,
actionDefinitions: sp.plugin.actionDefinitions as any[],
metadata: sp.plugin.metadata as Record<string, any>,
})),
[studyPluginsRaw],
);
@@ -272,11 +274,11 @@ export function DesignerRoot({
if (initialized) return;
if (loadingExperiment && !initialDesign) return;
console.log('[DesignerRoot] 🚀 INITIALIZING', {
hasExperiment: !!experiment,
hasInitialDesign: !!initialDesign,
loadingExperiment,
});
// console.log('[DesignerRoot] 🚀 INITIALIZING', {
// hasExperiment: !!experiment,
// hasInitialDesign: !!initialDesign,
// loadingExperiment,
// });
const adapted =
initialDesign ??
@@ -304,7 +306,7 @@ export function DesignerRoot({
setInitialized(true);
// NOTE: We don't call recomputeHash() here because the automatic
// hash recomputation useEffect will trigger when setSteps() updates the steps array
console.log('[DesignerRoot] 🚀 Initialization complete, steps set');
// console.log('[DesignerRoot] 🚀 Initialization complete, steps set');
}, [
initialized,
loadingExperiment,
@@ -346,7 +348,7 @@ export function DesignerRoot({
// Small delay to ensure all components have rendered
const timer = setTimeout(() => {
setIsReady(true);
console.log('[DesignerRoot] ✅ Designer ready (plugins loaded), fading in');
// console.log('[DesignerRoot] ✅ Designer ready (plugins loaded), fading in');
}, 150);
return () => clearTimeout(timer);
}
@@ -357,19 +359,13 @@ export function DesignerRoot({
useEffect(() => {
if (!initialized) return;
console.log('[DesignerRoot] Steps changed, scheduling hash recomputation', {
stepsCount: steps.length,
actionsCount: steps.reduce((sum, s) => sum + s.actions.length, 0),
});
// console.log('[DesignerRoot] Steps changed, scheduling hash recomputation');
const timeoutId = setTimeout(async () => {
console.log('[DesignerRoot] Executing debounced hash recomputation');
// console.log('[DesignerRoot] Executing debounced hash recomputation');
const result = await recomputeHash();
if (result) {
console.log('[DesignerRoot] Hash recomputed:', {
newHash: result.designHash.slice(0, 16),
fullHash: result.designHash,
});
// console.log('[DesignerRoot] Hash recomputed:', result.designHash.slice(0, 16));
}
}, 300); // Debounce 300ms
@@ -383,13 +379,12 @@ export function DesignerRoot({
// Debug logging to track hash updates and save button state
useEffect(() => {
console.log('[DesignerRoot] Hash State:', {
currentDesignHash: currentDesignHash?.slice(0, 10),
lastPersistedHash: lastPersistedHash?.slice(0, 10),
hasUnsavedChanges,
stepsCount: steps.length,
});
}, [currentDesignHash, lastPersistedHash, hasUnsavedChanges, steps.length]);
// console.log('[DesignerRoot] Hash State:', {
// currentDesignHash: currentDesignHash?.slice(0, 10),
// lastPersistedHash: lastPersistedHash?.slice(0, 10),
// hasUnsavedChanges,
// });
}, [currentDesignHash, lastPersistedHash, hasUnsavedChanges]);
/* ------------------------------- Step Ops -------------------------------- */
const createNewStep = useCallback(() => {
@@ -426,13 +421,28 @@ export function DesignerRoot({
});
// Debug: log validation results for troubleshooting
console.debug("[DesignerRoot] validation", {
valid: result.valid,
errors: result.errorCount,
warnings: result.warningCount,
infos: result.infoCount,
issues: result.issues,
});
// Debug: Improved structured logging for validation results
console.group("🧪 Experiment Validation Results");
if (result.valid) {
console.log(`%c✓ VALID (0 errors, ${result.warningCount} warnings, ${result.infoCount} hints)`, "color: green; font-weight: bold; font-size: 12px;");
} else {
console.log(`%c✗ INVALID (${result.errorCount} errors, ${result.warningCount} warnings)`, "color: red; font-weight: bold; font-size: 12px;");
}
if (result.issues.length > 0) {
console.table(
result.issues.map(i => ({
Severity: i.severity.toUpperCase(),
Category: i.category,
Message: i.message,
Suggest: i.suggestion,
Location: i.actionId ? `Action ${i.actionId}` : (i.stepId ? `Step ${i.stepId}` : 'Global')
}))
);
} else {
console.log("No issues found. Design is perfectly compliant.");
}
console.groupEnd();
// Persist issues to store for inspector rendering
const grouped = groupIssuesByEntity(result.issues);
clearAllValidationIssues();

View File

@@ -232,9 +232,9 @@ export function PropertiesPanelBase({
</Badge>
{/* internal plugin identifiers hidden from UI */}
<Badge variant="outline" className="h-4 text-[10px]">
{selectedAction.execution.transport}
{selectedAction.execution?.transport}
</Badge>
{selectedAction.execution.retryable && (
{selectedAction.execution?.retryable && (
<Badge variant="outline" className="h-4 text-[10px]">
retryable
</Badge>
@@ -473,7 +473,7 @@ const ParameterEditor = React.memo(function ParameterEditor({
}: ParameterEditorProps) {
// Local state for immediate feedback
const [localValue, setLocalValue] = useState<unknown>(rawValue);
const debounceRef = useRef<NodeJS.Timeout | undefined>();
const debounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
// Sync from prop if it changes externally
useEffect(() => {

View File

@@ -207,6 +207,18 @@ export function InspectorPanel({
/* ------------------------------------------------------------------------ */
/* Render */
/* ------------------------------------------------------------------------ */
const designObject = useMemo(
() => ({
id: "design",
name: "Design",
description: "",
version: 1,
steps,
lastSaved: new Date(),
}),
[steps],
);
return (
<div
className={cn(
@@ -284,17 +296,7 @@ export function InspectorPanel({
<div className="flex-1 overflow-x-hidden overflow-y-auto">
<div className="w-full px-0 py-2 break-words whitespace-normal">
<PropertiesPanel
design={useMemo(
() => ({
id: "design",
name: "Design",
description: "",
version: 1,
steps,
lastSaved: new Date(),
}),
[steps],
)}
design={designObject}
selectedStep={selectedStep}
selectedAction={selectedAction}
onActionUpdate={handleActionUpdate}

View File

@@ -659,8 +659,8 @@ export function validateExecution(
const robotActions = steps.flatMap((step) =>
step.actions.filter(
(action) =>
action.execution.transport === "ros2" ||
action.execution.transport === "rest",
action.execution?.transport === "ros2" ||
action.execution?.transport === "rest",
),
);