Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Refactor types and add edit patch command
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago

This commit touches a few things, but ended up too broad to split up into separate commits.

  • Improves the hierarchy of types into separate folders
  • Add edit_patch tauri command similar to edit_issue.
  • Improve the types generation making more structs and less serde_json
  • Adding more optional values to the tauri commands

check

👉 Workflow runs 👉 Branch on GitHub

94 files changed +1835 -1122 0bdf0e71 5277028e
modified Cargo.lock
@@ -4017,6 +4017,7 @@ dependencies = [
 "radicle-surf",
 "serde",
 "serde_json",
+
 "thiserror",
 "ts-rs",
]

@@ -5388,18 +5389,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"

[[package]]
name = "thiserror"
-
version = "1.0.64"
+
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
+
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
 "thiserror-impl",
]

[[package]]
name = "thiserror-impl"
-
version = "1.0.64"
+
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
+
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
 "proc-macro2",
 "quote",
deleted crates/radicle-tauri/bindings/Author.ts
@@ -1,3 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type Author = { did: string; alias?: string };
deleted crates/radicle-tauri/bindings/CobOptions.ts
@@ -1,3 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type CobOptions = { announce?: boolean };
deleted crates/radicle-tauri/bindings/CodeLocation.ts
@@ -1,9 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { CodeRange } from "./CodeRange";
-

-
export type CodeLocation = {
-
  commit: string;
-
  path: string;
-
  old: CodeRange | null;
-
  new: CodeRange | null;
-
};
deleted crates/radicle-tauri/bindings/CodeRange.ts
@@ -1,5 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type CodeRange =
-
  | { type: "lines"; range: { start: number; end: number } }
-
  | { type: "chars"; line: number; range: { start: number; end: number } };
deleted crates/radicle-tauri/bindings/Comment.ts
@@ -1,15 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-
import type { Edit } from "./Edit";
-
import type { Reaction } from "./Reaction";
-

-
export type Comment<T> = {
-
  id: string;
-
  author: Author;
-
  edits: Array<Edit>;
-
  reactions: Array<Reaction>;
-
  replyTo?: string;
-
  location?: T;
-
  embeds: { name: string; content: string }[];
-
  resolved: boolean;
-
};
deleted crates/radicle-tauri/bindings/Config.ts
@@ -1,21 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
/**
-
 * Service configuration.
-
 */
-
export type Config = {
-
  /**
-
   * Node Public Key in NID format.
-
   */
-
  publicKey: string;
-
  /**
-
   * Node alias.
-
   */
-
  alias: string;
-
  /**
-
   * Default seeding policy.
-
   */
-
  seedingPolicy:
-
    | { default: "allow"; scope: "followed" | "all" }
-
    | { default: "block" };
-
};
deleted crates/radicle-tauri/bindings/CreateReviewComment.ts
@@ -1,10 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { CodeLocation } from "./CodeLocation";
-

-
export type CreateReviewComment = {
-
  reviewId: string;
-
  body: string;
-
  replyTo: string | null;
-
  location: CodeLocation | null;
-
  embeds: { name: string; content: string }[];
-
};
deleted crates/radicle-tauri/bindings/Edit.ts
@@ -1,9 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-

-
export type Edit = {
-
  author: Author;
-
  timestamp: number;
-
  body: string;
-
  embeds: { name: string; content: string };
-
};
deleted crates/radicle-tauri/bindings/Issue.ts
@@ -1,15 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-
import type { Comment } from "./Comment";
-
import type { Never } from "./Never";
-

-
export type Issue = {
-
  id: string;
-
  author: Author;
-
  title: string;
-
  state: { status: "closed"; reason: "other" | "solved" } | { status: "open" };
-
  assignees: Array<Author>;
-
  discussion: Array<Comment<Never>>;
-
  labels: Array<string>;
-
  timestamp: number;
-
};
deleted crates/radicle-tauri/bindings/IssueAction.ts
@@ -1,46 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type IssueAction =
-
  | { type: "assign"; assignees: Array<string> }
-
  | { type: "edit"; title: string }
-
  | {
-
      type: "lifecycle";
-
      state:
-
        | { status: "closed"; reason: "other" | "solved" }
-
        | { status: "open" };
-
    }
-
  | { type: "label"; labels: Array<string> }
-
  | {
-
      type: "comment";
-
      /**
-
       * Comment body.
-
       */
-
      body: string;
-
      /**
-
       * Comment this is a reply to.
-
       * Should be [`None` | `null`] if it's the top-level comment.
-
       * Should be the root [`CommentId`] if it's a top-level comment.
-
       */
-
      replyTo: string | null;
-
      /**
-
       * Embeded content.
-
       */
-
      embeds: Array<string>;
-
    }
-
  | {
-
      type: "comment.edit";
-
      /**
-
       * Comment being edited.
-
       */
-
      id: string;
-
      /**
-
       * New value for the comment body.
-
       */
-
      body: string;
-
      /**
-
       * New value for the embeds list.
-
       */
-
      embeds: Array<string>;
-
    }
-
  | { type: "comment.redact"; id: string }
-
  | { type: "comment.react"; id: string; reaction: string; active: boolean };
deleted crates/radicle-tauri/bindings/IssueOp.ts
@@ -1,48 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-

-
export type IssueOp = { entryId: string; timestamp: number; author: Author } & (
-
  | { type: "assign"; assignees: Array<string> }
-
  | { type: "edit"; title: string }
-
  | {
-
      type: "lifecycle";
-
      state:
-
        | { status: "closed"; reason: "other" | "solved" }
-
        | { status: "open" };
-
    }
-
  | { type: "label"; labels: Array<string> }
-
  | {
-
      type: "comment";
-
      /**
-
       * Comment body.
-
       */
-
      body: string;
-
      /**
-
       * Comment this is a reply to.
-
       * Should be [`None` | `null`] if it's the top-level comment.
-
       * Should be the root [`CommentId`] if it's a top-level comment.
-
       */
-
      replyTo: string | null;
-
      /**
-
       * Embeded content.
-
       */
-
      embeds: Array<string>;
-
    }
-
  | {
-
      type: "comment.edit";
-
      /**
-
       * Comment being edited.
-
       */
-
      id: string;
-
      /**
-
       * New value for the comment body.
-
       */
-
      body: string;
-
      /**
-
       * New value for the embeds list.
-
       */
-
      embeds: Array<string>;
-
    }
-
  | { type: "comment.redact"; id: string }
-
  | { type: "comment.react"; id: string; reaction: string; active: boolean }
-
);
deleted crates/radicle-tauri/bindings/Never.ts
@@ -1,8 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
/**
-
 * The `Infallible` type does not have a `Serialize`/`Deserialize`
-
 * implementation. The `Never` type imitates `Infallible` and
-
 * provides the derived implementations.
-
 */
-
export type Never = never;
deleted crates/radicle-tauri/bindings/NewIssue.ts
@@ -1,9 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type NewIssue = {
-
  title: string;
-
  description: string;
-
  labels: Array<string>;
-
  assignees: Array<string>;
-
  embeds: { name: string; content: string }[];
-
};
deleted crates/radicle-tauri/bindings/NewIssueComment.ts
@@ -1,8 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type NewIssueComment = {
-
  id: string;
-
  body: string;
-
  replyTo?: string;
-
  embeds: { name: string; content: string }[];
-
};
deleted crates/radicle-tauri/bindings/NewPatchComment.ts
@@ -1,11 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { CodeLocation } from "./CodeLocation";
-

-
export type NewPatchComment = {
-
  id: string;
-
  revision: string;
-
  body: string;
-
  replyTo?: string;
-
  location: CodeLocation | null;
-
  embeds: { name: string; content: string }[];
-
};
deleted crates/radicle-tauri/bindings/PaginatedQuery.ts
@@ -1,3 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type PaginatedQuery<T> = { cursor: number; more: boolean; content: T };
deleted crates/radicle-tauri/bindings/Patch.ts
@@ -1,30 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-

-
export type Patch = {
-
  id: string;
-
  author: Author;
-
  title: string;
-
  base: string;
-
  head: string;
-
  state:
-
    | {
-
        status: "draft";
-
      }
-
    | {
-
        status: "open";
-
        conflicts: [string, string][];
-
      }
-
    | {
-
        status: "archived";
-
      }
-
    | {
-
        status: "merged";
-
        revision: string;
-
        commit: string;
-
      };
-
  assignees: Array<Author>;
-
  labels: Array<string>;
-
  timestamp: number;
-
  revisionCount: number;
-
};
deleted crates/radicle-tauri/bindings/Reaction.ts
@@ -1,9 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-
import type { CodeLocation } from "./CodeLocation";
-

-
export type Reaction = {
-
  emoji: string;
-
  authors: Array<Author>;
-
  location?: CodeLocation;
-
};
deleted crates/radicle-tauri/bindings/RepoInfo.ts
@@ -1,15 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-
import type { SupportedPayloads } from "./SupportedPayloads";
-

-
/**
-
 * Repos info.
-
 */
-
export type RepoInfo = {
-
  payloads: SupportedPayloads;
-
  delegates: Array<Author>;
-
  threshold: number;
-
  visibility: { type: "public" } | { type: "private"; allow?: string[] };
-
  rid: string;
-
  seeding: number;
-
};
deleted crates/radicle-tauri/bindings/Review.ts
@@ -1,13 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-
import type { CodeLocation } from "./CodeLocation";
-
import type { Comment } from "./Comment";
-

-
export type Review = {
-
  id: string;
-
  author: Author;
-
  verdict?: "accept" | "reject";
-
  summary: string | null;
-
  comments: Array<Comment<CodeLocation>>;
-
  timestamp: number;
-
};
deleted crates/radicle-tauri/bindings/ReviewEdit.ts
@@ -1,8 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type ReviewEdit = {
-
  reviewId: string;
-
  verdict: string | null;
-
  summary: string | null;
-
  labels: Array<string>;
-
};
deleted crates/radicle-tauri/bindings/Revision.ts
@@ -1,19 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-
import type { Author } from "./Author";
-
import type { CodeLocation } from "./CodeLocation";
-
import type { Comment } from "./Comment";
-
import type { Edit } from "./Edit";
-
import type { Reaction } from "./Reaction";
-
import type { Review } from "./Review";
-

-
export type Revision = {
-
  id: string;
-
  author: Author;
-
  description: Array<Edit>;
-
  base: string;
-
  head: string;
-
  reviews: Array<Review>;
-
  timestamp: number;
-
  discussion: Array<Comment<CodeLocation>>;
-
  reactions: Array<Reaction>;
-
};
deleted crates/radicle-tauri/bindings/Stats.ts
@@ -1,7 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type Stats = {
-
  files_changed: number;
-
  insertions: number;
-
  deletions: number;
-
};
deleted crates/radicle-tauri/bindings/SupportedPayloads.ts
@@ -1,25 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type SupportedPayloads = {
-
  "xyz.radicle.project"?: {
-
    data: {
-
      defaultBranch: string;
-
      description: string;
-
      name: string;
-
    };
-
    meta: {
-
      head: string;
-
      issues: {
-
        open: number;
-
        closed: number;
-
      };
-
      patches: {
-
        open: number;
-
        draft: number;
-
        archived: number;
-
        merged: number;
-
      };
-
      lastCommit: number;
-
    };
-
  };
-
};
deleted crates/radicle-tauri/bindings/serde_json/JsonValue.ts
@@ -1,7 +0,0 @@
-
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-

-
export type JsonValue =
-
  | number
-
  | string
-
  | Array<JsonValue>
-
  | { [key: string]: JsonValue };
modified crates/radicle-tauri/src/commands/cob.rs
@@ -5,7 +5,6 @@ use radicle::git;
use radicle::identity;
use radicle::storage::{ReadRepository, ReadStorage};
use radicle_types as types;
-
use radicle_types::cobs::IssueAction;

use crate::{error, AppState};

@@ -13,19 +12,6 @@ pub mod draft;
pub mod issue;
pub mod patch;

-
#[derive(serde::Serialize, ts_rs::TS)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct IssueOp {
-
    #[ts(as = "String")]
-
    pub entry_id: git::Oid,
-
    #[serde(flatten)]
-
    pub action: types::cobs::IssueAction,
-
    #[ts(type = "number")]
-
    pub timestamp: cob::Timestamp,
-
    pub author: types::cobs::Author,
-
}
-

