fix: pipeline only shows compatible tools and displays errors

The pipeline tool picker was showing all tools, but only tools
registered via createToolRoute() support pipeline execution. Tools
with custom routes (remove-background, upscale, ocr, etc.) would
silently fail with "Tool not found" and the empty catch block hid
the error from users.

Add GET /api/v1/pipeline/tools endpoint that returns the IDs of
pipeline-compatible tools. The frontend fetches this list and filters
the tool picker accordingly. Also surface pipeline execution errors
in the UI instead of swallowing them.
This commit is contained in:
Siddharth Kumar Sah
2026-03-28 14:38:48 +08:00
parent 1703dcbcfe
commit 0410bf3461
4 changed files with 47 additions and 3 deletions
+11 -1
View File
@@ -17,7 +17,7 @@ import { validateImageBuffer } from "../lib/file-validation.js";
import { sanitizeFilename } from "../lib/filename.js"; import { sanitizeFilename } from "../lib/filename.js";
import { createWorkspace } from "../lib/workspace.js"; import { createWorkspace } from "../lib/workspace.js";
import { requireAuth } from "../plugins/auth.js"; import { requireAuth } from "../plugins/auth.js";
import { getToolConfig } from "./tool-factory.js"; import { getRegisteredToolIds, getToolConfig } from "./tool-factory.js";
/** Schema for a single pipeline step. */ /** Schema for a single pipeline step. */
const pipelineStepSchema = z.object({ const pipelineStepSchema = z.object({
@@ -312,5 +312,15 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
}, },
); );
/**
* GET /api/v1/pipeline/tools
*
* Returns the IDs of tools that can be used as pipeline steps.
* Only tools registered via createToolRoute() support pipeline execution.
*/
app.get("/api/v1/pipeline/tools", async (_request: FastifyRequest, reply: FastifyReply) => {
return reply.send({ toolIds: getRegisteredToolIds() });
});
app.log.info("Pipeline routes registered"); app.log.info("Pipeline routes registered");
} }
+8
View File
@@ -48,6 +48,14 @@ export function getToolConfig(toolId: string): AnyToolRouteConfig | undefined {
return toolRegistry.get(toolId); return toolRegistry.get(toolId);
} }
/**
* Return the IDs of all tools registered via createToolRoute().
* These are the tools that can be used in pipelines and batch processing.
*/
export function getRegisteredToolIds(): string[] {
return [...toolRegistry.keys()];
}
/** /**
* Factory that registers a POST /api/v1/tools/:toolId route. * Factory that registers a POST /api/v1/tools/:toolId route.
* *
@@ -42,6 +42,7 @@ interface PipelineBuilderProps {
processedSize: number; processedSize: number;
stepsCompleted: number; stepsCompleted: number;
} | null; } | null;
executionError?: string | null;
} }
export function PipelineBuilder({ export function PipelineBuilder({
@@ -52,6 +53,7 @@ export function PipelineBuilder({
saving = false, saving = false,
executing = false, executing = false,
executionResult = null, executionResult = null,
executionError = null,
}: PipelineBuilderProps) { }: PipelineBuilderProps) {
const [showToolPicker, setShowToolPicker] = useState(false); const [showToolPicker, setShowToolPicker] = useState(false);
const [expandedStep, setExpandedStep] = useState<string | null>(null); const [expandedStep, setExpandedStep] = useState<string | null>(null);
@@ -61,6 +63,7 @@ export function PipelineBuilder({
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [disabledTools, setDisabledTools] = useState<string[]>([]); const [disabledTools, setDisabledTools] = useState<string[]>([]);
const [experimentalEnabled, setExperimentalEnabled] = useState(false); const [experimentalEnabled, setExperimentalEnabled] = useState(false);
const [pipelineToolIds, setPipelineToolIds] = useState<string[] | null>(null);
useEffect(() => { useEffect(() => {
apiGet<{ settings: Record<string, string> }>("/v1/settings") apiGet<{ settings: Record<string, string> }>("/v1/settings")
@@ -71,15 +74,22 @@ export function PipelineBuilder({
setExperimentalEnabled(data.settings.enableExperimentalTools === "true"); setExperimentalEnabled(data.settings.enableExperimentalTools === "true");
}) })
.catch(() => {}); .catch(() => {});
// Fetch which tools actually support pipeline execution
apiGet<{ toolIds: string[] }>("/v1/pipeline/tools")
.then((data) => setPipelineToolIds(data.toolIds))
.catch(() => {});
}, []); }, []);
const PIPELINE_TOOLS = useMemo(() => { const PIPELINE_TOOLS = useMemo(() => {
return PIPELINE_TOOLS_BASE.filter((t) => { return PIPELINE_TOOLS_BASE.filter((t) => {
if (disabledTools.includes(t.id)) return false; if (disabledTools.includes(t.id)) return false;
if (t.experimental && !experimentalEnabled) return false; if (t.experimental && !experimentalEnabled) return false;
// Only show tools that are registered in the pipeline-compatible tool registry
if (pipelineToolIds && !pipelineToolIds.includes(t.id)) return false;
return true; return true;
}); });
}, [disabledTools, experimentalEnabled]); }, [disabledTools, experimentalEnabled, pipelineToolIds]);
const addStep = useCallback( const addStep = useCallback(
(toolId: string) => { (toolId: string) => {
@@ -326,6 +336,16 @@ export function PipelineBuilder({
</button> </button>
)} )}
{/* Execution error */}
{executionError && (
<div className="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4">
<div className="flex items-center gap-2 text-red-700 dark:text-red-400">
<icons.AlertCircle className="h-5 w-5 shrink-0" />
<span className="text-sm font-medium">{executionError}</span>
</div>
</div>
)}
{/* Execution result */} {/* Execution result */}
{executionResult && ( {executionResult && (
<div className="rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 p-4 space-y-2"> <div className="rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 p-4 space-y-2">
+7 -1
View File
@@ -26,6 +26,7 @@ export function AutomatePage() {
processedSize: number; processedSize: number;
stepsCompleted: number; stepsCompleted: number;
} | null>(null); } | null>(null);
const [executionError, setExecutionError] = useState<string | null>(null);
// Load saved pipelines // Load saved pipelines
const loadPipelines = useCallback(async () => { const loadPipelines = useCallback(async () => {
@@ -96,6 +97,7 @@ export function AutomatePage() {
async (file: File) => { async (file: File) => {
setExecuting(true); setExecuting(true);
setExecutionResult(null); setExecutionResult(null);
setExecutionError(null);
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
@@ -123,9 +125,12 @@ export function AutomatePage() {
processedSize: data.processedSize, processedSize: data.processedSize,
stepsCompleted: data.stepsCompleted, stepsCompleted: data.stepsCompleted,
}); });
} else {
const data = await res.json().catch(() => ({}));
setExecutionError(data.error || `Pipeline failed with status ${res.status}`);
} }
} catch { } catch {
// Error handling setExecutionError("Connection error. Please try again.");
} finally { } finally {
setExecuting(false); setExecuting(false);
} }
@@ -215,6 +220,7 @@ export function AutomatePage() {
saving={saving} saving={saving}
executing={executing} executing={executing}
executionResult={executionResult} executionResult={executionResult}
executionError={executionError}
/> />
</div> </div>
</div> </div>