Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Show job cobs on latest revision and default branch commits
Rūdolfs Ošiņš committed 10 months ago
commit bfa38d9fe9015aef7999174583b3ea7bdcf0d6b4
parent 8632ea90fa5a42f0217ead15924bc31b18e5d774
21 files changed +539 -15
modified Cargo.lock
@@ -192,6 +192,56 @@ dependencies = [
]

[[package]]
+
name = "anstream"
+
version = "0.6.19"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
+
dependencies = [
+
 "anstyle",
+
 "anstyle-parse",
+
 "anstyle-query",
+
 "anstyle-wincon",
+
 "colorchoice",
+
 "is_terminal_polyfill",
+
 "utf8parse",
+
]
+

+
[[package]]
+
name = "anstyle"
+
version = "1.0.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+

+
[[package]]
+
name = "anstyle-parse"
+
version = "0.2.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+
dependencies = [
+
 "utf8parse",
+
]
+

+
[[package]]
+
name = "anstyle-query"
+
version = "1.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
+
dependencies = [
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
+
name = "anstyle-wincon"
+
version = "3.0.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
+
dependencies = [
+
 "anstyle",
+
 "once_cell_polyfill",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -802,6 +852,47 @@ dependencies = [
]

[[package]]
+
name = "clap"
+
version = "4.5.41"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
+
dependencies = [
+
 "clap_builder",
+
 "clap_derive",
+
]
+

+
[[package]]
+
name = "clap_builder"
+
version = "4.5.41"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
+
dependencies = [
+
 "anstream",
+
 "anstyle",
+
 "clap_lex",
+
 "strsim",
+
 "terminal_size",
+
]
+

+
[[package]]
+
name = "clap_derive"
+
version = "4.5.41"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
+
dependencies = [
+
 "heck 0.5.0",
+
 "proc-macro2",
+
 "quote",
+
 "syn 2.0.101",
+
]
+

+
[[package]]
+
name = "clap_lex"
+
version = "0.7.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+

+
[[package]]
name = "clipboard-win"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -811,6 +902,12 @@ dependencies = [
]

[[package]]
+
name = "colorchoice"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+

+
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2562,6 +2659,12 @@ dependencies = [
]

[[package]]
+
name = "is_terminal_polyfill"
+
version = "1.70.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+

+
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3038,6 +3141,12 @@ dependencies = [
]

[[package]]
+
name = "nonempty"
+
version = "0.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "549e471b99ccaf2f89101bec68f4d244457d5a95a9c3d0672e9564124397741d"
+

+
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3361,6 +3470,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"

[[package]]
+
name = "once_cell_polyfill"
+
version = "1.70.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+

+
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3952,7 +4067,7 @@ dependencies = [
 "localtime",
 "log",
 "multibase",
-
 "nonempty",
+
 "nonempty 0.9.0",
 "qcheck",
 "radicle-cob",
 "radicle-crypto",
@@ -3977,7 +4092,7 @@ dependencies = [
 "fastrand",
 "git2",
 "log",
-
 "nonempty",
+
 "nonempty 0.9.0",
 "once_cell",
 "radicle-crypto",
 "radicle-dag",
@@ -4034,6 +4149,25 @@ dependencies = [
]

[[package]]
+
name = "radicle-job"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "70b2de1bb748a1b587f759137058c4ae917753652b5fb4ae3baee991a7b31a68"
+
dependencies = [
+
 "clap",
+
 "indexmap 2.9.0",
+
 "nonempty 0.11.0",
+
 "once_cell",
+
 "qcheck",
+
 "radicle",
+
 "serde",
+
 "serde_json",
+
 "thiserror 2.0.12",
+
 "url",
+
 "uuid",
+
]
+

+
[[package]]
name = "radicle-ssh"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4062,7 +4196,7 @@ dependencies = [
 "flate2",
 "git2",
 "log",
-
 "nonempty",
+
 "nonempty 0.9.0",
 "radicle-git-ext",
 "radicle-std-ext",
 "serde",
@@ -4081,6 +4215,7 @@ dependencies = [
 "infer",
 "log",
 "radicle",
+
 "radicle-job",
 "radicle-surf",
 "radicle-types",
 "serde",
@@ -4111,6 +4246,7 @@ dependencies = [
 "log",
 "mime-infer",
 "radicle",
+
 "radicle-job",
 "radicle-surf",
 "serde",
 "serde_json",
@@ -5775,6 +5911,16 @@ dependencies = [
]

[[package]]
+
name = "terminal_size"
+
version = "0.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
+
dependencies = [
+
 "rustix 1.0.7",
+
 "windows-sys 0.59.0",
+
]
+

+
[[package]]
name = "test-http-api"
version = "0.1.0"
dependencies = [
@@ -6533,13 +6679,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"

[[package]]
+
name = "utf8parse"
+
version = "0.2.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+

+
[[package]]
name = "uuid"
-
version = "1.16.0"
+
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
+
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
 "getrandom 0.3.3",
+
 "js-sys",
 "serde",
+
 "wasm-bindgen",
]

[[package]]
modified crates/radicle-tauri/Cargo.toml
@@ -21,6 +21,7 @@ either = { version = "1.15" }
infer = { version = "0.19.0" }
log = { version = "0.4.22" }
radicle = { version = "0.16.1" }
+
radicle-job = { version = "0.2.0" }
radicle-types = { version = "0.1.0", path = "../radicle-types" }
radicle-surf = { version = "0.22.1", features = ["serde"] }
serde = { version = "1.0.0", features = ["derive"] }
modified crates/radicle-tauri/src/commands/cob.rs
@@ -11,6 +11,7 @@ use tauri_plugin_dialog::DialogExt;
use crate::AppState;

pub mod issue;
+
pub mod job;
pub mod patch;

#[tauri::command]
added crates/radicle-tauri/src/commands/cob/job.rs
@@ -0,0 +1,16 @@
+
use radicle::git;
+
use radicle::identity::RepoId;
+
use radicle_types::cobs::job;
+
use radicle_types::error::Error;
+
use radicle_types::traits::job::Jobs;
+

+
use crate::AppState;
+

+
#[tauri::command]
+
pub fn list_jobs(
+
    ctx: tauri::State<AppState>,
+
    rid: RepoId,
+
    sha: git::Oid,
+
) -> Result<Vec<job::Job>, Error> {
+
    ctx.list_jobs(rid, sha)
+
}
modified crates/radicle-tauri/src/lib.rs
@@ -32,6 +32,7 @@ pub fn run() {
            cob::issue::issue_by_id,
            cob::issue::list_issues,
            cob::issue::rebuild_issue_cache,
+
            cob::job::list_jobs,
            cob::patch::activity_by_patch,
            cob::patch::edit_patch,
            cob::patch::list_patches,
modified crates/radicle-types/Cargo.toml
@@ -12,6 +12,7 @@ log = { version = "0.4.22" }
infer = { version = "0.19.0" }
mime-infer = { version = "3.0.0" }
radicle = { version = "0.16.1" }
+
radicle-job = { version = "0.2.0" }
radicle-surf = { version = "0.22.1", features = ["serde"] }
serde = { version = "1.0.0", features = ["derive"] }
serde_json = { version = "1.0.0" }
added crates/radicle-types/bindings/repo/Job.ts
@@ -0,0 +1,4 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Run } from "./Run";
+

+
export type Job = { jobId: string; commit: string; runs: Array<Run> };
added crates/radicle-types/bindings/repo/Run.ts
@@ -0,0 +1,5 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../cob/Author";
+
import type { Status } from "./Status";
+

+
export type Run = { runId: string; node: Author; status: Status; log: string };
added crates/radicle-types/bindings/repo/Status.ts
@@ -0,0 +1,3 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type Status = "started" | "failed" | "succeeded";
modified crates/radicle-types/src/cobs.rs
@@ -10,6 +10,7 @@ use radicle::node::{Alias, AliasStore};

pub mod diff;
pub mod issue;
+
pub mod job;
pub mod repo;
pub mod stream;
pub mod thread;
added crates/radicle-types/src/cobs/job.rs
@@ -0,0 +1,80 @@
+
use radicle::git;
+
use radicle::node::AliasStore;
+
use serde::Serialize;
+
use ts_rs::TS;
+
use url::Url;
+

+
use crate::cobs;
+

+
#[derive(Clone, Serialize, TS, Debug)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
+
pub struct Job {
+
    #[ts(as = "String")]
+
    pub job_id: radicle_job::JobId,
+
    #[ts(as = "String")]
+
    pub commit: git::Oid,
+
    pub runs: Vec<Run>,
+
}
+

+
impl PartialEq for Job {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.job_id == other.job_id && self.commit == other.commit && self.runs == other.runs
+
    }
+
}
+

+
impl Job {
+
    pub fn new(id: radicle_job::JobId, job: &radicle_job::Job, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            job_id: id,
+
            commit: *job.oid(),
+
            runs: job
+
                .runs()
+
                .iter()
+
                .flat_map(|(node_id, runs)| {
+
                    runs.iter().map(move |(run_id, run)| Run {
+
                        run_id: run_id.to_string(),
+
                        node: cobs::Author::new(&(*node_id).into(), aliases),
+
                        status: (*run.status()).into(),
+
                        log: run.log().clone(),
+
                    })
+
                })
+
                .collect(),
+
        }
+
    }
+
}
+

+
#[derive(Clone, Serialize, TS, Debug, PartialEq)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
+
pub struct Run {
+
    pub run_id: String,
+
    pub node: cobs::Author,
+
    pub status: Status,
+
    #[ts(as = "String")]
+
    pub log: Url,
+
}
+

+
#[derive(Clone, Serialize, TS, Debug, PartialEq)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
+
pub enum Status {
+
    Started,
+
    Failed,
+
    Succeeded,
+
}
+

+
impl From<radicle_job::Status> for Status {
+
    fn from(value: radicle_job::Status) -> Self {
+
        match value {
+
            radicle_job::Status::Started => Self::Started,
+
            radicle_job::Status::Finished(reason) => match reason {
+
                radicle_job::Reason::Failed => Self::Failed,
+
                radicle_job::Reason::Succeeded => Self::Succeeded,
+
            },
+
        }
+
    }
+
}
modified crates/radicle-types/src/lib.rs
@@ -1,5 +1,6 @@
use traits::cobs::Cobs;
use traits::issue::{Issues, IssuesMut};
+
use traits::job::Jobs;
use traits::patch::{Patches, PatchesMut};
use traits::repo::Repo;
use traits::thread::Thread;
@@ -27,6 +28,7 @@ impl Thread for AppState {}
impl Cobs for AppState {}
impl Issues for AppState {}
impl IssuesMut for AppState {}
+
impl Jobs for AppState {}
impl Patches for AppState {}
impl PatchesMut for AppState {}
impl Profile for AppState {
modified crates/radicle-types/src/traits.rs
@@ -4,6 +4,7 @@ use crate::config::Config;

pub mod cobs;
pub mod issue;
+
pub mod job;
pub mod patch;
pub mod repo;
pub mod thread;
added crates/radicle-types/src/traits/job.rs
@@ -0,0 +1,23 @@
+
use radicle::storage::ReadStorage;
+
use radicle::{git, identity};
+
use radicle_job::{Job, JobId, Jobs as JobsStore};
+

+
use crate::cobs::job;
+
use crate::error::Error;
+
use crate::traits::Profile;
+

+
pub trait Jobs: Profile {
+
    fn list_jobs(&self, rid: identity::RepoId, sha: git::Oid) -> Result<Vec<job::Job>, Error> {
+
        let profile = self.profile();
+
        let repo = profile.storage.repository(rid)?;
+
        let aliases = &profile.aliases();
+

+
        let jobs = JobsStore::open(&repo).unwrap();
+
        let found_jobs: Result<Vec<(JobId, Job)>, _> = jobs.find_by_commit(sha)?.collect();
+

+
        Ok(found_jobs?
+
            .into_iter()
+
            .map(|(id, job)| job::Job::new(id, &job, aliases))
+
            .collect())
+
    }
+
}
modified crates/test-http-api/src/api.rs
@@ -25,6 +25,7 @@ use radicle_types::error::Error;
use radicle_types::outbound::sqlite::Sqlite;
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::issue::{Issues, IssuesMut};
+
use radicle_types::traits::job::Jobs;
use radicle_types::traits::patch::{Patches, PatchesMut};
use radicle_types::traits::repo::{Repo, Show};
use radicle_types::traits::thread::Thread;
@@ -41,6 +42,7 @@ impl Cobs for Context {}
impl Thread for Context {}
impl Issues for Context {}
impl IssuesMut for Context {}
+
impl Jobs for Context {}
impl Patches for Context {}
impl PatchesMut for Context {}
impl Profile for Context {
@@ -90,6 +92,7 @@ pub fn router(ctx: Context) -> Router {
        .route("/save_embed_by_clipboard", post(save_embed_handler))
        .route("/save_embed_by_bytes", post(save_embed_handler))
        .route("/save_embed_to_disk", post(save_embed_handler))
+
        .route("/list_jobs", post(jobs_handler))
        .layer(
            CorsLayer::new()
                .allow_origin(cors::Any)
@@ -470,3 +473,18 @@ async fn revision_handler(

    Ok::<_, Error>(Json(revisions))
}
+

+
#[derive(Serialize, Deserialize)]
+
struct JobsBody {
+
    pub rid: identity::RepoId,
+
    pub sha: git::Oid,
+
}
+

+
async fn jobs_handler(
+
    State(ctx): State<Context>,
+
    Json(JobsBody { rid, sha }): Json<JobsBody>,
+
) -> impl IntoResponse {
+
    let jobs = ctx.list_jobs(rid, sha)?;
+

+
    Ok::<_, Error>(Json(jobs))
+
}
modified src/components/Changes.svelte
@@ -10,6 +10,7 @@
  import CommitsContainer from "@app/components/CommitsContainer.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
+
  import JobCob from "@app/components/JobCob.svelte";
  import NakedButton from "@app/components/NakedButton.svelte";

  interface Props {
@@ -175,7 +176,7 @@
            <div class="global-counter">Base</div>
          </div>
          <div class="commits">
-
            {#each [...commits].reverse() as commit}
+
            {#each [...commits].reverse() as commit, idx}
              <div
                class="commit"
                class:single-commit={commits.length === 1}
@@ -196,7 +197,11 @@
                      commitId: commit.id,
                    });
                  }}
-
                  {commit} />
+
                  {commit}>
+
                  {#if idx === commits.length - 1}
+
                    <JobCob {rid} commit={commit.id} />
+
                  {/if}
+
                </CobCommitTeaser>
              </div>
            {/each}
          </div>
modified src/components/CobCommitTeaser.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
  import type { Commit } from "@bindings/repo/Commit";
+
  import type { Snippet } from "svelte";

  import { twemoji } from "@app/lib/utils";

@@ -10,13 +11,20 @@
  import NakedButton from "@app/components/NakedButton.svelte";

  interface Props {
-
    disabled: boolean;
+
    children?: Snippet;
    commit: Commit;
-
    onclick: () => void;
+
    disabled: boolean;
    hoverable?: boolean;
+
    onclick: () => void;
  }

-
  const { disabled, hoverable = false, commit, onclick }: Props = $props();
+
  const {
+
    children,
+
    commit,
+
    disabled,
+
    hoverable = false,
+
    onclick,
+
  }: Props = $props();

  let commitMessageVisible = $state(false);
</script>
@@ -99,6 +107,7 @@
    {/if}
  </div>
  <div class="right">
+
    {@render children?.()}
    <CompactCommitAuthorship {commit}>
      <Id
        id={commit.id}
modified src/components/HoverPopover.svelte
@@ -5,8 +5,10 @@

  interface Props {
    popover: Snippet;
-
    stylePopoverPositionBottom: string | undefined;
-
    stylePopoverPositionLeft: string | undefined;
+
    stylePopoverPositionBottom?: string | undefined;
+
    stylePopoverPositionLeft?: string | undefined;
+
    stylePopoverPositionRight?: string | undefined;
+
    stylePadding?: string | undefined;
    toggle: Snippet;
  }

@@ -14,6 +16,8 @@
    popover,
    stylePopoverPositionBottom,
    stylePopoverPositionLeft,
+
    stylePopoverPositionRight,
+
    stylePadding = "0.5rem 1rem",
    toggle,
  }: Props = $props();

@@ -31,8 +35,6 @@
  }
  .popover {
    background: var(--color-fill-ghost);
-
    border-radius: var(--border-radius-regular);
-
    padding: 0.5rem 1rem;
    box-shadow: var(--elevation-low);
    position: absolute;
    clip-path: var(--2px-corner-fill);
@@ -41,9 +43,13 @@
</style>

<div class="container">
+
  <!-- svelte-ignore a11y_click_events_have_key_events -->
  <div
    role="button"
    tabindex="0"
+
    onclick={e => {
+
      e.stopPropagation();
+
    }}
    onmouseenter={() => setVisible(true)}
    onmouseleave={() => setVisible(false)}>
    {@render toggle()}
@@ -52,7 +58,9 @@
      <div style:position="absolute">
        <div
          class="popover"
+
          style:padding={stylePadding}
          style:left={stylePopoverPositionLeft}
+
          style:right={stylePopoverPositionRight}
          style:bottom={stylePopoverPositionBottom}>
          {@render popover()}
        </div>
modified src/components/Icon.svelte
@@ -21,6 +21,7 @@
      | "checkbox-checked"
      | "checkbox-unchecked"
      | "checkmark"
+
      | "checkmark-double"
      | "checkout"
      | "chevron-down"
      | "chevron-right"
@@ -34,6 +35,7 @@
      | "comment-cross"
      | "copy"
      | "cross"
+
      | "cross-double"
      | "dashboard"
      | "delegate"
      | "diff"
@@ -47,6 +49,7 @@
      | "filter"
      | "folder-closed"
      | "folder-open"
+
      | "help"
      | "home"
      | "hourglass"
      | "inbox"
@@ -296,6 +299,23 @@
    <path d="M4 8V9H3L3 8H4Z" />
    <path d="M5 9L5 10L4 10L4 9H5Z" />
    <path d="M6 10L6 11H5L5 10L6 10Z" />
+
  {:else if name === "checkmark-double"}
+
    <path d="M6 10L6 11H5L5 10H6Z" />
+
    <path d="M8 10V11H7L7 10H8Z" />
+
    <path d="M7 9L7 10L6 10V9H7Z" />
+
    <path d="M9 9V10H8V9L9 9Z" />
+
    <path d="M8 8L8 9L7 9L7 8H8Z" />
+
    <path d="M10 8V9L9 9V8H10Z" />
+
    <path d="M9 7L9 8L8 8L8 7H9Z" />
+
    <path d="M11 7V8L10 8L10 7H11Z" />
+
    <path d="M10 6V7L9 7V6L10 6Z" />
+
    <path d="M12 6V7H11V6H12Z" />
+
    <path d="M11 5V6L10 6V5L11 5Z" />
+
    <path d="M13 5V6L12 6V5L13 5Z" />
+
    <path d="M4 8V9H3L3 8H4Z" />
+
    <path d="M6 8L6 9L5 9V8H6Z" />
+
    <path d="M5 9L5 10H4L4 9H5Z" />
+
    <path d="M7 9L7 10L6 10V9H7Z" />
  {:else if name === "checkout"}
    <path d="M5 5H11V6H5V5Z" />
    <path d="M4 6L5 6L5 11H4L4 6Z" />
@@ -522,6 +542,33 @@
    <path d="M6.00003 6L6.00003 7H7.00003L7.00003 6H6.00003Z" />
    <path d="M5.00003 5V6L6.00003 6V5L5.00003 5Z" />
    <path d="M4.00003 4L4.00003 5L5.00003 5L5.00003 4L4.00003 4Z" />
+
  {:else if name === "cross-double"}
+
    <path d="M4 11V12H3L3 11H4Z" />
+
    <path d="M5 10L5 11H4L4 10H5Z" />
+
    <path d="M6 9V10H5L5 9L6 9Z" />
+
    <path d="M7 7V8H6V7H7Z" />
+
    <path d="M10 5V6L9 6V5H10Z" />
+
    <path d="M11 4V5L10 5V4L11 4Z" />
+
    <path d="M9 10V11H10V10H9Z" />
+
    <path d="M10 11V12H11V11H10Z" />
+
    <path d="M8 9V10H9V9H8Z" />
+
    <path d="M7 8L7 9H8V8L7 8Z" />
+
    <path d="M5 6L5 7H6L6 6H5Z" />
+
    <path d="M4 5V6L5 6V5L4 5Z" />
+
    <path d="M3 4L3 5L4 5L4 4L3 4Z" />
+
    <path d="M6 11V12H5L5 11H6Z" />
+
    <path d="M7 10L7 11H6L6 10H7Z" />
+
    <path d="M9 7V8H8V7H9Z" />
+
    <path d="M11 6V7H10V6H11Z" />
+
    <path d="M12 5V6L11 6V5H12Z" />
+
    <path d="M13 4V5L12 5V4L13 4Z" />
+
    <path d="M11 10V11H12V10H11Z" />
+
    <path d="M12 11V12H13V11H12Z" />
+
    <path d="M10 9V10H11V9H10Z" />
+
    <path d="M9 8L9 9H10V8L9 8Z" />
+
    <path d="M7 6L7 7H8L8 6H7Z" />
+
    <path d="M6 5V6L7 6V5L6 5Z" />
+
    <path d="M5 4L5 5L6 5L6 4L5 4Z" />
  {:else if name === "dashboard"}
    <path d="M2 11H14V12H2V11Z" />
    <path d="M2 9H3V11H2L2 9Z" />
@@ -721,6 +768,32 @@
    <path d="M4.5 8V10H3.5L3.5 8H4.5Z" />
    <path d="M14.5 8L14.5 10H13.5L13.5 8H14.5Z" />
    <path d="M11.5 12H12.5V13H11.5V12Z" />
+
  {:else if name === "help"}
+
    <path d="M9 12H8L8 14H9L9 12Z" />
+
    <path d="M8 12H7V14H8L8 12Z" />
+
    <path d="M11 5L11 7H12V5H11Z" />
+
    <path d="M11 5L11 7H12V5H11Z" />
+
    <path d="M10 5L10 7L11 7L11 5L10 5Z" />
+
    <path d="M5 4L5 6H4L4 4H5Z" />
+
    <path d="M6 4V6H5L5 4H6Z" />
+
    <path d="M11 4V5H12V4H11Z" />
+
    <path d="M10 4V5L11 5V4L10 4Z" />
+
    <path d="M9 8V9H10V8H9Z" />
+
    <path d="M8 9V10H9V9L8 9Z" />
+
    <path d="M8 10V11H9V10H8Z" />
+
    <path d="M6 4H5L5 3L6 3L6 4Z" />
+
    <path d="M9 4L11 4V3L9 3V4Z" />
+
    <path d="M11 6V7H12V6H11Z" />
+
    <path d="M10 6V7L11 7V6L10 6Z" />
+
    <path d="M9 7L9 8H10L10 7H9Z" />
+
    <path d="M8 8V9L9 9V8H8Z" />
+
    <path d="M7 9V10L8 10V9L7 9Z" />
+
    <path d="M7 10L7 11L8 11V10L7 10Z" />
+
    <path d="M10 7L10 8H11V7L10 7Z" />
+
    <path d="M7 3L6 3L6 2L7 2V3Z" />
+
    <path d="M7 4H6L6 3L7 3V4Z" />
+
    <path d="M7 3L9 3V2L7 2V3Z" />
+
    <path d="M7 4L9 4V3L7 3V4Z" />
  {:else if name === "home"}
    <path d="M7 1.50003H9V2.50003H7V1.50003Z" />
    <path d="M6 2.50003L7 2.50003V3.50003H6V2.50003Z" />
added src/components/JobCob.svelte
@@ -0,0 +1,115 @@
+
<script lang="ts">
+
  import type { Job } from "@bindings/repo/Job";
+

+
  import { invoke } from "@app/lib/invoke";
+
  import { authorForNodeId } from "@app/lib/utils";
+

+
  import HoverPopover from "@app/components/HoverPopover.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+

+
  import DropdownListItem from "./DropdownListItem.svelte";
+
  import NodeId from "./NodeId.svelte";
+

+
  interface Props {
+
    commit: string;
+
    rid: string;
+
  }
+

+
  const { commit, rid }: Props = $props();
+
</script>
+

+
<style>
+
  .status {
+
    gap: 0.5rem;
+
    width: fit-content;
+
  }
+
</style>
+

+
{#await invoke<Job[]>("list_jobs", { rid, sha: commit }) then jobs}
+
  {#if jobs.length > 0}
+
    <HoverPopover
+
      stylePadding="0.25rem"
+
      stylePopoverPositionBottom="2rem"
+
      stylePopoverPositionRight="-1.5rem">
+
      {#snippet toggle()}
+
        {#if jobs.every(j => {
+
          return j.runs.every(r => {
+
            return r.status === "succeeded";
+
          });
+
        })}
+
          <div
+
            class="global-counter"
+
            style:padding="0"
+
            style:color="var(--color-fill-success)"
+
            style:background-color="var(--color-fill-diff-green)">
+
            <Icon name="checkmark-double" />
+
          </div>
+
        {:else if jobs.every(j => {
+
          return j.runs.every(r => {
+
            return r.status === "failed";
+
          });
+
        })}
+
          <div
+
            class="global-counter"
+
            style:color="var(--color-foreground-red)"
+
            style:padding="0"
+
            style:background-color="var(--color-fill-diff-red)">
+
            <Icon name="cross-double" />
+
          </div>
+
        {:else}
+
          <div
+
            class="global-counter"
+
            style:padding="0"
+
            style:color="var(--color-fill-gray)"
+
            style:background-color="var(--color-fill-ghost)">
+
            <Icon name="help" />
+
          </div>
+
        {/if}
+
      {/snippet}
+

+
      {#snippet popover()}
+
        {#each jobs as job}
+
          {#each job.runs as run}
+
            <a
+
              style:text-decoration="none"
+
              style:cursor="pointer"
+
              style:width="100%"
+
              href={run.log}
+
              target="_blank">
+
              <DropdownListItem styleGap="0.5rem" selected={true}>
+
                {#if run.status === "started"}
+
                  <div
+
                    class="global-counter status"
+
                    style:background-color="var(--color-fill-float)"
+
                    style:color="var(--color-fill-gray)">
+
                    <Icon name="hourglass" /> Started
+
                  </div>
+
                {:else if run.status === "failed"}
+
                  <div
+
                    class="global-counter status"
+
                    style:color="var(--color-foreground-red)"
+
                    style:background-color="var(--color-fill-diff-red)">
+
                    <Icon name="cross" /> Failed
+
                  </div>
+
                {:else if run.status === "succeeded"}
+
                  <div
+
                    class="global-counter status"
+
                    style:color="var(--color-fill-success)"
+
                    style:background-color="var(--color-fill-diff-green)">
+
                    <Icon name="checkmark" /> Passed
+
                  </div>
+
                {/if}
+
                <NodeId {...authorForNodeId(run.node)} />
+
                <div
+
                  style:margin-left="auto"
+
                  style:color="var(--color-fill-gray)">
+
                  <Icon name="open-external" />
+
                </div>
+
              </DropdownListItem>
+
            </a>
+
          {/each}
+
        {/each}
+
      {/snippet}
+
    </HoverPopover>
+
  {/if}
+
{/await}
modified src/components/RepoMetadata.svelte
@@ -11,6 +11,8 @@
  import NodeId from "@app/components/NodeId.svelte";
  import VisibilityBadge from "@app/components/VisibilityBadge.svelte";

+
  import JobCob from "./JobCob.svelte";
+

  interface Props {
    horizontal?: boolean;
    repo: RepoInfo;
@@ -111,13 +113,14 @@
    style:width="100%">
    <div class="metadata-section">
      <div class="metadata-section-title">Default branch</div>
-
      <span>
+
      <span class="global-flex">
        <span class="txt-selectable">{project.data.defaultBranch}</span>
        ->
        <Id
          id={project.meta.head}
          clipboard={project.meta.head}
          variant="commit" />
+
        <JobCob rid={repo.rid} commit={project.meta.head} />
      </span>
    </div>