#[tauri::command]
pub async fn get_file_by_oid(
    ctx: tauri::State<'_, AppState>,
@@ -57,27 +43,25 @@ pub fn activity_by_id(
    rid: identity::RepoId,
    type_name: cob::TypeName,
    id: git::Oid,
-
) -> Result<Vec<IssueOp>, error::Error> {
+
) -> Result<Vec<types::cobs::issue::Operation>, error::Error> {
    let aliases = ctx.profile.aliases();
    let repo = ctx.profile.storage.repository(rid)?;
    let ops = cob::store::ops(&id.into(), &type_name, &repo).unwrap();
-
    let mut actions: Vec<IssueOp> = Vec::new();
+
    let mut actions: Vec<types::cobs::issue::Operation> = Vec::new();

    for op in ops.into_iter() {
-
        actions.extend(
-
            op.actions
-
                .iter()
-
                .filter_map(|action: &Vec<u8>| -> Option<IssueOp> {
-
                    let action: IssueAction = serde_json::from_slice(action).ok()?;
-

-
                    Some(IssueOp {
-
                        entry_id: op.id,
-
                        action,
-
                        author: types::cobs::Author::new(op.author.into(), &aliases),
-
                        timestamp: op.timestamp,
-
                    })
-
                }),
-
        )
+
        actions.extend(op.actions.iter().filter_map(
+
            |action: &Vec<u8>| -> Option<types::cobs::issue::Operation> {
+
                let action: types::cobs::issue::Action = serde_json::from_slice(action).ok()?;
+

+
                Some(types::cobs::issue::Operation {
+
                    entry_id: op.id,
+
                    action,
+
                    author: types::cobs::Author::new(op.author.into(), &aliases),
+
                    timestamp: op.timestamp,
+
                })
+
            },
+
        ))
    }

    Ok::<_, error::Error>(actions)
modified crates/radicle-tauri/src/commands/cob/issue.rs
@@ -1,10 +1,10 @@
use radicle::git;
-
use radicle::identity::RepoId;
+
use radicle::identity;
use radicle::issue::cache::Issues;
use radicle::node::Handle;
use radicle::node::Node;
use radicle::storage::ReadStorage;
-
use radicle_types::cobs;
+
use radicle_types as types;

use crate::cob::query;
use crate::error::Error;
@@ -13,10 +13,10 @@ use crate::AppState;
#[tauri::command]
pub fn create_issue(
    ctx: tauri::State<AppState>,
-
    rid: RepoId,
-
    new: cobs::NewIssue,
-
    opts: cobs::CobOptions,
-
) -> Result<cobs::Issue, Error> {
+
    rid: identity::RepoId,
+
    new: types::cobs::issue::NewIssue,
+
    opts: types::cobs::CobOptions,
+
) -> Result<types::cobs::issue::Issue, Error> {
    let mut node = Node::new(ctx.profile.socket());
    let repo = ctx.profile.storage.repository(rid)?;
    let signer = ctx.profile.signer()?;
@@ -27,7 +27,7 @@ pub fn create_issue(
        new.description,
        &new.labels,
        &new.assignees,
-
        new.embeds,
+
        new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
        &signer,
    )?;

@@ -35,17 +35,17 @@ pub fn create_issue(
        node.announce_refs(rid)?;
    }

-
    Ok::<_, Error>(cobs::Issue::new(issue.id(), &issue, &aliases))
+
    Ok::<_, Error>(types::cobs::issue::Issue::new(issue.id(), &issue, &aliases))
}

#[tauri::command]
pub fn edit_issue(
    ctx: tauri::State<AppState>,
-
    rid: RepoId,
+
    rid: identity::RepoId,
    cob_id: git::Oid,
-
    action: cobs::IssueAction,
-
    opts: cobs::CobOptions,
-
) -> Result<cobs::Issue, Error> {
+
    action: types::cobs::issue::Action,
+
    opts: types::cobs::CobOptions,
+
) -> Result<types::cobs::issue::Issue, Error> {
    let mut node = Node::new(ctx.profile.socket());
    let repo = ctx.profile.storage.repository(rid)?;
    let signer = ctx.profile.signer()?;
@@ -54,36 +54,46 @@ pub fn edit_issue(
    let mut issue = issues.get_mut(&cob_id.into())?;

    match action {
-
        cobs::IssueAction::Lifecycle { state } => {
-
            issue.lifecycle(state, &signer)?;
+
        types::cobs::issue::Action::Lifecycle { state } => {
+
            issue.lifecycle(state.into(), &signer)?;
        }
-
        cobs::IssueAction::Assign { assignees } => {
+
        types::cobs::issue::Action::Assign { assignees } => {
            issue.assign(assignees, &signer)?;
        }
-
        cobs::IssueAction::Label { labels } => {
+
        types::cobs::issue::Action::Label { labels } => {
            issue.label(labels, &signer)?;
        }
-
        cobs::IssueAction::CommentReact {
+
        types::cobs::issue::Action::CommentReact {
            id,
            reaction,
            active,
        } => {
            issue.react(id, reaction, active, &signer)?;
        }
-
        cobs::IssueAction::CommentRedact { id } => {
+
        types::cobs::issue::Action::CommentRedact { id } => {
            issue.redact_comment(id, &signer)?;
        }
-
        cobs::IssueAction::Comment {
+
        types::cobs::issue::Action::Comment {
            body,
            reply_to,
            embeds,
        } => {
-
            issue.comment(body, reply_to.unwrap_or(cob_id), embeds, &signer)?;
+
            issue.comment(
+
                body,
+
                reply_to.unwrap_or(cob_id),
+
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                &signer,
+
            )?;
        }
-
        cobs::IssueAction::CommentEdit { id, body, embeds } => {
-
            issue.edit_comment(id, body, embeds, &signer)?;
+
        types::cobs::issue::Action::CommentEdit { id, body, embeds } => {
+
            issue.edit_comment(
+
                id,
+
                body,
+
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                &signer,
+
            )?;
        }
-
        cobs::IssueAction::Edit { title } => {
+
        types::cobs::issue::Action::Edit { title } => {
            issue.edit(title, &signer)?;
        }
    }
@@ -92,15 +102,15 @@ pub fn edit_issue(
        node.announce_refs(rid)?;
    }

-
    Ok::<_, Error>(cobs::Issue::new(issue.id(), &issue, &aliases))
+
    Ok::<_, Error>(types::cobs::issue::Issue::new(issue.id(), &issue, &aliases))
}

#[tauri::command]
pub fn list_issues(
    ctx: tauri::State<AppState>,
-
    rid: RepoId,
+
    rid: identity::RepoId,
    status: query::IssueStatus,
-
) -> Result<Vec<cobs::Issue>, Error> {
+
) -> Result<Vec<types::cobs::issue::Issue>, Error> {
    let repo = ctx.profile.storage.repository(rid)?;
    let issues = ctx.profile.issues(&repo)?;
    let mut issues: Vec<_> = issues
@@ -115,7 +125,7 @@ pub fn list_issues(
    let aliases = &ctx.profile.aliases();
    let issues = issues
        .into_iter()
-
        .map(|(id, issue)| cobs::Issue::new(&id, &issue, aliases))
+
        .map(|(id, issue)| types::cobs::issue::Issue::new(&id, &issue, aliases))
        .collect::<Vec<_>>();

    Ok::<_, Error>(issues)
@@ -124,15 +134,15 @@ pub fn list_issues(
#[tauri::command]
pub fn issue_by_id(
    ctx: tauri::State<AppState>,
-
    rid: RepoId,
+
    rid: identity::RepoId,
    id: git::Oid,
-
) -> Result<Option<cobs::Issue>, Error> {
+
) -> Result<Option<types::cobs::issue::Issue>, Error> {
    let repo = ctx.profile.storage.repository(rid)?;
    let issues = ctx.profile.issues(&repo)?;
    let issue = issues.get(&id.into())?;

    let aliases = &ctx.profile.aliases();
-
    let issue = issue.map(|issue| cobs::Issue::new(&id.into(), &issue, aliases));
+
    let issue = issue.map(|issue| types::cobs::issue::Issue::new(&id.into(), &issue, aliases));

    Ok::<_, Error>(issue)
}
modified crates/radicle-tauri/src/commands/cob/patch.rs
@@ -1,28 +1,18 @@
-
use serde::{Deserialize, Serialize};
-
use ts_rs::TS;
-

use radicle::cob;
use radicle::git;
use radicle::identity;
+
use radicle::node::Handle;
+
use radicle::node::Node;
use radicle::patch;
use radicle::patch::cache::Patches;
use radicle::storage;
use radicle::storage::ReadStorage;
-
use radicle_types::cobs;
-
use radicle_types::thread;
+
use radicle_types as types;

use crate::cob::query;
use crate::error::Error;
use crate::AppState;

-
#[derive(Serialize, Deserialize, TS)]
-
#[ts(export)]
-
pub struct PaginatedQuery<T> {
-
    pub cursor: usize,
-
    pub more: bool,
-
    pub content: T,
-
}
-

#[tauri::command]
pub async fn list_patches(
    ctx: tauri::State<'_, AppState>,
@@ -30,7 +20,7 @@ pub async fn list_patches(
    status: Option<query::PatchStatus>,
    skip: Option<usize>,
    take: Option<usize>,
-
) -> Result<PaginatedQuery<Vec<cobs::Patch>>, Error> {
+
) -> Result<types::cobs::PaginatedQuery<Vec<types::cobs::patch::Patch>>, Error> {
    let cursor = skip.unwrap_or(0);
    let take = take.unwrap_or(20);
    let repo = ctx.profile.storage.repository(rid)?;
@@ -45,7 +35,7 @@ pub async fn list_patches(
    let mut patches = patches
        .into_iter()
        .filter_map(|p| {
-
            p.map(|(id, patch)| cobs::Patch::new(id, patch, aliases))
+
            p.map(|(id, patch)| types::cobs::patch::Patch::new(id, &patch, aliases))
                .ok()
        })
        .skip(cursor)
@@ -54,7 +44,7 @@ pub async fn list_patches(

    patches.sort_by_key(|b| std::cmp::Reverse(b.timestamp()));

-
    Ok::<_, Error>(PaginatedQuery {
+
    Ok::<_, Error>(types::cobs::PaginatedQuery {
        cursor,
        more,
        content: patches,
@@ -66,12 +56,12 @@ pub fn patch_by_id(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
    id: git::Oid,
-
) -> Result<Option<cobs::Patch>, Error> {
+
) -> Result<Option<types::cobs::patch::Patch>, Error> {
    let repo = ctx.profile.storage.repository(rid)?;
    let patches = ctx.profile.patches(&repo)?;
    let patch = patches.get(&id.into())?;
    let aliases = &ctx.profile.aliases();
-
    let patches = patch.map(|patch| cobs::Patch::new(id.into(), patch, aliases));
+
    let patches = patch.map(|patch| types::cobs::patch::Patch::new(id.into(), &patch, aliases));

    Ok::<_, Error>(patches)
}
@@ -81,7 +71,7 @@ pub fn revisions_by_patch(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
    id: git::Oid,
-
) -> Result<Option<Vec<cobs::Revision>>, Error> {
+
) -> Result<Option<Vec<types::cobs::patch::Revision>>, Error> {
    let repo = ctx.profile.storage.repository(rid)?;
    let patches = ctx.profile.patches(&repo)?;
    let revisions = patches.get(&id.into())?.map(|patch| {
@@ -89,7 +79,7 @@ pub fn revisions_by_patch(

        patch
            .revisions()
-
            .map(|(_, r)| cobs::Revision::new(r.clone(), aliases))
+
            .map(|(_, r)| types::cobs::patch::Revision::new(r.clone(), aliases))
            .collect::<Vec<_>>()
    });

@@ -102,7 +92,7 @@ pub fn revision_by_patch_and_id(
    rid: identity::RepoId,
    id: git::Oid,
    revision_id: git::Oid,
-
) -> Result<Option<cobs::Revision>, Error> {
+
) -> Result<Option<types::cobs::patch::Revision>, Error> {
    let repo = ctx.profile.storage.repository(rid)?;
    let patches = ctx.profile.patches(&repo)?;
    let revision = patches.get(&id.into())?.and_then(|patch| {
@@ -110,7 +100,7 @@ pub fn revision_by_patch_and_id(

        patch
            .revision(&revision_id.into())
-
            .map(|r| cobs::Revision::new(r.clone(), aliases))
+
            .map(|r| types::cobs::patch::Revision::new(r.clone(), aliases))
    });

    Ok::<_, Error>(revision)
@@ -171,7 +161,7 @@ pub fn create_draft_review_comment(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
    cob_id: git::Oid,
-
    new: thread::CreateReviewComment,
+
    new: types::cobs::thread::CreateReviewComment,
) -> Result<(), Error> {
    let repo = ctx.profile.storage.repository(rid)?;
    let signer = ctx.profile.signer()?;
@@ -186,7 +176,7 @@ pub fn create_draft_review_comment(
            new.body,
            new.location.map(|l| l.into()),
            new.reply_to,
-
            new.embeds,
+
            new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
        )?;

        Ok(())
@@ -206,7 +196,7 @@ pub fn edit_draft_review(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
    cob_id: git::Oid,
-
    edit: cobs::ReviewEdit,
+
    edit: types::cobs::patch::ReviewEdit,
) -> Result<(), Error> {
    let repo = ctx.profile.storage.repository(rid)?;
    let signer = ctx.profile.signer()?;
@@ -251,3 +241,186 @@ pub fn get_draft_review(

    review
}
+

+
#[tauri::command]
+
pub fn edit_patch(
+
    ctx: tauri::State<AppState>,
+
    rid: identity::RepoId,
+
    cob_id: git::Oid,
+
    action: types::cobs::patch::Action,
+
    opts: types::cobs::CobOptions,
+
) -> Result<types::cobs::patch::Patch, Error> {
+
    let mut node = Node::new(ctx.profile.socket());
+
    let repo = ctx.profile.storage.repository(rid)?;
+
    let signer = ctx.profile.signer()?;
+
    let aliases = ctx.profile.aliases();
+
    let mut patches = ctx.profile.patches_mut(&repo)?;
+
    let mut patch = patches.get_mut(&cob_id.into())?;
+

+
    match action {
+
        types::cobs::patch::Action::RevisionEdit {
+
            revision,
+
            description,
+
            embeds,
+
        } => {
+
            patch.edit_revision(
+
                revision,
+
                description,
+
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                &signer,
+
            )?;
+
        }
+
        types::cobs::patch::Action::RevisionCommentRedact { revision, comment } => {
+
            patch.comment_redact(revision, comment, &signer)?;
+
        }
+
        types::cobs::patch::Action::ReviewCommentRedact { review, comment } => {
+
            patch.redact_review_comment(review, comment, &signer)?;
+
        }
+
        types::cobs::patch::Action::ReviewCommentReact {
+
            review,
+
            comment,
+
            reaction,
+
            active,
+
        } => {
+
            patch.react_review_comment(review, comment, reaction, active, &signer)?;
+
        }
+
        types::cobs::patch::Action::ReviewCommentResolve { review, comment } => {
+
            patch.resolve_review_comment(review, comment, &signer)?;
+
        }
+
        types::cobs::patch::Action::ReviewCommentUnresolve { review, comment } => {
+
            patch.unresolve_review_comment(review, comment, &signer)?;
+
        }
+
        types::cobs::patch::Action::Edit { title, target } => {
+
            patch.edit(title, target, &signer)?;
+
        }
+
        types::cobs::patch::Action::ReviewEdit {
+
            review,
+
            summary,
+
            verdict,
+
            labels,
+
        } => {
+
            patch.review_edit(review, verdict, summary, labels, &signer)?;
+
        }
+
        types::cobs::patch::Action::Review {
+
            revision,
+
            summary,
+
            verdict,
+
            labels,
+
        } => {
+
            patch.review(revision, verdict, summary, labels, &signer)?;
+
        }
+
        types::cobs::patch::Action::ReviewRedact { review } => {
+
            patch.redact_review(review, &signer)?;
+
        }
+
        types::cobs::patch::Action::ReviewComment {
+
            review,
+
            body,
+
            location,
+
            reply_to,
+
            embeds,
+
        } => {
+
            patch.review_comment(
+
                review,
+
                body,
+
                location.map(|l| l.into()),
+
                reply_to,
+
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                &signer,
+
            )?;
+
        }
+
        types::cobs::patch::Action::ReviewCommentEdit {
+
            review,
+
            comment,
+
            body,
+
            embeds,
+
        } => {
+
            patch.edit_review_comment(
+
                review,
+
                comment,
+
                body,
+
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                &signer,
+
            )?;
+
        }
+
        types::cobs::patch::Action::Lifecycle { state } => {
+
            patch.lifecycle(state, &signer)?;
+
        }
+
        types::cobs::patch::Action::Assign { assignees } => {
+
            patch.assign(assignees, &signer)?;
+
        }
+
        types::cobs::patch::Action::Label { labels } => {
+
            patch.label(labels, &signer)?;
+
        }
+
        types::cobs::patch::Action::RevisionReact {
+
            revision,
+
            reaction,
+
            location,
+
            active,
+
        } => {
+
            patch.react(
+
                revision,
+
                reaction,
+
                location.map(|l| l.into()),
+
                active,
+
                &signer,
+
            )?;
+
        }
+
        types::cobs::patch::Action::RevisionComment {
+
            revision,
+
            location,
+
            body,
+
            reply_to,
+
            embeds,
+
        } => {
+
            patch.comment(
+
                revision,
+
                body,
+
                reply_to,
+
                location.map(|l| l.into()),
+
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                &signer,
+
            )?;
+
        }
+
        types::cobs::patch::Action::RevisionCommentEdit {
+
            revision,
+
            comment,
+
            body,
+
            embeds,
+
        } => {
+
            patch.comment_edit(
+
                revision,
+
                comment,
+
                body,
+
                embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
                &signer,
+
            )?;
+
        }
+
        types::cobs::patch::Action::RevisionCommentReact {
+
            revision,
+
            comment,
+
            reaction,
+
            active,
+
        } => {
+
            patch.comment_react(revision, comment, reaction, active, &signer)?;
+
        }
+
        types::cobs::patch::Action::RevisionRedact { revision } => {
+
            patch.redact(revision, &signer)?;
+
        }
+
        types::cobs::patch::Action::Merge { .. } => {
+
            unimplemented!("We don't support merging of patches through the desktop")
+
        }
+
        types::cobs::patch::Action::Revision { .. } => {
+
            unimplemented!("We don't support creating new revisions through the desktop")
+
        }
+
    }
+

+
    if opts.announce() {
+
        node.announce_refs(rid)?;
+
    }
+

+
    Ok::<_, Error>(types::cobs::patch::Patch::new(
+
        *patch.id(),
+
        &patch,
+
        &aliases,
+
    ))
+
}
modified crates/radicle-tauri/src/commands/profile.rs
@@ -3,7 +3,6 @@ use radicle_types::config::Config;
use crate::error::Error;
use crate::AppState;

-
/// Get active config.
#[tauri::command]
pub fn config(ctx: tauri::State<AppState>) -> Result<Config, Error> {
    let config = Config {
modified crates/radicle-tauri/src/commands/repo.rs
@@ -1,5 +1,3 @@
-
use serde_json::json;
-

use radicle::crypto::Verified;
use radicle::identity::doc::PayloadId;
use radicle::identity::{DocAt, RepoId};
@@ -15,7 +13,6 @@ use radicle_types as types;
use crate::error::Error;
use crate::AppState;

-
/// List all repos.
#[tauri::command]
pub fn list_repos(ctx: tauri::State<AppState>) -> Result<Vec<types::repo::RepoInfo>, Error> {
    let storage = &ctx.profile.storage;
@@ -94,22 +91,22 @@ pub fn repo_info(
        let issues = profile.issues(repo).ok()?;
        let issues = issues.counts().ok()?;

-
        Some(json!({
-
            "data": payload,
-
            "meta": {
-
                "issues": issues,
-
                "patches": patches,
-
                "head": head,
-
                "lastCommit": commit.time().seconds() * 1000,
-
            },
-
        }))
+
        let data: types::repo::ProjectPayloadData = (*payload).clone().try_into().ok()?;
+
        let meta = types::repo::ProjectPayloadMeta {
+
            issues,
+
            patches,
+
            head,
+
            last_commit_timestamp: commit.time().seconds() * 1000,
+
        };
+

+
        Some(types::repo::ProjectPayload::new(data, meta))
    });

    Ok::<_, Error>(types::repo::RepoInfo {
        payloads: types::repo::SupportedPayloads { project },
        delegates,
        threshold: doc.threshold,
-
        visibility: doc.visibility.clone(),
+
        visibility: doc.visibility.clone().into(),
        rid: repo.id,
        seeding,
    })
modified crates/radicle-tauri/src/commands/thread.rs
@@ -5,8 +5,7 @@ use radicle::identity;
use radicle::node::Handle;
use radicle::storage::ReadStorage;
use radicle::Node;
-
use radicle_types::cobs;
-
use radicle_types::thread;
+
use radicle_types as types;

use crate::error::Error;
use crate::AppState;
@@ -15,9 +14,9 @@ use crate::AppState;
pub fn create_issue_comment(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
-
    new: thread::NewIssueComment,
-
    opts: cobs::CobOptions,
-
) -> Result<thread::Comment<cobs::Never>, Error> {
+
    new: types::cobs::thread::NewIssueComment,
+
    opts: types::cobs::CobOptions,
+
) -> Result<types::cobs::thread::Comment<types::cobs::Never>, Error> {
    let aliases = &ctx.profile.aliases();
    let mut node = Node::new(ctx.profile.socket());
    let signer = ctx.profile.signer()?;
@@ -28,20 +27,26 @@ pub fn create_issue_comment(
        let (root_id, _) = issue.root();
        *root_id
    });
-
    let oid = issue.comment(new.body.clone(), id, new.embeds.clone(), &signer)?;
+
    let n = new.clone();
+
    let oid = issue.comment(
+
        n.body,
+
        id,
+
        n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
+
        &signer,
+
    )?;

    if opts.announce() {
        node.announce_refs(rid)?;
    }

-
    Ok(thread::Comment::<cobs::Never>::new(
+
    Ok(types::cobs::thread::Comment::<types::cobs::Never>::new(
        oid,
        cob::thread::Comment::new(
            *signer.public_key(),
            new.body,
            id.into(),
            None,
-
            new.embeds,
+
            new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
            LocalTime::now().into(),
        ),
        aliases,
@@ -52,9 +57,9 @@ pub fn create_issue_comment(
pub fn create_patch_comment(
    ctx: tauri::State<AppState>,
    rid: identity::RepoId,
-
    new: thread::NewPatchComment,
-
    opts: cobs::CobOptions,
-
) -> Result<thread::Comment<thread::CodeLocation>, Error> {
+
    new: types::cobs::thread::NewPatchComment,
+
    opts: types::cobs::CobOptions,
+
) -> Result<types::cobs::thread::Comment<types::cobs::thread::CodeLocation>, Error> {
    let aliases = &ctx.profile.aliases();
    let mut node = Node::new(ctx.profile.socket());
    let signer = ctx.profile.signer()?;
@@ -67,7 +72,7 @@ pub fn create_patch_comment(
        n.body,
        n.reply_to,
        n.location.map(|l| l.into()),
-
        n.embeds,
+
        n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
        &signer,
    )?;

@@ -75,14 +80,16 @@ pub fn create_patch_comment(
        node.announce_refs(rid)?;
    }

-
    Ok(thread::Comment::<thread::CodeLocation>::new(
+
    Ok(types::cobs::thread::Comment::<
+
        types::cobs::thread::CodeLocation,
+
    >::new(
        oid,
        cob::thread::Comment::new(
            *signer.public_key(),
            new.body,
            new.reply_to,
            new.location.map(|l| l.into()),
-
            new.embeds,
+
            new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
            LocalTime::now().into(),
        ),
        aliases,
modified crates/radicle-tauri/src/lib.rs
@@ -85,6 +85,7 @@ pub fn run() {
            cob::issue::edit_issue,
            cob::patch::list_patches,
            cob::patch::patch_by_id,
+
            cob::patch::edit_patch,
            cob::patch::revisions_by_patch,
            cob::patch::revision_by_patch_and_id,
            cob::patch::create_draft_review,
modified crates/radicle-types/Cargo.toml
@@ -8,4 +8,5 @@ radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
radicle-surf = { version = "0.22.1", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = { version = "1.0.132" }
-
ts-rs = { version = "10.0.0", features = ["serde-json-impl", "no-serde-warnings"] }

\ No newline at end of file
+
thiserror = { version = "1.0.65" }
+
ts-rs = { version = "10.0.0", features = ["serde-json-impl", "no-serde-warnings"] }
added crates/radicle-types/bindings/cob/Author.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 Author = { did: string; alias?: string };
added crates/radicle-types/bindings/cob/CobOptions.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 CobOptions = { announce?: boolean };
added crates/radicle-types/bindings/cob/Never.ts
@@ -0,0 +1,6 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
/**
+
 *A type alias for the TS type `never`.
+
 */
+
export type Never = never;
added crates/radicle-types/bindings/cob/PaginatedQuery.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 PaginatedQuery<T> = { cursor: number; more: boolean; content: T };
added crates/radicle-types/bindings/cob/Reaction.ts
@@ -0,0 +1,9 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "./Author";
+
import type { CodeLocation } from "./thread/CodeLocation";
+

+
export type Reaction = {
+
  emoji: string;
+
  authors: Array<Author>;
+
  location?: CodeLocation;
+
};
added crates/radicle-types/bindings/cob/Stats.ts
@@ -0,0 +1,7 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type Stats = {
+
  files_changed: number;
+
  insertions: number;
+
  deletions: number;
+
};
added crates/radicle-types/bindings/cob/issue/Action.ts
@@ -0,0 +1,13 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Embed } from "../thread/Embed";
+
import type { State } from "./State";
+

+
export type Action =
+
  | { type: "assign"; assignees: Array<string> }
+
  | { type: "edit"; title: string }
+
  | { type: "lifecycle"; state: State }
+
  | { type: "label"; labels: Array<string> }
+
  | { type: "comment"; body: string; replyTo?: string; embeds?: Array<Embed> }
+
  | { type: "comment.edit"; id: string; body: string; embeds?: Array<Embed> }
+
  | { type: "comment.redact"; id: string }
+
  | { type: "comment.react"; id: string; reaction: string; active: boolean };
added crates/radicle-types/bindings/cob/issue/CloseReason.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 CloseReason = "other" | "solved";
added crates/radicle-types/bindings/cob/issue/Issue.ts
@@ -0,0 +1,16 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { Comment } from "../thread/Comment";
+
import type { Never } from "../Never";
+
import type { State } from "./State";
+

+
export type Issue = {
+
  id: string;
+
  author: Author;
+
  title: string;
+
  state: State;
+
  assignees: Array<Author>;
+
  discussion: Array<Comment<Never>>;
+
  labels: Array<string>;
+
  timestamp: number;
+
};
added crates/radicle-types/bindings/cob/issue/NewIssue.ts
@@ -0,0 +1,10 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Embed } from "../thread/Embed";
+

+
export type NewIssue = {
+
  title: string;
+
  description: string;
+
  labels?: Array<string>;
+
  assignees?: Array<string>;
+
  embeds?: Array<Embed>;
+
};
added crates/radicle-types/bindings/cob/issue/Operation.ts
@@ -0,0 +1,19 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { Embed } from "../thread/Embed";
+
import type { State } from "./State";
+

+
export type Operation = {
+
  entry_id: string;
+
  timestamp: number;
+
  author: Author;
+
} & (
+
  | { type: "assign"; assignees: Array<string> }
+
  | { type: "edit"; title: string }
+
  | { type: "lifecycle"; state: State }
+
  | { type: "label"; labels: Array<string> }
+
  | { type: "comment"; body: string; replyTo?: string; embeds?: Array<Embed> }
+
  | { type: "comment.edit"; id: string; body: string; embeds?: Array<Embed> }
+
  | { type: "comment.redact"; id: string }
+
  | { type: "comment.react"; id: string; reaction: string; active: boolean }
+
);
added crates/radicle-types/bindings/cob/issue/State.ts
@@ -0,0 +1,6 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { CloseReason } from "./CloseReason";
+

+
export type State =
+
  | { status: "closed"; reason: CloseReason }
+
  | { status: "open" };
added crates/radicle-types/bindings/cob/patch/Action.ts
@@ -0,0 +1,101 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { CodeLocation } from "../thread/CodeLocation";
+
import type { Embed } from "../thread/Embed";
+

+
/**
+
 * Patch operation.
+
 */
+
export type Action =
+
  | { type: "edit"; title: string; target: string }
+
  | { type: "label"; labels: Array<string> }
+
  | { type: "lifecycle"; state: { status: "draft" | "open" | "archived" } }
+
  | { type: "assign"; assignees: Array<string> }
+
  | { type: "merge"; revision: string; commit: string }
+
  | {
+
      type: "review";
+
      revision: string;
+
      summary?: string;
+
      verdict?: string;
+
      labels?: Array<string>;
+
    }
+
  | {
+
      type: "review.edit";
+
      review: string;
+
      summary?: string;
+
      verdict?: string;
+
      labels?: Array<string>;
+
    }
+
  | { type: "review.redact"; review: string }
+
  | {
+
      type: "review.comment";
+
      review: string;
+
      body: string;
+
      location?: CodeLocation;
+
      /**
+
       * Comment this is a reply to.
+
       * Should be [`null`] if it's the top-level comment.
+
       */
+
      reply_to?: string;
+
      embeds?: Array<Embed>;
+
    }
+
  | {
+
      type: "review.comment.edit";
+
      review: string;
+
      comment: string;
+
      body: string;
+
      embeds?: Array<Embed>;
+
    }
+
  | { type: "review.comment.redact"; review: string; comment: string }
+
  | {
+
      type: "review.comment.react";
+
      review: string;
+
      comment: string;
+
      reaction: string;
+
      active: boolean;
+
    }
+
  | { type: "review.comment.resolve"; review: string; comment: string }
+
  | { type: "review.comment.unresolve"; review: string; comment: string }
+
  | {
+
      type: "revision";
+
      description: string;
+
      base: string;
+
      oid: string;
+
      resolves?: Array<[string, string]>;
+
    }
+
  | {
+
      type: "revision.edit";
+
      revision: string;
+
      description: string;
+
      embeds?: Array<Embed>;
+
    }
+
  | {
+
      type: "revision.react";
+
      revision: string;
+
      location?: CodeLocation;
+
      reaction: string;
+
      active: boolean;
+
    }
+
  | { type: "revision.redact"; revision: string }
+
  | {
+
      type: "revision.comment";
+
      revision: string;
+
      location?: CodeLocation;
+
      body: string;
+
      replyTo?: string;
+
      embeds?: Array<Embed>;
+
    }
+
  | {
+
      type: "revision.comment.edit";
+
      revision: string;
+
      comment: string;
+
      body: string;
+
      embeds?: Array<Embed>;
+
    }
+
  | { type: "revision.comment.redact"; revision: string; comment: string }
+
  | {
+
      type: "revision.comment.react";
+
      revision: string;
+
      comment: string;
+
      reaction: string;
+
      active: boolean;
+
    };
added crates/radicle-types/bindings/cob/patch/Edit.ts
@@ -0,0 +1,10 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { Embed } from "../thread/Embed";
+

+
export type Edit = {
+
  author: Author;
+
  timestamp: number;
+
  body: string;
+
  embeds?: Array<Embed>;
+
};
added crates/radicle-types/bindings/cob/patch/Patch.ts
@@ -0,0 +1,16 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { State } from "./State";
+

+
export type Patch = {
+
  id: string;
+
  author: Author;
+
  title: string;
+
  base: string;
+
  head: string;
+
  state: State;
+
  assignees: Array<Author>;
+
  labels: Array<string>;
+
  timestamp: number;
+
  revisionCount: number;
+
};
added crates/radicle-types/bindings/cob/patch/Review.ts
@@ -0,0 +1,13 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { CodeLocation } from "../thread/CodeLocation";
+
import type { Comment } from "../thread/Comment";
+

+
export type Review = {
+
  id: string;
+
  author: Author;
+
  verdict?: "accept" | "reject";
+
  summary?: string;
+
  comments?: Array<Comment<CodeLocation>>;
+
  timestamp: number;
+
};
added crates/radicle-types/bindings/cob/patch/ReviewEdit.ts
@@ -0,0 +1,8 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type ReviewEdit = {
+
  reviewId: string;
+
  verdict?: string;
+
  summary?: string;
+
  labels?: Array<string>;
+
};
added crates/radicle-types/bindings/cob/patch/Revision.ts
@@ -0,0 +1,19 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { CodeLocation } from "../thread/CodeLocation";
+
import type { Comment } from "../thread/Comment";
+
import type { Edit } from "./Edit";
+
import type { Reaction } from "../Reaction";
+
import type { Review } from "./Review";
+

+
export type Revision = {
+
  id: string;
+
  author: Author;
+
  description: Array<Edit>;
+
  base: string;
+
  head: string;
+
  reviews?: Array<Review>;
+
  timestamp: number;
+
  discussion?: Array<Comment<CodeLocation>>;
+
  reactions?: Array<Reaction>;
+
};
added crates/radicle-types/bindings/cob/patch/State.ts
@@ -0,0 +1,7 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type State =
+
  | { status: "draft" }
+
  | { status: "open"; conflicts?: Array<[string, string]> }
+
  | { status: "archived" }
+
  | { status: "merged"; revision: string; commit: string };
added crates/radicle-types/bindings/cob/thread/CodeLocation.ts
@@ -0,0 +1,9 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { CodeRange } from "./CodeRange";
+

+
export type CodeLocation = {
+
  commit: string;
+
  path: string;
+
  old: CodeRange | null;
+
  new: CodeRange | null;
+
};
added crates/radicle-types/bindings/cob/thread/CodeRange.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.
+

+
export type CodeRange =
+
  | { type: "lines"; range: { start: number; end: number } }
+
  | { type: "chars"; line: number; range: { start: number; end: number } };
added crates/radicle-types/bindings/cob/thread/Comment.ts
@@ -0,0 +1,17 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Author } from "../Author";
+
import type { Edit } from "../patch/Edit";
+
import type { Embed } from "./Embed";
+
import type { Never } from "../Never";
+
import type { Reaction } from "../Reaction";
+

+
export type Comment<T = Never> = {
+
  id: string;
+
  author: Author;
+
  edits: Array<Edit>;
+
  reactions: Array<Reaction>;
+
  replyTo?: string;
+
  location?: T;
+
  embeds?: Array<Embed>;
+
  resolved: boolean;
+
};
added crates/radicle-types/bindings/cob/thread/CreateReviewComment.ts
@@ -0,0 +1,11 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { CodeLocation } from "./CodeLocation";
+
import type { Embed } from "./Embed";
+

+
export type CreateReviewComment = {
+
  reviewId: string;
+
  body: string;
+
  replyTo?: string;
+
  location?: CodeLocation;
+
  embeds?: Array<Embed>;
+
};
added crates/radicle-types/bindings/cob/thread/Embed.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 Embed = { name: string; content: string };
added crates/radicle-types/bindings/cob/thread/NewIssueComment.ts
@@ -0,0 +1,9 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { Embed } from "./Embed";
+

+
export type NewIssueComment = {
+
  id: string;
+
  body: string;
+
  replyTo?: string;
+
  embeds?: Array<Embed>;
+
};
added crates/radicle-types/bindings/cob/thread/NewPatchComment.ts
@@ -0,0 +1,12 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { CodeLocation } from "./CodeLocation";
+
import type { Embed } from "./Embed";
+

+
export type NewPatchComment = {
+
  id: string;
+
  revision: string;
+
  body: string;
+
  replyTo?: string;
+
  location?: CodeLocation;
+
  embeds?: Array<Embed>;
+
};
added crates/radicle-types/bindings/config/Config.ts
@@ -0,0 +1,21 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
/**
+
 * Service configuration.
+
 */
+
export type Config = {
+
  /**
+
   * Node Public Key in NID format.
+
   */
+
  publicKey: string;
+
  /**
+
   * Node alias.
+
   */
+
  alias: string;
+
  /**
+
   * Default seeding policy.
+
   */
+
  seedingPolicy:
+
    | { default: "allow"; scope: "followed" | "all" }
+
    | { default: "block" };
+
};
added crates/radicle-types/bindings/repo/ProjectPayload.ts
@@ -0,0 +1,8 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
import type { ProjectPayloadData } from "./ProjectPayloadData";
+
import type { ProjectPayloadMeta } from "./ProjectPayloadMeta";
+

+
export type ProjectPayload = {
+
  data: ProjectPayloadData;
+
  meta: ProjectPayloadMeta;
+
};
added crates/radicle-types/bindings/repo/ProjectPayloadData.ts
@@ -0,0 +1,7 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type ProjectPayloadData = {
+
  defaultBranch: string;
+
  description: string;
+
  name: string;
+
};
added crates/radicle-types/bindings/repo/ProjectPayloadMeta.ts
@@ -0,0 +1,8 @@
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+

+
export type ProjectPayloadMeta = {
+
  head: string;
+
  issues: { open: number; closed: number };
+
  patches: { open: number; draft: number; archived: number; merged: number };
+
  lastCommitTimestamp: number;
+
};
added crates/radicle-types/bindings/repo/RepoInfo.ts
@@ -0,0 +1,13 @@
+
// 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 { SupportedPayloads } from "./SupportedPayloads";
+
import type { Visibility } from "./Visibility";
+

+
export type RepoInfo = {
+
  payloads: SupportedPayloads;
+
  delegates: Array<Author>;
+
  threshold: number;
+
  visibility: Visibility;
+
  rid: string;
+
  seeding: number;
+
};
added crates/radicle-types/bindings/repo/SupportedPayloads.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 { ProjectPayload } from "./ProjectPayload";
+

+
export type SupportedPayloads = { "xyz.radicle.project"?: ProjectPayload };
added crates/radicle-types/bindings/repo/Visibility.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.
+

+
export type Visibility =
+
  | { type: "public" }
+
  | { type: "private"; allow?: Array<string> };
modified crates/radicle-types/src/cobs.rs
@@ -1,28 +1,22 @@
-
use std::collections::BTreeMap;
-
use std::collections::BTreeSet;
-

use serde::{Deserialize, Serialize};
use ts_rs::TS;

-
use radicle::cob;
-
use radicle::crypto;
use radicle::identity;
-
use radicle::issue;
use radicle::node::{Alias, AliasStore};
-
use radicle::patch;
-
use radicle::storage::git;

-
use crate::cobs;
-
use crate::thread;
+
pub mod issue;
+
pub mod patch;
+
pub mod thread;

#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/")]
pub struct Author {
    #[ts(as = "String")]
    did: identity::Did,
-
    #[serde(skip_serializing_if = "Option::is_none")]
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
+
    #[serde(default, skip_serializing_if = "Option::is_none")]
+
    #[ts(as = "Option<String>", optional)]
    alias: Option<Alias>,
}

@@ -36,295 +30,15 @@ impl Author {
}

#[derive(TS, Serialize)]
+
#[doc = "A type alias for the TS type `never`."]
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Issue {
-
    #[ts(as = "String")]
-
    id: String,
-
    author: Author,
-
    title: String,
-
    #[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
-
    state: issue::State,
-
    assignees: Vec<Author>,
-
    discussion: Vec<thread::Comment<cobs::Never>>,
-
    #[ts(as = "Vec<String>")]
-
    labels: Vec<cob::Label>,
-
    #[ts(type = "number")]
-
    timestamp: cob::Timestamp,
-
}
-

-
impl Issue {
-
    pub fn new(id: &issue::IssueId, issue: &issue::Issue, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            id: id.to_string(),
-
            author: Author::new(*issue.author().id(), aliases),
-
            title: issue.title().to_string(),
-
            state: *issue.state(),
-
            assignees: issue
-
                .assignees()
-
                .map(|did| Author::new(*did, aliases))
-
                .collect::<Vec<_>>(),
-
            discussion: issue
-
                .comments()
-
                .map(|(id, c)| thread::Comment::<Never>::new(*id, c.clone(), aliases))
-
                .collect::<Vec<_>>(),
-
            labels: issue.labels().cloned().collect::<Vec<_>>(),
-
            timestamp: issue.timestamp(),
-
        }
-
    }
-
}
-

-
#[derive(TS, Serialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Patch {
-
    #[ts(as = "String")]
-
    id: String,
-
    author: Author,
-
    title: String,
-
    #[ts(as = "String")]
-
    base: git::Oid,
-
    #[ts(as = "String")]
-
    head: git::Oid,
-
    #[ts(type = r#"{
-
  status: 'draft'
-
} | {
-
  status: 'open',
-
  conflicts: [string, string][]
-
} | {
-
  status: 'archived'
-
} | {
-
  status: 'merged', revision: string, commit: string
-
} "#)]
-
    state: patch::State,
-
    assignees: Vec<Author>,
-
    #[ts(as = "Vec<String>")]
-
    labels: Vec<cob::Label>,
-
    #[ts(type = "number")]
-
    timestamp: cob::Timestamp,
-
    revision_count: usize,
-
}
-

-
impl Patch {
-
    pub fn new(id: patch::PatchId, patch: patch::Patch, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            id: id.to_string(),
-
            author: Author::new(*patch.author().id(), aliases),
-
            title: patch.title().to_string(),
-
            state: patch.state().clone(),
-
            base: *patch.base(),
-
            head: *patch.head(),
-
            assignees: patch
-
                .assignees()
-
                .map(|did| Author::new(did, aliases))
-
                .collect::<Vec<_>>(),
-
            labels: patch.labels().cloned().collect::<Vec<_>>(),
-
            timestamp: patch.timestamp(),
-
            revision_count: patch.revisions().count(),
-
        }
-
    }
-

-
    pub fn timestamp(&self) -> u64 {
-
        self.timestamp.as_millis()
-
    }
-
}
-

-
#[derive(Serialize, Deserialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct ReviewEdit {
-
    #[ts(as = "String")]
-
    pub review_id: cob::patch::ReviewId,
-
    #[ts(as = "Option<String>")]
-
    pub verdict: Option<cob::patch::Verdict>,
-
    pub summary: Option<String>,
-
    #[ts(as = "Vec<String>")]
-
    pub labels: Vec<cob::Label>,
-
}
-

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct Revision {
-
    #[ts(as = "String")]
-
    id: patch::RevisionId,
-
    author: Author,
-
    description: Vec<Edit>,
-
    #[ts(as = "String")]
-
    base: git::Oid,
-
    #[ts(as = "String")]
-
    head: git::Oid,
-
    reviews: Vec<Review>,
-
    #[ts(type = "number")]
-
    timestamp: cob::common::Timestamp,
-
    discussion: Vec<thread::Comment<thread::CodeLocation>>,
-
    reactions: Vec<Reaction>,
-
}
-

-
impl Revision {
-
    pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            id: value.id(),
-
            author: Author::new(*value.author().id(), aliases),
-
            description: value
-
                .edits()
-
                .map(|e| Edit::new(e, aliases))
-
                .collect::<Vec<_>>(),
-
            base: *value.base(),
-
            head: value.head(),
-
            reviews: value
-
                .reviews()
-
                .map(|(id, r)| Review::new(*id, r.clone(), aliases))
-
                .collect::<Vec<_>>(),
-
            timestamp: value.timestamp(),
-
            discussion: value
-
                .discussion()
-
                .comments()
-
                .map(|(id, c)| {
-
                    thread::Comment::<thread::CodeLocation>::new(*id, c.clone(), aliases)
-
                })
-
                .collect::<Vec<_>>(),
-
            reactions: value
-
                .reactions()
-
                .iter()
-
                .flat_map(|(location, reactions)| {
-
                    let reaction_by_author = reactions.iter().fold(
-
                        BTreeMap::new(),
-
                        |mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| {
-
                            acc.entry(emoji).or_default().push(author);
-
                            acc
-
                        },
-
                    );
-
                    reaction_by_author
-
                        .into_iter()
-
                        .map(|(emoji, authors)| {
-
                            Reaction::new(
-
                                *emoji,
-
                                authors,
-
                                location
-
                                    .as_ref()
-
                                    .map(|l| thread::CodeLocation::new(l.clone())),
-
                                aliases,
-
                            )
-
                        })
-
                        .collect::<Vec<_>>()
-
                })
-
                .collect::<Vec<_>>(),
-
        }
-
    }
-
}
-

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct Reaction {
-
    #[ts(as = "String")]
-
    emoji: cob::Reaction,
-
    authors: Vec<Author>,
-
    #[ts(optional)]
-
    location: Option<thread::CodeLocation>,
-
}
-

-
impl Reaction {
-
    pub fn new(
-
        emoji: cob::Reaction,
-
        authors: Vec<&crypto::PublicKey>,
-
        location: Option<thread::CodeLocation>,
-
        aliases: &impl AliasStore,
-
    ) -> Self {
-
        Self {
-
            emoji,
-
            authors: authors
-
                .into_iter()
-
                .map(|a| Author::new(a.into(), aliases))
-
                .collect::<Vec<_>>(),
-
            location,
-
        }
-
    }
-
}
-

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Review {
-
    #[ts(as = "String")]
-
    id: identity::PublicKey,
-
    author: Author,
-
    #[ts(type = "'accept' | 'reject'")]
-
    #[ts(optional)]
-
    verdict: Option<cob::patch::Verdict>,
-
    summary: Option<String>,
-
    comments: Vec<thread::Comment<thread::CodeLocation>>,
-
    #[ts(type = "number")]
-
    timestamp: cob::common::Timestamp,
-
}
-

-
impl Review {
-
    pub fn new(
-
        id: identity::PublicKey,
-
        review: cob::patch::Review,
-
        aliases: &impl AliasStore,
-
    ) -> Self {
-
        Self {
-
            id,
-
            author: Author::new(review.author().id, aliases),
-
            verdict: review.verdict(),
-
            summary: review.summary().map(|s| s.to_string()),
-
            comments: review
-
                .comments()
-
                .map(|(id, c)| {
-
                    thread::Comment::<thread::CodeLocation>::new(*id, c.clone(), aliases)
-
                })
-
                .collect::<Vec<_>>(),
-
            timestamp: review.timestamp(),
-
        }
-
    }
-
}
-

-
#[derive(TS, Serialize)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Edit {
-
    pub author: Author,
-
    #[ts(type = "number")]
-
    pub timestamp: cob::common::Timestamp,
-
    pub body: String,
-
    #[ts(type = "{ name: string, content: string }")]
-
    pub embeds: Vec<cob::change::store::Embed<cob::common::Uri>>,
-
}
-

-
impl Edit {
-
    pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self {
-
        Self {
-
            author: Author::new(edit.author.into(), aliases),
-
            timestamp: edit.timestamp,
-
            body: edit.body.clone(),
-
            embeds: edit.embeds.clone(),
-
        }
-
    }
-
}
-

-
/// The `Infallible` type does not have a `Serialize`/`Deserialize`
-
/// implementation. The `Never` type imitates `Infallible` and
-
/// provides the derived implementations.
-
#[derive(TS, Serialize)]
+
#[ts(export_to = "cob/")]
pub enum Never {}

#[derive(TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct NewIssue {
-
    pub title: String,
-
    pub description: String,
-
    #[ts(as = "Vec<String>")]
-
    pub labels: Vec<cob::Label>,
-
    #[ts(as = "Vec<String>")]
-
    pub assignees: Vec<identity::Did>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(TS, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
+
#[ts(export_to = "cob/")]
pub struct CobOptions {
    #[ts(as = "Option<bool>")]
    #[ts(optional)]
@@ -337,8 +51,18 @@ impl CobOptions {
    }
}

+
#[derive(Serialize, Deserialize, TS)]
+
#[ts(export)]
+
#[ts(export_to = "cob/")]
+
pub struct PaginatedQuery<T> {
+
    pub cursor: usize,
+
    pub more: bool,
+
    pub content: T,
+
}
+

#[derive(TS, Serialize)]
#[ts(export)]
+
#[ts(export_to = "cob/")]
pub struct Stats {
    pub files_changed: usize,
    pub insertions: usize,
@@ -354,81 +78,3 @@ impl Stats {
        }
    }
}
-

-
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TS)]
-
#[serde(tag = "type", rename_all = "camelCase")]
-
#[ts(export)]
-
pub enum IssueAction {
-
    /// Assign issue to an actor.
-
    #[serde(rename = "assign")]
-
    Assign {
-
        #[ts(as = "Vec<String>")]
-
        assignees: BTreeSet<identity::Did>,
-
    },
-

-
    /// Edit issue title.
-
    #[serde(rename = "edit")]
-
    Edit { title: String },
-

-
    /// Transition to a different state.
-
    #[serde(rename = "lifecycle")]
-
    Lifecycle {
-
        #[ts(type = "{ status: 'closed', reason: 'other' | 'solved' } | { status: 'open' } ")]
-
        state: issue::State,
-
    },
-

-
    /// Modify issue labels.
-
    #[serde(rename = "label")]
-
    Label {
-
        #[ts(as = "Vec<String>")]
-
        labels: BTreeSet<cob::Label>,
-
    },
-

-
    /// Comment on a thread.
-
    #[serde(rename_all = "camelCase")]
-
    #[serde(rename = "comment")]
-
    Comment {
-
        /// Comment body.
-
        body: String,
-
        /// Comment this is a reply to.
-
        /// Should be [`None` | `null`] if it's the top-level comment.
-
        /// Should be the root [`CommentId`] if it's a top-level comment.
-
        #[serde(default, skip_serializing_if = "Option::is_none")]
-
        #[ts(as = "Option<String>")]
-
        reply_to: Option<cob::thread::CommentId>,
-
        /// Embeded content.
-
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
-
        #[ts(as = "Vec<String>")]
-
        embeds: Vec<cob::Embed<cob::Uri>>,
-
    },
-

-
    /// Edit a comment.
-
    #[serde(rename = "comment.edit")]
-
    CommentEdit {
-
        /// Comment being edited.
-
        #[ts(as = "String")]
-
        id: cob::thread::CommentId,
-
        /// New value for the comment body.
-
        body: String,
-
        /// New value for the embeds list.
-
        #[ts(as = "Vec<String>")]
-
        embeds: Vec<cob::Embed<cob::Uri>>,
-
    },
-

-
    /// Redact a change. Not all changes can be redacted.
-
    #[serde(rename = "comment.redact")]
-
    CommentRedact {
-
        #[ts(as = "String")]
-
        id: cob::thread::CommentId,
-
    },
-

-
    /// React to a comment.
-
    #[serde(rename = "comment.react")]
-
    CommentReact {
-
        #[ts(as = "String")]
-
        id: cob::thread::CommentId,
-
        #[ts(as = "String")]
-
        reaction: cob::Reaction,
-
        active: bool,
-
    },
-
}
added crates/radicle-types/src/cobs/issue.rs
@@ -0,0 +1,199 @@
+
use std::collections::BTreeSet;
+

+
use radicle::git;
+
use radicle::node::AliasStore;
+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;
+

+
use radicle::cob;
+
use radicle::identity;
+
use radicle::issue;
+

+
use crate::cobs;
+

+
#[derive(TS, Serialize)]
+
#[ts(export)]
+
#[ts(export_to = "cob/issue/")]
+
#[serde(rename_all = "camelCase")]
+
pub struct Issue {
+
    id: String,
+
    author: cobs::Author,
+
    title: String,
+
    state: cobs::issue::State,
+
    assignees: Vec<cobs::Author>,
+
    discussion: Vec<cobs::thread::Comment<cobs::Never>>,
+
    #[ts(as = "Vec<String>")]
+
    labels: Vec<cob::Label>,
+
    #[ts(type = "number")]
+
    timestamp: cob::Timestamp,
+
}
+

+
impl Issue {
+
    pub fn new(id: &issue::IssueId, issue: &issue::Issue, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            id: id.to_string(),
+
            author: cobs::Author::new(*issue.author().id(), aliases),
+
            title: issue.title().to_string(),
+
            state: (*issue.state()).into(),
+
            assignees: issue
+
                .assignees()
+
                .map(|did| cobs::Author::new(*did, aliases))
+
                .collect::<Vec<_>>(),
+
            discussion: issue
+
                .comments()
+
                .map(|(id, c)| cobs::thread::Comment::<cobs::Never>::new(*id, c.clone(), aliases))
+
                .collect::<Vec<_>>(),
+
            labels: issue.labels().cloned().collect::<Vec<_>>(),
+
            timestamp: issue.timestamp(),
+
        }
+
    }
+
}
+

+
#[derive(Serialize, TS)]
+
#[ts(export)]
+
#[ts(export_to = "cob/issue/")]
+
pub struct Operation {
+
    #[ts(as = "String")]
+
    pub entry_id: git::Oid,
+
    #[ts(flatten)]
+
    pub action: Action,
+
    #[ts(type = "number")]
+
    pub timestamp: cob::Timestamp,
+
    pub author: cobs::Author,
+
}
+

+
#[derive(Default, Serialize, Deserialize, TS)]
+
#[serde(rename_all = "camelCase", tag = "status")]
+
#[ts(export)]
+
#[ts(export_to = "cob/issue/")]
+
pub enum State {
+
    Closed {
+
        reason: CloseReason,
+
    },
+
    #[default]
+
    Open,
+
}
+

+
impl From<State> for issue::State {
+
    fn from(value: State) -> Self {
+
        match value {
+
            State::Closed { reason } => Self::Closed {
+
                reason: reason.into(),
+
            },
+
            State::Open => Self::Open,
+
        }
+
    }
+
}
+

+
impl From<issue::State> for State {
+
    fn from(value: issue::State) -> Self {
+
        match value {
+
            issue::State::Closed { reason } => Self::Closed {
+
                reason: reason.into(),
+
            },
+
            issue::State::Open => Self::Open,
+
        }
+
    }
+
}
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/issue/")]
+
pub enum CloseReason {
+
    Other,
+
    Solved,
+
}
+

+
impl From<CloseReason> for issue::CloseReason {
+
    fn from(value: CloseReason) -> Self {
+
        match value {
+
            CloseReason::Other => Self::Other,
+
            CloseReason::Solved => Self::Solved,
+
        }
+
    }
+
}
+

+
impl From<issue::CloseReason> for CloseReason {
+
    fn from(value: issue::CloseReason) -> Self {
+
        match value {
+
            issue::CloseReason::Other => Self::Other,
+
            issue::CloseReason::Solved => Self::Solved,
+
        }
+
    }
+
}
+

+
#[derive(TS, Serialize, Deserialize)]
+
#[ts(export)]
+
#[ts(export_to = "cob/issue/")]
+
#[serde(rename_all = "camelCase")]
+
pub struct NewIssue {
+
    pub title: String,
+
    pub description: String,
+
    #[ts(as = "Option<Vec<String>>", optional)]
+
    pub labels: Vec<cob::Label>,
+
    #[ts(as = "Option<Vec<String>>", optional)]
+
    pub assignees: Vec<identity::Did>,
+
    #[ts(as = "Option<_>", optional)]
+
    pub embeds: Vec<cobs::thread::Embed>,
+
}
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[serde(tag = "type", rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/issue/")]
+
pub enum Action {
+
    #[serde(rename = "assign")]
+
    Assign {
+
        #[ts(as = "Vec<String>")]
+
        assignees: BTreeSet<identity::Did>,
+
    },
+

+
    #[serde(rename = "edit")]
+
    Edit { title: String },
+

+
    #[serde(rename = "lifecycle")]
+
    Lifecycle { state: cobs::issue::State },
+

+
    #[serde(rename = "label")]
+
    Label {
+
        #[ts(as = "Vec<String>")]
+
        labels: BTreeSet<cob::Label>,
+
    },
+

+
    #[serde(rename_all = "camelCase")]
+
    #[serde(rename = "comment")]
+
    Comment {
+
        body: String,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(as = "Option<String>", optional)]
+
        reply_to: Option<cob::thread::CommentId>,
+
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
        #[ts(as = "Option<_>", optional)]
+
        embeds: Vec<cobs::thread::Embed>,
+
    },
+

+
    #[serde(rename = "comment.edit")]
+
    CommentEdit {
+
        #[ts(as = "String")]
+
        id: cob::thread::CommentId,
+
        body: String,
+
        #[ts(as = "Option<_>", optional)]
+
        embeds: Vec<cobs::thread::Embed>,
+
    },
+

+
    #[serde(rename = "comment.redact")]
+
    CommentRedact {
+
        #[ts(as = "String")]
+
        id: cob::thread::CommentId,
+
    },
+

+
    #[serde(rename = "comment.react")]
+
    CommentReact {
+
        #[ts(as = "String")]
+
        id: cob::thread::CommentId,
+
        #[ts(as = "String")]
+
        reaction: cob::Reaction,
+
        active: bool,
+
    },
+
}
added crates/radicle-types/src/cobs/patch.rs
@@ -0,0 +1,475 @@
+
use std::collections::BTreeMap;
+
use std::collections::BTreeSet;
+

+
use radicle::node::AliasStore;
+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;
+

+
use radicle::cob;
+
use radicle::git;
+
use radicle::identity;
+
use radicle::patch;
+

+
use crate::cobs;
+

+
#[derive(TS, Serialize)]
+
#[ts(export)]
+
#[ts(export_to = "cob/patch/")]
+
#[serde(rename_all = "camelCase")]
+
pub struct Patch {
+
    id: String,
+
    author: cobs::Author,
+
    title: String,
+
    #[ts(as = "String")]
+
    base: git::Oid,
+
    #[ts(as = "String")]
+
    head: git::Oid,
+
    state: State,
+
    assignees: Vec<cobs::Author>,
+
    #[ts(as = "Vec<String>")]
+
    labels: Vec<cob::Label>,
+
    #[ts(type = "number")]
+
    timestamp: cob::Timestamp,
+
    revision_count: usize,
+
}
+

+
impl Patch {
+
    pub fn new(id: patch::PatchId, patch: &patch::Patch, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            id: id.to_string(),
+
            author: cobs::Author::new(*patch.author().id(), aliases),
+
            title: patch.title().to_string(),
+
            state: patch.state().clone().into(),
+
            base: *patch.base(),
+
            head: *patch.head(),
+
            assignees: patch
+
                .assignees()
+
                .map(|did| cobs::Author::new(did, aliases))
+
                .collect::<Vec<_>>(),
+
            labels: patch.labels().cloned().collect::<Vec<_>>(),
+
            timestamp: patch.timestamp(),
+
            revision_count: patch.revisions().count(),
+
        }
+
    }
+

+
    pub fn timestamp(&self) -> u64 {
+
        self.timestamp.as_millis()
+
    }
+
}
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[serde(rename_all = "camelCase", tag = "status")]
+
#[ts(export)]
+
#[ts(export_to = "cob/patch/")]
+
pub enum State {
+
    Draft,
+
    Open {
+
        #[serde(skip_serializing_if = "Vec::is_empty")]
+
        #[serde(default)]
+
        #[ts(as = "Option<Vec<(String, String)>>", optional)]
+
        conflicts: Vec<(patch::RevisionId, git::Oid)>,
+
    },
+
    Archived,
+
    Merged {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[ts(as = "String")]
+
        commit: git::Oid,
+
    },
+
}
+

+
impl From<State> for patch::State {
+
    fn from(value: State) -> Self {
+
        match value {
+
            State::Archived => Self::Archived,
+
            State::Draft => Self::Draft,
+
            State::Merged { revision, commit } => Self::Merged { revision, commit },
+
            State::Open { conflicts } => Self::Open { conflicts },
+
        }
+
    }
+
}
+

+
impl From<patch::State> for State {
+
    fn from(value: patch::State) -> Self {
+
        match value {
+
            patch::State::Archived => Self::Archived,
+
            patch::State::Draft => Self::Draft,
+
            patch::State::Merged { revision, commit } => Self::Merged { revision, commit },
+
            patch::State::Open { conflicts } => Self::Open { conflicts },
+
        }
+
    }
+
}
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/patch/")]
+
pub struct ReviewEdit {
+
    #[ts(as = "String")]
+
    pub review_id: cob::patch::ReviewId,
+
    #[ts(as = "Option<String>", optional)]
+
    pub verdict: Option<cob::patch::Verdict>,
+
    #[ts(optional)]
+
    pub summary: Option<String>,
+
    #[ts(as = "Option<Vec<String>>", optional)]
+
    pub labels: Vec<cob::Label>,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/patch/")]
+
pub struct Revision {
+
    #[ts(as = "String")]
+
    id: patch::RevisionId,
+
    author: cobs::Author,
+
    description: Vec<Edit>,
+
    #[ts(as = "String")]
+
    base: git::Oid,
+
    #[ts(as = "String")]
+
    head: git::Oid,
+
    #[ts(as = "Option<_>", optional)]
+
    reviews: Vec<Review>,
+
    #[ts(type = "number")]
+
    timestamp: cob::common::Timestamp,
+
    #[ts(as = "Option<_>", optional)]
+
    discussion: Vec<cobs::thread::Comment<cobs::thread::CodeLocation>>,
+
    #[ts(as = "Option<_>", optional)]
+
    reactions: Vec<cobs::thread::Reaction>,
+
}
+

+
impl Revision {
+
    pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            id: value.id(),
+
            author: cobs::Author::new(*value.author().id(), aliases),
+
            description: value
+
                .edits()
+
                .map(|e| Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            base: *value.base(),
+
            head: value.head(),
+
            reviews: value
+
                .reviews()
+
                .map(|(id, r)| Review::new(*id, r.clone(), aliases))
+
                .collect::<Vec<_>>(),
+
            timestamp: value.timestamp(),
+
            discussion: value
+
                .discussion()
+
                .comments()
+
                .map(|(id, c)| {
+
                    cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
+
                        *id,
+
                        c.clone(),
+
                        aliases,
+
                    )
+
                })
+
                .collect::<Vec<_>>(),
+
            reactions: value
+
                .reactions()
+
                .iter()
+
                .flat_map(|(location, reactions)| {
+
                    let reaction_by_author = reactions.iter().fold(
+
                        BTreeMap::new(),
+
                        |mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| {
+
                            acc.entry(emoji).or_default().push(author);
+
                            acc
+
                        },
+
                    );
+
                    reaction_by_author
+
                        .into_iter()
+
                        .map(|(emoji, authors)| {
+
                            cobs::thread::Reaction::new(
+
                                *emoji,
+
                                authors,
+
                                location
+
                                    .as_ref()
+
                                    .map(|l| cobs::thread::CodeLocation::new(l.clone())),
+
                                aliases,
+
                            )
+
                        })
+
                        .collect::<Vec<_>>()
+
                })
+
                .collect::<Vec<_>>(),
+
        }
+
    }
+
}
+

+
#[derive(TS, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/patch/")]
+
pub struct Edit {
+
    pub author: cobs::Author,
+
    #[ts(type = "number")]
+
    pub timestamp: cob::common::Timestamp,
+
    pub body: String,
+
    #[ts(as = "Option<_>", optional)]
+
    pub embeds: Vec<cobs::thread::Embed>,
+
}
+

+
impl Edit {
+
    pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self {
+
        Self {
+
            author: cobs::Author::new(edit.author.into(), aliases),
+
            timestamp: edit.timestamp,
+
            body: edit.body.clone(),
+
            embeds: edit
+
                .embeds
+
                .iter()
+
                .cloned()
+
                .map(|e| e.into())
+
                .collect::<Vec<_>>(),
+
        }
+
    }
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/patch/")]
+
pub struct Review {
+
    #[ts(as = "String")]
+
    id: identity::PublicKey,
+
    author: cobs::Author,
+
    #[ts(type = "'accept' | 'reject'")]
+
    #[ts(optional)]
+
    verdict: Option<cob::patch::Verdict>,
+
    #[ts(optional)]
+
    summary: Option<String>,
+
    #[ts(as = "Option<_>", optional)]
+
    comments: Vec<cobs::thread::Comment<cobs::thread::CodeLocation>>,
+
    #[ts(type = "number")]
+
    timestamp: cob::common::Timestamp,
+
}
+

+
impl Review {
+
    pub fn new(
+
        id: identity::PublicKey,
+
        review: cob::patch::Review,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            id,
+
            author: cobs::Author::new(review.author().id, aliases),
+
            verdict: review.verdict(),
+
            summary: review.summary().map(|s| s.to_string()),
+
            comments: review
+
                .comments()
+
                .map(|(id, c)| {
+
                    cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
+
                        *id,
+
                        c.clone(),
+
                        aliases,
+
                    )
+
                })
+
                .collect::<Vec<_>>(),
+
            timestamp: review.timestamp(),
+
        }
+
    }
+
}
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[serde(tag = "type", rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/patch/")]
+
pub enum Action {
+
    #[serde(rename = "edit")]
+
    Edit {
+
        title: String,
+
        #[ts(as = "String")]
+
        target: patch::MergeTarget,
+
    },
+
    #[serde(rename = "label")]
+
    Label {
+
        #[ts(as = "Vec<String>")]
+
        labels: BTreeSet<cob::Label>,
+
    },
+
    #[serde(rename = "lifecycle")]
+
    Lifecycle {
+
        #[ts(type = "{ status: 'draft' | 'open' | 'archived' }")]
+
        state: patch::Lifecycle,
+
    },
+
    #[serde(rename = "assign")]
+
    Assign {
+
        #[ts(as = "Vec<String>")]
+
        assignees: BTreeSet<identity::Did>,
+
    },
+
    #[serde(rename = "merge")]
+
    Merge {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[ts(as = "String")]
+
        commit: git::Oid,
+
    },
+

+
    #[serde(rename = "review")]
+
    Review {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(optional)]
+
        summary: Option<String>,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(as = "Option<String>", optional)]
+
        verdict: Option<patch::Verdict>,
+
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
        #[ts(as = "Option<Vec<String>>", optional)]
+
        labels: Vec<cob::Label>,
+
    },
+
    #[serde(rename = "review.edit")]
+
    ReviewEdit {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(optional)]
+
        summary: Option<String>,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(as = "Option<String>", optional)]
+
        verdict: Option<patch::Verdict>,
+
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
        #[ts(as = "Option<Vec<String>>", optional)]
+
        labels: Vec<cob::Label>,
+
    },
+
    #[serde(rename = "review.redact")]
+
    ReviewRedact {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
    },
+
    #[serde(rename = "review.comment")]
+
    ReviewComment {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
        body: String,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(optional)]
+
        location: Option<cobs::thread::CodeLocation>,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(as = "Option<String>", optional)]
+
        reply_to: Option<cob::thread::CommentId>,
+
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
        #[ts(as = "Option<_>", optional)]
+
        embeds: Vec<cobs::thread::Embed>,
+
    },
+
    #[serde(rename = "review.comment.edit")]
+
    ReviewCommentEdit {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
        #[ts(as = "String")]
+
        comment: cob::EntryId,
+
        body: String,
+
        #[ts(as = "Option<_>", optional)]
+
        embeds: Vec<cobs::thread::Embed>,
+
    },
+
    #[serde(rename = "review.comment.redact")]
+
    ReviewCommentRedact {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
        #[ts(as = "String")]
+
        comment: cob::EntryId,
+
    },
+
    #[serde(rename = "review.comment.react")]
+
    ReviewCommentReact {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
        #[ts(as = "String")]
+
        comment: cob::EntryId,
+
        #[ts(as = "String")]
+
        reaction: cob::Reaction,
+
        active: bool,
+
    },
+
    #[serde(rename = "review.comment.resolve")]
+
    ReviewCommentResolve {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
        #[ts(as = "String")]
+
        comment: cob::EntryId,
+
    },
+
    #[serde(rename = "review.comment.unresolve")]
+
    ReviewCommentUnresolve {
+
        #[ts(as = "String")]
+
        review: patch::ReviewId,
+
        #[ts(as = "String")]
+
        comment: cob::EntryId,
+
    },
+

+
    #[serde(rename = "revision")]
+
    Revision {
+
        description: String,
+
        #[ts(as = "String")]
+
        base: git::Oid,
+
        #[ts(as = "String")]
+
        oid: git::Oid,
+
        #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
+
        #[ts(as = "Option<BTreeSet<(String, String)>>", optional)]
+
        resolves: BTreeSet<(cob::EntryId, cob::thread::CommentId)>,
+
    },
+
    #[serde(rename = "revision.edit")]
+
    RevisionEdit {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        description: String,
+
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
        #[ts(as = "Option<_>", optional)]
+
        embeds: Vec<cobs::thread::Embed>,
+
    },
+
    #[serde(rename = "revision.react")]
+
    RevisionReact {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(optional)]
+
        location: Option<cobs::thread::CodeLocation>,
+
        #[ts(as = "String")]
+
        reaction: cob::Reaction,
+
        active: bool,
+
    },
+
    #[serde(rename = "revision.redact")]
+
    RevisionRedact {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
    },
+
    #[serde(rename_all = "camelCase")]
+
    #[serde(rename = "revision.comment")]
+
    RevisionComment {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(optional)]
+
        location: Option<cobs::thread::CodeLocation>,
+
        body: String,
+
        #[serde(default, skip_serializing_if = "Option::is_none")]
+
        #[ts(as = "Option<String>", optional)]
+
        reply_to: Option<cob::thread::CommentId>,
+
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
        #[ts(as = "Option<_>", optional)]
+
        embeds: Vec<cobs::thread::Embed>,
+
    },
+
    #[serde(rename = "revision.comment.edit")]
+
    RevisionCommentEdit {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[ts(as = "String")]
+
        comment: cob::thread::CommentId,
+
        body: String,
+
        #[ts(as = "Option<_>", optional)]
+
        embeds: Vec<cobs::thread::Embed>,
+
    },
+
    #[serde(rename = "revision.comment.redact")]
+
    RevisionCommentRedact {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[ts(as = "String")]
+
        comment: cob::thread::CommentId,
+
    },
+
    #[serde(rename = "revision.comment.react")]
+
    RevisionCommentReact {
+
        #[ts(as = "String")]
+
        revision: patch::RevisionId,
+
        #[ts(as = "String")]
+
        comment: cob::thread::CommentId,
+
        #[ts(as = "String")]
+
        reaction: cob::Reaction,
+
        active: bool,
+
    },
+
}
added crates/radicle-types/src/cobs/thread.rs
@@ -0,0 +1,282 @@
+
use serde::{Deserialize, Serialize};
+
use ts_rs::TS;
+

+
use radicle::node::AliasStore;
+
use radicle::{cob, crypto, git};
+

+
use crate::cobs;
+

+
#[derive(TS, Serialize, Deserialize)]
+
#[ts(export)]
+
#[ts(export_to = "cob/thread/")]
+
#[serde(rename_all = "camelCase")]
+
pub struct CreateReviewComment {
+
    #[ts(as = "String")]
+
    pub review_id: cob::patch::ReviewId,
+
    pub body: String,
+
    #[ts(as = "Option<String>", optional)]
+
    pub reply_to: Option<cob::thread::CommentId>,
+
    #[ts(as = "Option<CodeLocation>", optional)]
+
    pub location: Option<CodeLocation>,
+
    #[ts(as = "Option<_>", optional)]
+
    pub embeds: Vec<Embed>,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/thread/")]
+
pub struct Comment<T = cobs::Never> {
+
    #[ts(as = "String")]
+
    id: cob::thread::CommentId,
+
    author: cobs::Author,
+
    edits: Vec<cobs::patch::Edit>,
+
    reactions: Vec<cobs::thread::Reaction>,
+
    #[ts(as = "Option<String>", optional)]
+
    reply_to: Option<cob::thread::CommentId>,
+
    #[ts(optional)]
+
    location: Option<T>,
+
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+
    #[ts(as = "Option<_>", optional)]
+
    embeds: Vec<Embed>,
+
    resolved: bool,
+
}
+

+
impl Comment<CodeLocation> {
+
    pub fn new(
+
        id: cob::thread::CommentId,
+
        comment: cob::thread::Comment<cob::common::CodeLocation>,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            id,
+
            author: cobs::Author::new(comment.author().into(), aliases),
+
            edits: comment
+
                .edits()
+
                .map(|e| cobs::patch::Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            reactions: comment
+
                .reactions()
+
                .into_iter()
+
                .map(|(reaction, authors)| {
+
                    cobs::thread::Reaction::new(*reaction, authors, None, aliases)
+
                })
+
                .collect::<Vec<_>>(),
+
            reply_to: comment.reply_to(),
+
            location: comment.location().map(|l| CodeLocation::new(l.clone())),
+
            embeds: comment
+
                .embeds()
+
                .iter()
+
                .cloned()
+
                .map(|e| e.into())
+
                .collect::<Vec<_>>(),
+
            resolved: comment.is_resolved(),
+
        }
+
    }
+
}
+

+
impl Comment<cobs::Never> {
+
    pub fn new(
+
        id: cob::thread::CommentId,
+
        comment: cob::thread::Comment,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            id,
+
            author: cobs::Author::new(comment.author().into(), aliases),
+
            edits: comment
+
                .edits()
+
                .map(|e| cobs::patch::Edit::new(e, aliases))
+
                .collect::<Vec<_>>(),
+
            reactions: comment
+
                .reactions()
+
                .into_iter()
+
                .map(|(reaction, authors)| {
+
                    cobs::thread::Reaction::new(*reaction, authors, None, aliases)
+
                })
+
                .collect::<Vec<_>>(),
+
            reply_to: comment.reply_to(),
+
            location: None,
+
            embeds: comment
+
                .embeds()
+
                .iter()
+
                .cloned()
+
                .map(|e| e.into())
+
                .collect::<Vec<_>>(),
+
            resolved: comment.is_resolved(),
+
        }
+
    }
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/")]
+
pub struct Reaction {
+
    #[ts(as = "String")]
+
    emoji: cob::Reaction,
+
    authors: Vec<cobs::Author>,
+
    #[ts(optional)]
+
    location: Option<CodeLocation>,
+
}
+

+
impl Reaction {
+
    pub fn new(
+
        emoji: cob::Reaction,
+
        authors: Vec<&crypto::PublicKey>,
+
        location: Option<CodeLocation>,
+
        aliases: &impl AliasStore,
+
    ) -> Self {
+
        Self {
+
            emoji,
+
            authors: authors
+
                .into_iter()
+
                .map(|a| cobs::Author::new(a.into(), aliases))
+
                .collect::<Vec<_>>(),
+
            location,
+
        }
+
    }
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/thread/")]
+
pub struct NewIssueComment {
+
    #[ts(as = "String")]
+
    pub id: git::Oid,
+
    pub body: String,
+
    #[serde(default)]
+
    #[ts(as = "Option<String>", optional)]
+
    pub reply_to: Option<cob::thread::CommentId>,
+
    #[serde(default)]
+
    #[ts(as = "Option<_>", optional)]
+
    pub embeds: Vec<Embed>,
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/thread/")]
+
pub struct NewPatchComment {
+
    #[ts(as = "String")]
+
    pub id: git::Oid,
+
    #[ts(as = "String")]
+
    pub revision: git::Oid,
+
    pub body: String,
+
    #[serde(default)]
+
    #[ts(as = "Option<String>", optional)]
+
    pub reply_to: Option<cob::thread::CommentId>,
+
    #[serde(default)]
+
    #[ts(optional)]
+
    pub location: Option<CodeLocation>,
+
    #[serde(default)]
+
    #[ts(as = "Option<_>", optional)]
+
    pub embeds: Vec<Embed>,
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "cob/thread/")]
+
pub struct CodeLocation {
+
    #[ts(as = "String")]
+
    commit: git::Oid,
+
    path: std::path::PathBuf,
+
    old: Option<CodeRange>,
+
    new: Option<CodeRange>,
+
}
+

+
impl From<cob::CodeLocation> for CodeLocation {
+
    fn from(val: cob::CodeLocation) -> Self {
+
        Self {
+
            commit: val.commit,
+
            path: val.path,
+
            old: val.old.map(|o| o.into()),
+
            new: val.new.map(|o| o.into()),
+
        }
+
    }
+
}
+

+
impl CodeLocation {
+
    pub fn new(location: cob::common::CodeLocation) -> Self {
+
        Self {
+
            commit: location.commit,
+
            path: location.path,
+
            old: location.old.map(|l| l.into()),
+
            new: location.new.map(|l| l.into()),
+
        }
+
    }
+
}
+

+
impl From<CodeLocation> for cob::CodeLocation {
+
    fn from(val: CodeLocation) -> Self {
+
        Self {
+
            commit: val.commit,
+
            path: val.path,
+
            old: val.old.map(|o| o.into()),
+
            new: val.new.map(|o| o.into()),
+
        }
+
    }
+
}
+

+
#[derive(Clone, TS, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase", tag = "type")]
+
#[ts(export)]
+
#[ts(export_to = "cob/thread/")]
+
pub enum CodeRange {
+
    Lines {
+
        #[ts(type = "{ start: number, end: number }")]
+
        range: std::ops::Range<usize>,
+
    },
+
    Chars {
+
        line: usize,
+
        #[ts(type = "{ start: number, end: number }")]
+
        range: std::ops::Range<usize>,
+
    },
+
}
+

+
impl From<cob::CodeRange> for CodeRange {
+
    fn from(val: cob::CodeRange) -> Self {
+
        match val {
+
            cob::CodeRange::Chars { line, range } => Self::Chars { line, range },
+
            cob::CodeRange::Lines { range } => Self::Lines { range },
+
        }
+
    }
+
}
+

+
impl From<CodeRange> for cob::CodeRange {
+
    fn from(val: CodeRange) -> Self {
+
        match val {
+
            CodeRange::Chars { line, range } => Self::Chars { line, range },
+
            CodeRange::Lines { range } => Self::Lines { range },
+
        }
+
    }
+
}
+

+
#[derive(TS, Clone, Deserialize, Serialize)]
+
#[ts(export)]
+
#[ts(export_to = "cob/thread/")]
+
pub struct Embed {
+
    name: String,
+
    #[ts(as = "String")]
+
    content: cob::Uri,
+
}
+

+
impl From<cob::Embed<cob::Uri>> for Embed {
+
    fn from(value: cob::Embed<cob::Uri>) -> Self {
+
        Self {
+
            name: value.name,
+
            content: value.content,
+
        }
+
    }
+
}
+

+
impl From<Embed> for cob::Embed<cob::Uri> {
+
    fn from(value: Embed) -> Self {
+
        Self {
+
            name: value.name,
+
            content: value.content,
+
        }
+
    }
+
}
modified crates/radicle-types/src/config.rs
@@ -9,6 +9,7 @@ use radicle::node::Alias;
#[derive(TS, Serialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
+
#[ts(export_to = "config/")]
pub struct Config {
    /// Node Public Key in NID format.
    #[ts(as = "String")]
added crates/radicle-types/src/error.rs
@@ -0,0 +1,26 @@
+
use serde::Serialize;
+

+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    /// Serde JSON error.
+
    #[error(transparent)]
+
    SerdeJSON(#[from] serde_json::error::Error),
+
}
+

+
#[derive(Serialize)]
+
struct ErrorWrapper {
+
    err: String,
+
}
+

+
impl Serialize for Error {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: serde::ser::Serializer,
+
    {
+
        let wrapper = ErrorWrapper {
+
            err: self.to_string(),
+
        };
+

+
        wrapper.serialize(serializer)
+
    }
+
}
modified crates/radicle-types/src/lib.rs
@@ -1,4 +1,4 @@
pub mod cobs;
pub mod config;
+
pub mod error;
pub mod repo;
-
pub mod thread;
modified crates/radicle-types/src/repo.rs
@@ -1,52 +1,114 @@
-
use serde::Serialize;
-
use serde_json::Value;
+
use std::collections::BTreeSet;
+

+
use serde::{Deserialize, Serialize};
use ts_rs::TS;

-
use radicle::identity::RepoId;
+
use radicle::{git, identity, issue, patch};

-
use super::cobs::Author;
+
use crate::cobs::Author;
+
use crate::error;

-
/// Repos info.
#[derive(Serialize, TS)]
#[ts(export)]
+
#[ts(export_to = "repo/")]
pub struct RepoInfo {
    pub payloads: SupportedPayloads,
-
    #[ts(as = "Vec<Author>")]
    pub delegates: Vec<Author>,
    pub threshold: usize,
-
    #[ts(type = "{ type: 'public' } | { type: 'private', allow?: string[] }")]
-
    pub visibility: radicle::identity::Visibility,
+
    pub visibility: Visibility,
    #[ts(as = "String")]
-
    pub rid: RepoId,
+
    pub rid: identity::RepoId,
    pub seeding: usize,
}

+
#[derive(Default, Serialize, TS)]
+
#[serde(rename_all = "camelCase", tag = "type")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
+
pub enum Visibility {
+
    /// Anyone and everyone.
+
    #[default]
+
    Public,
+
    /// Delegates plus the allowed DIDs.
+
    Private {
+
        #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
+
        #[ts(as = "Option<BTreeSet<String>>", optional)]
+
        allow: BTreeSet<identity::Did>,
+
    },
+
}
+

+
impl From<identity::Visibility> for Visibility {
+
    fn from(value: identity::Visibility) -> Self {
+
        match value {
+
            identity::Visibility::Private { allow } => Self::Private { allow },
+
            identity::Visibility::Public => Self::Public,
+
        }
+
    }
+
}
+

+
impl From<Visibility> for identity::Visibility {
+
    fn from(value: Visibility) -> Self {
+
        match value {
+
            Visibility::Private { allow } => Self::Private { allow },
+
            Visibility::Public => Self::Public,
+
        }
+
    }
+
}
+

#[derive(Serialize, TS)]
#[ts(export)]
+
#[ts(export_to = "repo/")]
pub struct SupportedPayloads {
    #[serde(rename = "xyz.radicle.project")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[ts(optional)]
-
    #[ts(type = r#"{
-
  data: {
-
    defaultBranch: string,
-
    description: string,
-
    name: string,
-
  },
-
  meta: {
-
    head: string,
-
    issues: {
-
      open: number,
-
      closed: number,
-
    },
-
    patches: {
-
      open: number,
-
      draft: number,
-
      archived: number,
-
      merged: number,
+
    pub project: Option<ProjectPayload>,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
+
pub struct ProjectPayload {
+
    data: ProjectPayloadData,
+
    meta: ProjectPayloadMeta,
+
}
+

+
impl ProjectPayload {
+
    pub fn new(data: ProjectPayloadData, meta: ProjectPayloadMeta) -> Self {
+
        Self { data, meta }
+
    }
+
}
+

+
impl TryFrom<identity::doc::Payload> for ProjectPayloadData {
+
    type Error = error::Error;
+

+
    fn try_from(value: identity::doc::Payload) -> Result<Self, Self::Error> {
+
        serde_json::from_value::<Self>((*value).clone()).map_err(error::Error::from)
    }
-
    lastCommit: number,
-
  }
-
}"#)]
-
    pub project: Option<Value>,
+
}
+

+
#[derive(Serialize, Deserialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
+
pub struct ProjectPayloadData {
+
    pub default_branch: String,
+
    pub description: String,
+
    pub name: String,
+
}
+

+
#[derive(Serialize, TS)]
+
#[serde(rename_all = "camelCase")]
+
#[ts(export)]
+
#[ts(export_to = "repo/")]
+
pub struct ProjectPayloadMeta {
+
    #[ts(as = "String")]
+
    pub head: git::Oid,
+
    #[ts(type = "{ open: number, closed: number }")]
+
    pub issues: issue::IssueCounts,
+
    #[ts(type = "{ open: number, draft: number, archived: number, merged: number }")]
+
    pub patches: patch::PatchCounts,
+
    #[ts(type = "number")]
+
    pub last_commit_timestamp: i64,
}
deleted crates/radicle-types/src/thread.rs
@@ -1,200 +0,0 @@
-
use serde::{Deserialize, Serialize};
-
use ts_rs::TS;
-

-
use radicle::node::AliasStore;
-
use radicle::{cob, git};
-

-
use crate::cobs;
-
use crate::thread;
-

-
#[derive(TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct CreateReviewComment {
-
    #[ts(as = "String")]
-
    pub review_id: cob::patch::ReviewId,
-
    pub body: String,
-
    #[ts(as = "Option<String>")]
-
    pub reply_to: Option<cob::thread::CommentId>,
-
    pub location: Option<thread::CodeLocation>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(Serialize, TS)]
-
#[serde(rename_all = "camelCase")]
-
pub struct Comment<T> {
-
    #[ts(as = "String")]
-
    id: cob::thread::CommentId,
-
    author: cobs::Author,
-
    edits: Vec<cobs::Edit>,
-
    reactions: Vec<cobs::Reaction>,
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
-
    reply_to: Option<cob::thread::CommentId>,
-
    #[ts(optional)]
-
    location: Option<T>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    embeds: Vec<cob::Embed<cob::Uri>>,
-
    resolved: bool,
-
}
-

-
impl Comment<CodeLocation> {
-
    pub fn new(
-
        id: cob::thread::CommentId,
-
        comment: cob::thread::Comment<cob::common::CodeLocation>,
-
        aliases: &impl AliasStore,
-
    ) -> Self {
-
        Self {
-
            id,
-
            author: cobs::Author::new(comment.author().into(), aliases),
-
            edits: comment
-
                .edits()
-
                .map(|e| cobs::Edit::new(e, aliases))
-
                .collect::<Vec<_>>(),
-
            reactions: comment
-
                .reactions()
-
                .into_iter()
-
                .map(|(reaction, authors)| cobs::Reaction::new(*reaction, authors, None, aliases))
-
                .collect::<Vec<_>>(),
-
            reply_to: comment.reply_to(),
-
            location: comment.location().map(|l| CodeLocation::new(l.clone())),
-
            embeds: comment.embeds().to_vec(),
-
            resolved: comment.is_resolved(),
-
        }
-
    }
-
}
-

-
impl Comment<cobs::Never> {
-
    pub fn new(
-
        id: cob::thread::CommentId,
-
        comment: cob::thread::Comment,
-
        aliases: &impl AliasStore,
-
    ) -> Self {
-
        Self {
-
            id,
-
            author: cobs::Author::new(comment.author().into(), aliases),
-
            edits: comment
-
                .edits()
-
                .map(|e| cobs::Edit::new(e, aliases))
-
                .collect::<Vec<_>>(),
-
            reactions: comment
-
                .reactions()
-
                .into_iter()
-
                .map(|(reaction, authors)| cobs::Reaction::new(*reaction, authors, None, aliases))
-
                .collect::<Vec<_>>(),
-
            reply_to: comment.reply_to(),
-
            location: None,
-
            embeds: comment.embeds().to_vec(),
-
            resolved: comment.is_resolved(),
-
        }
-
    }
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct NewIssueComment {
-
    #[ts(as = "String")]
-
    pub id: git::Oid,
-
    pub body: String,
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
-
    pub reply_to: Option<cob::thread::CommentId>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[ts(export)]
-
#[serde(rename_all = "camelCase")]
-
pub struct NewPatchComment {
-
    #[ts(as = "String")]
-
    pub id: git::Oid,
-
    #[ts(as = "String")]
-
    pub revision: git::Oid,
-
    pub body: String,
-
    #[ts(as = "Option<String>")]
-
    #[ts(optional)]
-
    pub reply_to: Option<cob::thread::CommentId>,
-
    pub location: Option<CodeLocation>,
-
    #[ts(type = "{ name: string, content: string }[]")]
-
    pub embeds: Vec<cob::Embed<cob::Uri>>,
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase")]
-
#[ts(export)]
-
pub struct CodeLocation {
-
    #[ts(as = "String")]
-
    commit: git::Oid,
-
    path: std::path::PathBuf,
-
    old: Option<CodeRange>,
-
    new: Option<CodeRange>,
-
}
-

-
impl From<cob::CodeLocation> for CodeLocation {
-
    fn from(val: cob::CodeLocation) -> Self {
-
        Self {
-
            commit: val.commit,
-
            path: val.path,
-
            old: val.old.map(|o| o.into()),
-
            new: val.new.map(|o| o.into()),
-
        }
-
    }
-
}
-

-
impl CodeLocation {
-
    pub fn new(location: cob::common::CodeLocation) -> Self {
-
        Self {
-
            commit: location.commit,
-
            path: location.path,
-
            old: location.old.map(|l| l.into()),
-
            new: location.new.map(|l| l.into()),
-
        }
-
    }
-
}
-

-
impl From<CodeLocation> for cob::CodeLocation {
-
    fn from(val: CodeLocation) -> Self {
-
        Self {
-
            commit: val.commit,
-
            path: val.path,
-
            old: val.old.map(|o| o.into()),
-
            new: val.new.map(|o| o.into()),
-
        }
-
    }
-
}
-

-
#[derive(Clone, TS, Serialize, Deserialize)]
-
#[serde(rename_all = "camelCase", tag = "type")]
-
#[ts(export)]
-
pub enum CodeRange {
-
    Lines {
-
        #[ts(type = "{ start: number, end: number }")]
-
        range: std::ops::Range<usize>,
-
    },
-
    Chars {
-
        line: usize,
-
        #[ts(type = "{ start: number, end: number }")]
-
        range: std::ops::Range<usize>,
-
    },
-
}
-

-
impl From<cob::CodeRange> for CodeRange {
-
    fn from(val: cob::CodeRange) -> Self {
-
        match val {
-
            cob::CodeRange::Chars { line, range } => Self::Chars { line, range },
-
            cob::CodeRange::Lines { range } => Self::Lines { range },
-
        }
-
    }
-
}
-

-
impl From<CodeRange> for cob::CodeRange {
-
    fn from(val: CodeRange) -> Self {
-
        match val {
-
            CodeRange::Chars { line, range } => Self::Chars { line, range },
-
            CodeRange::Lines { range } => Self::Lines { range },
-
        }
-
    }
-
}
modified eslint.config.js
@@ -139,6 +139,7 @@ export default [
      "node_modules/**/*",
      "target/*",
      "crates/radicle-tauri/**/*",
+
      "crates/radicle-types/**/*",
      "eslint.config.js",
    ],
  },
modified package.json
@@ -13,7 +13,7 @@
    "check-js": "scripts/check-js",
    "check-rs": "scripts/check-rs",
    "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
-
    "generate-types": "cargo test --manifest-path ./crates/radicle-tauri/Cargo.toml && npx prettier ./crates/radicle-tauri/bindings --write",
+
    "generate-types": "cargo test --manifest-path ./crates/radicle-types/Cargo.toml && npx prettier ./crates/radicle-types/bindings --write",
    "tauri": "npx tauri"
  },
  "engines": {
modified src/components/DiffStatBadge.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Stats } from "@bindings/Stats";
+
  import type { Stats } from "@bindings/cob/Stats";

  export let stats: Stats;
</script>
modified src/components/IssueTeaser.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { Issue } from "@bindings/Issue";
+
  import type { Issue } from "@bindings/cob/issue/Issue";

  import {
    authorForNodeId,
modified src/components/PatchTeaser.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
-
  import type { Patch } from "@bindings/Patch";
-
  import type { Stats } from "@bindings/Stats";
+
  import type { Patch } from "@bindings/cob/patch/Patch";
+
  import type { Stats } from "@bindings/cob/Stats";

  import {
    authorForNodeId,
modified src/components/RepoCard.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { RepoInfo } from "@bindings/RepoInfo";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import { formatRepositoryId, formatTimestamp } from "@app/lib/utils";

@@ -59,7 +59,7 @@
        </div>
      </div>
      <span style:color="var(--color-fill-gray)">
-
        Updated {formatTimestamp(project.meta.lastCommit)}
+
        Updated {formatTimestamp(project.meta.lastCommitTimestamp)}
      </span>
    </div>
  </div>
modified src/components/RepoHeader.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { RepoInfo } from "@bindings/RepoInfo";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import Icon from "./Icon.svelte";

modified src/lib/router/definitions.ts
@@ -1,5 +1,5 @@
-
import type { Config } from "@bindings/Config";
-
import type { RepoInfo } from "@bindings/RepoInfo";
+
import type { Config } from "@bindings/config/Config";
+
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { LoadedRepoRoute, RepoRoute } from "@app/views/repo/router";

import { invoke } from "@tauri-apps/api/core";
modified src/lib/utils.ts
@@ -1,8 +1,8 @@
import type { ComponentProps } from "svelte";

-
import type { Author } from "@bindings/Author";
-
import type { Issue } from "@bindings/Issue";
-
import type { Patch } from "@bindings/Patch";
+
import type { Author } from "@bindings/cob/Author";
+
import type { Issue } from "@bindings/cob/issue/Issue";
+
import type { Patch } from "@bindings/cob/patch/Patch";

import bs58 from "bs58";
import twemojiModule from "twemoji";
modified src/views/Home.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
-
  import type { Config } from "@bindings/Config";
-
  import type { RepoInfo } from "@bindings/RepoInfo";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import * as router from "@app/lib/router";

modified src/views/repo/CreateIssue.svelte
@@ -1,8 +1,8 @@
<script lang="ts">
-
  import type { Author } from "@bindings/Author";
-
  import type { Config } from "@bindings/Config";
-
  import type { Issue } from "@bindings/Issue";
-
  import type { RepoInfo } from "@bindings/RepoInfo";
+
  import type { Author } from "@bindings/cob/Author";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { Issue } from "@bindings/cob/issue/Issue";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import { invoke } from "@tauri-apps/api/core";

modified src/views/repo/Issue.svelte
@@ -1,8 +1,8 @@
<script lang="ts">
-
  import type { Config } from "@bindings/Config";
-
  import type { Issue } from "@bindings/Issue";
-
  import type { RepoInfo } from "@bindings/RepoInfo";
-
  import type { IssueOp } from "@bindings/IssueOp";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { Issue } from "@bindings/cob/issue/Issue";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+
  import type { Operation } from "@bindings/cob/issue/Operation";

  import capitalize from "lodash/capitalize";

@@ -221,7 +221,7 @@
      {/if}
    </div>
    <div>
-
      {#await invoke<IssueOp[]>( "activity_by_id", { rid: repo.rid, typeName: "xyz.radicle.issue", id: issue.id }, ) then activity}
+
      {#await invoke<Operation[]>( "activity_by_id", { rid: repo.rid, typeName: "xyz.radicle.issue", id: issue.id }, ) then activity}
        {#each activity.slice(1) as op}
          {#if op.type === "lifecycle"}
            <div class="txt-small body">
modified src/views/repo/Issues.svelte
@@ -1,8 +1,8 @@
<script lang="ts">
-
  import type { Config } from "@bindings/Config";
-
  import type { Issue } from "@bindings/Issue";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { Issue } from "@bindings/cob/issue/Issue";
  import type { IssueStatus } from "./router";
-
  import type { RepoInfo } from "@bindings/RepoInfo";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import * as router from "@app/lib/router";

modified src/views/repo/Patch.svelte
@@ -1,8 +1,8 @@
<script lang="ts">
-
  import type { Config } from "@bindings/Config";
-
  import type { Patch } from "@bindings/Patch";
-
  import type { RepoInfo } from "@bindings/RepoInfo";
-
  import type { Revision } from "@bindings/Revision";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { Patch } from "@bindings/cob/patch/Patch";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+
  import type { Revision } from "@bindings/cob/patch/Revision";

  import {
    authorForNodeId,
modified src/views/repo/Patches.svelte
@@ -1,9 +1,9 @@
<script lang="ts">
-
  import type { Config } from "@bindings/Config";
-
  import type { PaginatedQuery } from "@bindings/PaginatedQuery";
-
  import type { Patch } from "@bindings/Patch";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
+
  import type { Patch } from "@bindings/cob/patch/Patch";
  import type { PatchStatus } from "./router";
-
  import type { RepoInfo } from "@bindings/RepoInfo";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import { invoke } from "@tauri-apps/api/core";

modified src/views/repo/router.ts
@@ -1,9 +1,9 @@
-
import type { Config } from "@bindings/Config";
-
import type { PaginatedQuery } from "@bindings/PaginatedQuery";
-
import type { Issue } from "@bindings/Issue";
-
import type { Patch } from "@bindings/Patch";
-
import type { RepoInfo } from "@bindings/RepoInfo";
-
import type { Revision } from "@bindings/Revision";
+
import type { Config } from "@bindings/config/Config";
+
import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
+
import type { Issue } from "@bindings/cob/issue/Issue";
+
import type { Patch } from "@bindings/cob/patch/Patch";
+
import type { RepoInfo } from "@bindings/repo/RepoInfo";
+
import type { Revision } from "@bindings/cob/patch/Revision";

import { invoke } from "@tauri-apps/api/core";
import { unreachable } from "@app/lib/utils";
modified tsconfig.json
@@ -20,7 +20,7 @@
    "skipLibCheck": true,
    "paths": {
      "@app/*": ["./src/*"],
-
      "@bindings/*": ["./crates/radicle-tauri/bindings/*"],
+
      "@bindings/*": ["./crates/radicle-types/bindings/*"],
      "@public/*": ["./public/*"]
    }
  }
modified vite.config.ts
@@ -20,7 +20,7 @@ export default defineConfig({
  resolve: {
    alias: {
      "@app": path.resolve("./src"),
-
      "@bindings": path.resolve("./crates/radicle-tauri/bindings/"),
+
      "@bindings": path.resolve("./crates/radicle-types/bindings/"),
      "@public": path.resolve("./public"),
    },
  },