Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Surface artifact action errors and rename Download when local
Daniel Norman committed 7 days ago
commit 4ab0237ce91ca4bc3ad50730647b364627be0d3f
parent a74b5b4c9d139db7d391ce26edf99967287e0f0a
2 files changed +86 -6
modified crates/radicle-tauri/src/commands/cob/release.rs
@@ -274,6 +274,15 @@ pub async fn download_artifact(
        let _ = app.emit("artifact_progress", payload);
    };

+
    // Fast path: bytes already in the local iroh store. Skip every
+
    // network transport and just write them straight to disk.
+
    if iroh.blobs.blobs().has(hash).await.unwrap_or(false) {
+
        emit("writing", None);
+
        fetch::export(&iroh.blobs, hash, kind, &dest).await?;
+
        emit("done", None);
+
        return Ok(());
+
    }
+

    emit("connecting", None);

    let mut errors: Vec<String> = Vec::new();
modified src/views/repo/Release.svelte
@@ -8,7 +8,7 @@
  import { listen } from "@tauri-apps/api/event";
  import { onDestroy, onMount } from "svelte";

-
  import { invoke } from "@app/lib/invoke";
+
  import { invoke, InvokeError } from "@app/lib/invoke";
  import { isDelegateOrAuthor } from "@app/lib/roles";
  import * as router from "@app/lib/router";
  import { authorForNodeId } from "@app/lib/utils";
@@ -103,6 +103,30 @@
  const localShared = $state<Record<string, boolean>>({});
  const localAvailable = $state<Record<string, boolean>>({});
  const busy = $state<Record<string, boolean>>({});
+
  // Per-artifact error messages displayed inline under the action row.
+
  // Cleared when the user retries the same artifact's action.
+
  const actionErrors = $state<Record<string, string | undefined>>({});
+

+
  // Translate a backend error into something a user can act on. Falls
+
  // through to the raw message for codes we haven't styled yet.
+
  function describeError(err: unknown): string {
+
    if (err instanceof InvokeError) {
+
      if (err.code === "ArtifactError.CidMismatch") {
+
        return `${err.message}. Pick the original file that produced this CID.`;
+
      }
+
      if (err.code === "ArtifactError.NoLocations") {
+
        return "This artifact has no advertised locations to fetch from.";
+
      }
+
      if (err.code === "ArtifactError.NoProviders") {
+
        return "No iroh providers are currently reachable for this artifact.";
+
      }
+
      if (err.code === "ArtifactError.AllTransportsFailed") {
+
        return err.message;
+
      }
+
      return err.message;
+
    }
+
    return String(err);
+
  }
  const progress = $state<
    Record<string, { stage: string; bytes?: number } | undefined>
  >({});
@@ -157,6 +181,7 @@
    }
    if (!path) return;
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
    try {
      await invoke("seed_artifact", {
        rid: repo.rid,
@@ -168,6 +193,7 @@
      localAvailable[artifact.cid] = true;
    } catch (err) {
      console.error("seed failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
    } finally {
      busy[artifact.cid] = false;
    }
@@ -175,6 +201,7 @@

  async function unseed(artifact: Artifact) {
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
    try {
      await invoke("unseed_artifact", {
        rid: repo.rid,
@@ -185,6 +212,7 @@
      void refreshAvailability(artifact.cid);
    } catch (err) {
      console.error("unseed failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
    } finally {
      busy[artifact.cid] = false;
    }
@@ -200,6 +228,7 @@
    if (!dest) return;

    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
    try {
      await invoke("download_artifact", {
        rid: repo.rid,
@@ -209,6 +238,7 @@
      });
    } catch (err) {
      console.error("download failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
    } finally {
      busy[artifact.cid] = false;
    }
@@ -216,6 +246,7 @@

  async function attest(artifact: Artifact) {
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
    try {
      await invoke("attest_artifact", {
        rid: repo.rid,
@@ -225,6 +256,7 @@
      await refresh();
    } catch (err) {
      console.error("attest failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
    } finally {
      busy[artifact.cid] = false;
    }
@@ -238,6 +270,7 @@
    );
    if (reason === null) return;
    busy[artifact.cid] = true;
+
    actionErrors[artifact.cid] = undefined;
    try {
      await invoke("redact_artifact", {
        rid: repo.rid,
@@ -248,6 +281,7 @@
      await refresh();
    } catch (err) {
      console.error("redact failed", err);
+
      actionErrors[artifact.cid] = describeError(err);
    } finally {
      busy[artifact.cid] = false;
    }
@@ -498,6 +532,28 @@
    font: var(--txt-body-s-regular);
    color: var(--color-text-secondary);
  }
+
  .action-error {
+
    margin-top: 0.5rem;
+
    padding: 0.5rem 0.75rem;
+
    background-color: var(--color-feedback-error-bg);
+
    border: 1px solid var(--color-feedback-error-border);
+
    border-radius: var(--border-radius-sm);
+
    color: var(--color-feedback-error-text);
+
    font: var(--txt-body-s-regular);
+
    display: flex;
+
    align-items: flex-start;
+
    gap: 0.5rem;
+
  }
+
  .action-error .dismiss {
+
    margin-left: auto;
+
    background: none;
+
    border: none;
+
    cursor: pointer;
+
    color: inherit;
+
    line-height: 1;
+
    font-size: 1rem;
+
    padding: 0;
+
  }
  .empty {
    padding: 2rem;
    color: var(--color-text-secondary);
@@ -675,11 +731,14 @@
                <button
                  onclick={() => download(artifact)}
                  disabled={busy[artifact.cid] ||
-
                    artifact.locations.length === 0}
-
                  title={artifact.locations.length === 0
-
                    ? "No locations to download from"
-
                    : "Download to disk"}>
-
                  Download
+
                    (!isAvailableLocally(artifact) &&
+
                      artifact.locations.length === 0)}
+
                  title={isAvailableLocally(artifact)
+
                    ? "Export the locally-stored copy to disk"
+
                    : artifact.locations.length === 0
+
                      ? "No locations to download from"
+
                      : "Fetch from a peer and write to disk"}>
+
                  {isAvailableLocally(artifact) ? "Save to disk" : "Download"}
                </button>
                {#if isShared(artifact)}
                  <button
@@ -826,6 +885,18 @@
            {#if progress[artifact.cid]}
              <div class="progress">{progressText(artifact.cid)}</div>
            {/if}
+
            {#if actionErrors[artifact.cid]}
+
              <div class="action-error" role="alert">
+
                <Icon name="warning" />
+
                <span>{actionErrors[artifact.cid]}</span>
+
                <button
+
                  class="dismiss"
+
                  title="Dismiss"
+
                  onclick={() => (actionErrors[artifact.cid] = undefined)}>
+
                  ×
+
                </button>
+
              </div>
+
            {/if}
          </div>
        </div>
      {:else}