tinyhumansai/openhuman · info

NOT_DESKTOP

NOT_DESKTOP

Error message

Downloads are only available in the desktop app

What it means

downloadArtifact first checks isTauri(); in a plain browser context (Vite dev server at pnpm dev, or a web deployment) there is no Tauri shell to invoke the 'download_artifact_to_downloads' command, so it returns { ok: false, code: 'NOT_DESKTOP', error: 'Downloads are only available in the desktop app' } without attempting any RPC. This is an environment capability gate, not a failure - it fires deterministically whenever the same UI runs outside the desktop host.

Source

Thrown at app/src/services/artifactDownloadService.ts:238

  // requested extension (case-insensitive, with any other extension also
  // tolerated), don't append again. Prevents `deck.pptx.pptx` when the
  // persisted title is `deck.pptx` and the caller passes `'pptx'`.
  const titleHasExtension = /\.[^./\\]+$/.test(title);
  const titleHasSameExt = ext.length > 0 && title.toLowerCase().endsWith(`.${ext.toLowerCase()}`);
  const filename = ext && !titleHasExtension && !titleHasSameExt ? `${title}.${ext}` : title;

  return { ok: true, sourcePath, filename };
}

export async function downloadArtifact(
  artifactId: string,
  fallbackTitle: string,
  extension: string
): Promise<DownloadArtifactOutcome> {
  if (!isTauri()) {
    return {
      ok: false,
      code: 'NOT_DESKTOP',
      error: 'Downloads are only available in the desktop app',
    };
  }

  const resolved = await resolveArtifactForExport(artifactId, fallbackTitle, extension);
  if (!resolved.ok) {
    return { ok: false, code: resolved.code, error: resolved.error };
  }

  try {
    const dest = await invoke<string>('download_artifact_to_downloads', {
      sourcePath: resolved.sourcePath,
      filename: resolved.filename,
    });
    return { ok: true, path: dest };
  } catch (err) {
    const reason = err instanceof Error ? err.message : String(err);
    return { ok: false, code: 'DOWNLOAD_FAILED', error: reason };

View on GitHub (pinned to 7491200858)

Solutions

  1. Run the full desktop app (pnpm dev:app / installed build) for download flows
  2. In UI code, hide or disable download buttons when !isTauri() so the state is unreachable
  3. Treat the code field, not the message string, as the branch condition if you need to react to it

Example fix

// before - always render the download button
<DownloadButton onClick={() => void downloadArtifact(id, title, 'md')} />

// after - only offer it where it can work
{isTauri() && (
  <DownloadButton onClick={() => void downloadArtifact(id, title, 'md')} />
)}
Defensive patterns

Strategy: type-guard

Validate before calling

// Gate the affordance on the environment before the service is called
import { isTauri } from '<tauri guard util>';
if (!isTauri()) {
  // hide the download button or show 'desktop app only' hint
}

Type guard

const isDesktop = (): boolean => isTauri();

// NOT_DESKTOP is deterministic outside Tauri - the type guard IS the environment check
const isNotDesktop = (o: DownloadArtifactOutcome): o is { ok: false; code: 'NOT_DESKTOP'; error: string } =>
  !o.ok && o.code === 'NOT_DESKTOP';

Prevention

When it happens

Trigger: Opening the app in a browser via the Vite dev server instead of the Tauri shell (pnpm dev vs pnpm dev:app), or any web-only deployment, then clicking an artifact download / Save-As affordance.

Common situations: Frontend development in the browser where core RPC proxies exist over HTTP but Tauri invoke() does not; testing shared components in Storybook/Vitest (jsdom has no Tauri); users hitting a hosted preview of the UI.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/c232cc492940a626. Report an issue: GitHub.