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 modelsDir = getModelsDir();
|
||||||
|
|
||||||
const child = spawn(pythonPath, [scriptPath, bundleId, manifestPath, modelsDir], {
|
const child = spawn(pythonPath, [scriptPath, bundleId, manifestPath, modelsDir], {
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
BUNDLE_ID: bundleId,
|
BUNDLE_ID: bundleId,
|
||||||
|
PIP_CACHE_DIR: join(getAiDir(), "pip-cache"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let stderrBuffer = "";
|
let stderrBuffer = "";
|
||||||
|
let stdoutBuffer = "";
|
||||||
|
const lastStderrLines: string[] = [];
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk: Buffer) => {
|
||||||
|
stdoutBuffer += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
child.stderr.on("data", (chunk: Buffer) => {
|
child.stderr.on("data", (chunk: Buffer) => {
|
||||||
stderrBuffer += chunk.toString();
|
stderrBuffer += chunk.toString();
|
||||||
|
|
||||||
// Process complete lines
|
|
||||||
const lines = stderrBuffer.split("\n");
|
const lines = stderrBuffer.split("\n");
|
||||||
// Keep the last incomplete line in the buffer
|
|
||||||
stderrBuffer = lines.pop() ?? "";
|
stderrBuffer = lines.pop() ?? "";
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
|
|
||||||
|
lastStderrLines.push(trimmed);
|
||||||
|
if (lastStderrLines.length > 20) lastStderrLines.shift();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed) as { progress?: number; stage?: string };
|
const parsed = JSON.parse(trimmed) as { progress?: number; stage?: string };
|
||||||
if (typeof parsed.progress === "number") {
|
if (typeof parsed.progress === "number") {
|
||||||
@@ -170,7 +178,7 @@ export async function registerFeatureRoutes(app: FastifyInstance): Promise<void>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} 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) {
|
if (code === 0) {
|
||||||
invalidateCache();
|
invalidateCache();
|
||||||
shutdownDispatcher();
|
shutdownDispatcher();
|
||||||
setInstallProgress(bundleId, { percent: 100, stage: "Complete" }, null);
|
setInstallProgress(null, null, null);
|
||||||
updateSingleFileProgress({ jobId, phase: "complete", percent: 100, stage: "Complete" });
|
updateSingleFileProgress({ jobId, phase: "complete", percent: 100, stage: "Complete" });
|
||||||
} else {
|
} 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);
|
setInstallProgress(bundleId, null, errorMsg);
|
||||||
updateSingleFileProgress({ jobId, phase: "failed", percent: 0, error: 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 {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
@@ -110,12 +110,22 @@ export function ToolPage() {
|
|||||||
[toolId],
|
[toolId],
|
||||||
);
|
);
|
||||||
const isAiTool = toolId ? (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(toolId) : false;
|
const isAiTool = toolId ? (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(toolId) : false;
|
||||||
const getBundleForTool = useFeaturesStore((s) => s.getBundleForTool);
|
const featuresLoaded = useFeaturesStore((s) => s.loaded);
|
||||||
const isToolInstalled = useFeaturesStore((s) => s.isToolInstalled);
|
const featureBundles = useFeaturesStore((s) => s.bundles);
|
||||||
const featureBundle = toolId ? getBundleForTool(toolId) : null;
|
const fetchFeatures = useFeaturesStore((s) => s.fetch);
|
||||||
const toolInstalled = toolId ? isToolInstalled(toolId) : true;
|
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 { hasPermission } = useAuth();
|
||||||
const isAdmin = hasPermission("settings:write");
|
const isAdmin = hasPermission("settings:write");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAiTool) fetchFeatures();
|
||||||
|
}, [isAiTool, fetchFeatures]);
|
||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
entries,
|
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) {
|
if (isAiTool && !toolInstalled && featureBundle) {
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user