diff --git a/src/lib/components/dialogs/ToolboxManagerDialog.svelte b/src/lib/components/dialogs/ToolboxManagerDialog.svelte
index 898ca599..25122af6 100644
--- a/src/lib/components/dialogs/ToolboxManagerDialog.svelte
+++ b/src/lib/components/dialogs/ToolboxManagerDialog.svelte
@@ -2,6 +2,7 @@
import { onDestroy } from 'svelte';
import Icon from '$lib/components/icons/Icon.svelte';
import { tooltip } from '$lib/components/Tooltip.svelte';
+ import { getBackendType } from '$lib/pyodide/backend';
import DialogShell from './shared/DialogShell.svelte';
import {
TOOLBOX_CATALOG,
@@ -80,8 +81,13 @@
let activeOverrideRow = $state
Only continue if you trust the source.
+ {#if isWebRuntime} +pip install pathview desktop app.
+
+ pip install {resolvedSource.pkg}{resolvedSource.version ? `==${resolvedSource.version}` : ''}
@@ -940,6 +957,30 @@
color: var(--text-muted);
}
+ /* Web-runtime (Pyodide) install limitation notice on the trust step */
+ .web-note {
+ display: flex;
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ background: var(--accent-bg);
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-base);
+ line-height: 1.5;
+ color: var(--text-muted);
+ }
+
+ .web-note :global(svg) {
+ flex-shrink: 0;
+ margin-top: 2px;
+ color: var(--accent);
+ }
+
+ .web-note code {
+ font-family: var(--font-mono);
+ color: var(--text-muted);
+ }
+
.spinner-row {
display: flex;
align-items: center;
diff --git a/src/lib/toolbox/installer.ts b/src/lib/toolbox/installer.ts
index f24627f0..d768596b 100644
--- a/src/lib/toolbox/installer.ts
+++ b/src/lib/toolbox/installer.ts
@@ -78,6 +78,40 @@ function pyStr(s: string): string {
return JSON.stringify(s);
}
+/**
+ * Turn a raw micropip failure into a web-version-aware message.
+ *
+ * The web app runs Python through Pyodide, which can only install
+ * pure-Python wheels (or packages Pyodide ships pre-built). Toolboxes with
+ * compiled/native code fail here even though they install fine in the
+ * standalone (pip-backed) PathView. `_pv_install_micropip` tags those
+ * failures with `PV_INCOMPATIBLE`, so we give a useful hint instead of
+ * surfacing a raw traceback. Genuine failures pass through unchanged.
+ */
+function reframePyodideInstallError(spec: string, err: unknown): Error {
+ const raw = err instanceof Error ? err.message : String(err);
+ if (!raw.includes('PV_INCOMPATIBLE')) {
+ // Network error, bad spec, etc. — pass through, just strip our tag.
+ return new Error(raw.replace(/PV_INSTALL_ERROR:\s*/, ''));
+ }
+ const detail = raw.split('PV_INCOMPATIBLE:').pop()?.trim() || raw;
+ return new Error(
+ `"${spec}" can't be installed in the PathView web app.\n` +
+ `\n` +
+ `The web version runs Python in your browser via Pyodide, which can\n` +
+ `only install pure-Python packages (or packages Pyodide ships\n` +
+ `pre-built). This toolbox needs compiled or native code that isn't\n` +
+ `available in the browser.\n` +
+ `\n` +
+ `To use it, install the standalone PathView desktop app:\n` +
+ ` pip install pathview\n` +
+ ` pathview\n` +
+ `It runs a real Python environment and can install any pip package.\n` +
+ `\n` +
+ `micropip: ${detail}`
+ );
+}
+
/**
* Install a package. Skips when `importPath` is given and the module is
* already importable (saves a round-trip + download).
@@ -94,8 +128,13 @@ export async function installPackage(spec: string, importPath?: string): Promise
}
const backend = getBackendType();
if (backend === 'pyodide') {
- // runPythonAsync supports top-level await for micropip.install
- await exec(`await _pv_install_micropip(${pyStr(spec)})`);
+ // runPythonAsync supports top-level await for micropip.install.
+ // Reframe Pyodide-incompatibility failures into an actionable hint.
+ try {
+ await exec(`await _pv_install_micropip(${pyStr(spec)})`);
+ } catch (e) {
+ throw reframePyodideInstallError(spec, e);
+ }
} else {
// Flask / remote: real CPython, use subprocess pip (sync)
await exec(`_pv_install_pip(${pyStr(spec)})`);
diff --git a/src/lib/toolbox/python.ts b/src/lib/toolbox/python.ts
index 2ebcac21..22c368fa 100644
--- a/src/lib/toolbox/python.ts
+++ b/src/lib/toolbox/python.ts
@@ -34,9 +34,29 @@ def _pv_already_installed(import_path):
async def _pv_install_micropip(spec):
- """Pyodide-side install via micropip (top-level await)."""
+ """Pyodide-side install via micropip (top-level await).
+
+ micropip can only install pure-Python wheels (or packages Pyodide
+ ships pre-built), so toolboxes with compiled/native code fail here
+ even though they install fine in the standalone (pip-backed) build.
+ On failure we classify the error and prefix it with PV_INCOMPATIBLE
+ (browser-runtime limitation) or PV_INSTALL_ERROR (genuine failure)
+ so the JS side can show a useful hint instead of a raw traceback."""
import micropip
- await micropip.install(spec, keep_going=True)
+ try:
+ await micropip.install(spec, keep_going=True)
+ except Exception as e:
+ msg = str(e)
+ low = msg.lower()
+ incompatible = (
+ "pure python" in low
+ or "can't find" in low
+ or "cannot find" in low
+ or "no matching distribution" in low
+ or "no known package" in low
+ )
+ tag = "PV_INCOMPATIBLE" if incompatible else "PV_INSTALL_ERROR"
+ raise RuntimeError(tag + ": " + msg)
return {"ok": True, "spec": spec, "via": "micropip"}