mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: tool page race condition on refresh and install error reporting
- Subscribe to features store reactively in ToolPage so refresh shows install prompt correctly instead of the tool UI - Show loading state while features are being fetched for AI tools - Capture stdout from install script for better error messages - Keep last 20 stderr lines for error context instead of just exit code - Clear install progress on success (was leaving stale state) - Pass PIP_CACHE_DIR to install subprocess
This commit is contained in:
@@ -133,27 +133,35 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise<void>
|
||||
const modelsDir = getModelsDir();
|
||||
|
||||
const child = spawn(pythonPath, [scriptPath, bundleId, manifestPath, modelsDir], {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
BUNDLE_ID: bundleId,
|
||||
PIP_CACHE_DIR: join(getAiDir(), "pip-cache"),
|
||||
},
|
||||
});
|
||||
|
||||
let stderrBuffer = "";
|
||||
let stdoutBuffer = "";
|
||||
const lastStderrLines: string[] = [];
|
||||
|
||||
child.stdout.on("data", (chunk: Buffer) => {
|
||||
stdoutBuffer += chunk.toString();
|
||||
});
|
||||
|
||||
child.stderr.on("data", (chunk: Buffer) => {
|
||||
stderrBuffer += chunk.toString();
|
||||
|
||||
// Process complete lines
|
||||
const lines = stderrBuffer.split("\n");
|
||||
// Keep the last incomplete line in the buffer
|
||||
stderrBuffer = lines.pop() ?? "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
lastStderrLines.push(trimmed);
|
||||
if (lastStderrLines.length > 20) lastStderrLines.shift();
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as { progress?: number; stage?: string };
|
||||
if (typeof parsed.progress === "number") {
|
||||
@@ -170,7 +178,7 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise<void>
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Not JSON — ignore non-progress stderr output
|
||||
// Not JSON progress — rembg/pip output noise, keep in lastStderrLines for error reporting
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -181,10 +189,12 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise<void>
|
||||
if (code === 0) {
|
||||
invalidateCache();
|
||||
shutdownDispatcher();
|
||||
setInstallProgress(bundleId, { percent: 100, stage: "Complete" }, null);
|
||||
setInstallProgress(null, null, null);
|
||||
updateSingleFileProgress({ jobId, phase: "complete", percent: 100, stage: "Complete" });
|
||||
} else {
|
||||
const errorMsg = `Install failed with exit code ${code}`;
|
||||
const errorDetail =
|
||||
lastStderrLines.filter((l) => !l.startsWith("{")).join("\n") || stdoutBuffer.trim();
|
||||
const errorMsg = errorDetail || `Install failed with exit code ${code}`;
|
||||
setInstallProgress(bundleId, null, errorMsg);
|
||||
updateSingleFileProgress({ jobId, phase: "failed", percent: 0, error: errorMsg });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PYTHON_SIDECAR_TOOLS, TOOLS } from "@ashim/shared";
|
||||
import { PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@ashim/shared";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
@@ -110,12 +110,22 @@ export function ToolPage() {
|
||||
[toolId],
|
||||
);
|
||||
const isAiTool = toolId ? (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(toolId) : false;
|
||||
const getBundleForTool = useFeaturesStore((s) => s.getBundleForTool);
|
||||
const isToolInstalled = useFeaturesStore((s) => s.isToolInstalled);
|
||||
const featureBundle = toolId ? getBundleForTool(toolId) : null;
|
||||
const toolInstalled = toolId ? isToolInstalled(toolId) : true;
|
||||
const featuresLoaded = useFeaturesStore((s) => s.loaded);
|
||||
const featureBundles = useFeaturesStore((s) => s.bundles);
|
||||
const fetchFeatures = useFeaturesStore((s) => s.fetch);
|
||||
const featureBundle = useMemo(() => {
|
||||
if (!toolId) return null;
|
||||
const bundleId = TOOL_BUNDLE_MAP[toolId];
|
||||
if (!bundleId) return null;
|
||||
return featureBundles.find((b) => b.id === bundleId) ?? null;
|
||||
}, [toolId, featureBundles]);
|
||||
const toolInstalled = featureBundle ? featureBundle.status === "installed" : !isAiTool;
|
||||
const { hasPermission } = useAuth();
|
||||
const isAdmin = hasPermission("settings:write");
|
||||
|
||||
useEffect(() => {
|
||||
if (isAiTool) fetchFeatures();
|
||||
}, [isAiTool, fetchFeatures]);
|
||||
const {
|
||||
files,
|
||||
entries,
|
||||
@@ -244,6 +254,16 @@ export function ToolPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isAiTool && !featuresLoaded) {
|
||||
return (
|
||||
<AppLayout>
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAiTool && !toolInstalled && featureBundle) {
|
||||
return (
|
||||
<AppLayout>
|
||||
|
||||
Reference in New Issue
Block a user