Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Update the UI to work with the new COB schema
Rūdolfs Ošiņš committed 2 years ago
commit 0146ca00b5cdd38eafd846e0b500a37c63df66e1
parent 0d024759d3225e6adb5b3dbabf1b81bd7b7bfbfd
27 files changed +335 -315
modified httpd-client/index.ts
@@ -17,9 +17,12 @@ import type {
} from "./lib/project/commit.js";
import type { Issue, IssueState } from "./lib/project/issue.js";
import type {
+
  CodeLocation,
+
  LifecycleState,
  Merge,
  Patch,
  PatchState,
+
  Range,
  Review,
  Revision,
} from "./lib/project/patch.js";
@@ -35,6 +38,7 @@ import { Fetcher } from "./lib/fetcher.js";
export type {
  BaseUrl,
  Blob,
+
  CodeLocation,
  Comment,
  Commit,
  CommitHeader,
@@ -45,10 +49,12 @@ export type {
  HunkLine,
  Issue,
  IssueState,
+
  LifecycleState,
  Merge,
  Patch,
  PatchState,
  Project,
+
  Range,
  Remote,
  Review,
  Revision,
modified httpd-client/lib/project.ts
@@ -407,7 +407,7 @@ export class Client {
      title: string;
      description: string;
      assignees: string[];
-
      tags: string[];
+
      labels: string[];
    },
    authToken: string,
    options?: RequestOptions,
modified httpd-client/lib/project/comment.ts
@@ -1,17 +1,6 @@
import type { ZodSchema } from "zod";
import { array, number, object, string, tuple } from "zod";

-
export type ThreadUpdateAction =
-
  | { type: "comment"; body: string; replyTo?: string }
-
  | { type: "edit"; id: string; body: string }
-
  | { type: "redact"; id: string }
-
  | {
-
      type: "react";
-
      to: string;
-
      reaction: { emoji: string };
-
      active: boolean;
-
    };
-

export interface Comment {
  id: string;
  author: { id: string; alias?: string };
modified httpd-client/lib/project/issue.ts
@@ -1,4 +1,4 @@
-
import type { Comment, ThreadUpdateAction } from "./comment.js";
+
import type { Comment } from "./comment.js";
import type { ZodSchema } from "zod";
import { array, boolean, literal, object, string, union } from "zod";

@@ -22,7 +22,7 @@ export interface Issue {
  title: string;
  state: IssueState;
  discussion: Comment[];
-
  tags: string[];
+
  labels: string[];
  assignees: string[];
}

@@ -32,7 +32,7 @@ export const issueSchema = object({
  title: string(),
  state: issueStateSchema,
  discussion: array(commentSchema),
-
  tags: array(string()),
+
  labels: array(string()),
  assignees: array(string()),
}) satisfies ZodSchema<Issue>;

@@ -49,12 +49,19 @@ export const issueCreatedSchema = object({
export const issuesSchema = array(issueSchema) satisfies ZodSchema<Issue[]>;

export type IssueUpdateAction =
+
  | { type: "edit"; title: string }
+
  | { type: "label"; labels: string[] }
  | {
      type: "assign";
-
      add: string[];
-
      remove: string[];
+
      assignees: string[];
    }
-
  | { type: "edit"; title: string }
  | { type: "lifecycle"; state: IssueState }
-
  | { type: "tag"; add: string[]; remove: string[] }
-
  | { type: "thread"; action: ThreadUpdateAction };
+
  | { type: "comment"; body: string; replyTo: string }
+
  | { type: "comment.edit"; id: string; body: string }
+
  | { type: "comment.redact"; id: string }
+
  | {
+
      type: "comment.react";
+
      id: string;
+
      reaction: string;
+
      active: boolean;
+
    };
modified httpd-client/lib/project/patch.ts
@@ -1,4 +1,4 @@
-
import type { Comment, ThreadUpdateAction } from "./comment.js";
+
import type { Comment } from "./comment.js";
import type { ZodSchema, z } from "zod";

import { commentSchema } from "./comment.js";
@@ -101,9 +101,9 @@ export interface Patch {
  title: string;
  state: PatchState;
  target: string;
-
  tags: string[];
+
  labels: string[];
  merges: Merge[];
-
  reviewers: string[];
+
  assignees: string[];
  revisions: Revision[];
}

@@ -113,37 +113,108 @@ export const patchSchema = object({
  title: string(),
  state: patchStateSchema,
  target: string(),
-
  tags: array(string()),
+
  labels: array(string()),
  merges: array(mergeSchema),
-
  reviewers: array(string()),
+
  assignees: array(string()),
  revisions: array(revisionSchema),
}) satisfies ZodSchema<Patch>;

export const patchesSchema = array(patchSchema) satisfies ZodSchema<Patch[]>;

+
export type LifecycleState =
+
  | { status: "draft" }
+
  | { status: "open" }
+
  | { status: "archived" };
+

+
export type Range =
+
  | {
+
      type: "lines";
+
      range: { start: number; end: number };
+
    }
+
  | {
+
      type: "chars";
+
      line: number;
+
      range: { start: number; end: number };
+
    };
+

+
export type CodeLocation = {
+
  path: string;
+
  old?: Range;
+
  new?: Range;
+
};
+

export type PatchUpdateAction =
-
  | { type: "edit"; title: string; target: string }
-
  | { type: "editRevision"; revision: string; description: string }
-
  | { type: "editReview"; review: string; summary?: string }
-
  | { type: "tag"; add: string[]; remove: string[] }
-
  | { type: "revision"; description: string; base: string; oid: string }
-
  | { type: "lifecycle"; state: PatchState }
-
  | { type: "redact"; revision: string }
+
  | { type: "edit"; title: string; target: "delegates" }
+
  | { type: "label"; labels: string[] }
+
  | { type: "assign"; assignees: string[] }
+
  | { type: "merge"; revision: string; commit: string }
+
  | { type: "lifecycle"; state: LifecycleState }
  | {
      type: "review";
      revision: string;
      summary?: string;
      verdict?: Verdict | null;
    }
-
  | { type: "merge"; revision: string; commit: string }
-
  | { type: "thread"; revision: string; action: ThreadUpdateAction };
+
  | { type: "review.edit"; review: string; summary?: string }
+
  | { type: "review.redact"; review: string }
+
  | {
+
      type: "review.comment";
+
      review: string;
+
      body: string;
+
      location: CodeLocation;
+
    }
+
  | {
+
      type: "review.comment.edit";
+
      review: string;
+
      comment: string;
+
      body: string;
+
    }
+
  | {
+
      type: "review.comment.redact";
+
      review: string;
+
      comment: string;
+
    }
+
  | {
+
      type: "review.comment.react";
+
      review: string;
+
      comment: string;
+
      reaction: string;
+
      active: boolean;
+
    }
+
  | { type: "revision"; description: string; base: string; oid: string }
+
  | { type: "revision.edit"; revision: string; description: string }
+
  | { type: "revision.redact"; revision: string }
+
  | {
+
      type: "revision.comment";
+
      revision: string;
+
      body: string;
+
      replyTo: string;
+
    }
+
  | {
+
      type: "revision.comment.edit";
+
      revision: string;
+
      comment: string;
+
      body: string;
+
    }
+
  | {
+
      type: "revision.comment.redact";
+
      revision: string;
+
      comment: string;
+
    }
+
  | {
+
      type: "revision.comment.react";
+
      revision: string;
+
      comment: string;
+
      reaction: string;
+
      active: boolean;
+
    };

export const patchCreateSchema = object({
  title: string(),
  description: string(),
  target: string(),
  oid: string(),
-
  tags: array(string()),
+
  labels: array(string()),
});

export type PatchCreate = z.infer<typeof patchCreateSchema>;
modified httpd-client/tests/project.test.ts
@@ -98,7 +98,7 @@ describe("project", () => {
  test("#getIssueById(id, issueId)", async () => {
    await api.project.getIssueById(
      cobRid,
-
      "4fc727e722d3979fd2073d9b56b2751658a4ae79",
+
      "9cedac832f0791bea5c9cf8fa32db8a68c592166",
    );
  });

@@ -111,7 +111,7 @@ describe("project", () => {
  });

  testWithAPI(
-
    "#createIssue(id, { title, description, assignees, tags })",
+
    "#createIssue(id, { title, description, assignees, labels })",
    async ({ httpd: { api, peer } }) => {
      const sessionId = await authenticate(api, peer);
      const { id: issueId } = await api.project.createIssue(
@@ -120,7 +120,7 @@ describe("project", () => {
          title: "aaa",
          description: "bbb",
          assignees: [],
-
          tags: ["bug", "documentation"],
+
          labels: ["bug", "documentation"],
        },
        sessionId,
      );
@@ -130,7 +130,7 @@ describe("project", () => {
          title: "aaa",
          discussion: [{ body: "bbb" }],
          assignees: [],
-
          tags: ["bug", "documentation"],
+
          labels: ["bug", "documentation"],
        },
        api,
      );
@@ -153,17 +153,17 @@ describe("project", () => {
  );

  testWithAPI(
-
    "#updateIssue(id, issueId, { type: 'tag' }, authToken)",
+
    "#updateIssue(id, issueId, { type: 'label' }, authToken)",
    async ({ httpd: { api, peer } }) => {
      const sessionId = await authenticate(api, peer);
      const issueId = await createIssueToBeModified(api, sessionId);
      await api.project.updateIssue(
        cobRid,
        issueId,
-
        { type: "tag", add: ["bug"], remove: [] },
+
        { type: "label", labels: ["bug"] },
        sessionId,
      );
-
      await assertIssue(issueId, { tags: ["bug"] }, api);
+
      await assertIssue(issueId, { labels: ["bug"] }, api);
    },
  );

@@ -172,18 +172,16 @@ describe("project", () => {
    async ({ httpd: { api, peer } }) => {
      const sessionId = await authenticate(api, peer);
      const issueId = await createIssueToBeModified(api, sessionId);
-
      const assignee = bobRemote.replace("did:key:", "");
      await api.project.updateIssue(
        cobRid,
        issueId,
        {
          type: "assign",
-
          add: [assignee],
-
          remove: [],
+
          assignees: [bobRemote],
        },
        sessionId,
      );
-
      await assertIssue(issueId, { assignees: [`did:key:${assignee}`] }, api);
+
      await assertIssue(issueId, { assignees: [bobRemote] }, api);
    },
  );

@@ -211,7 +209,7 @@ describe("project", () => {
  test("#getPatchById(id, patchId)", async () => {
    await api.project.getPatchById(
      cobRid,
-
      "013f8b2734df1840b2e33d52ff5632c8d66b199a",
+
      "687c3268119d23c5da32055c0b44c03e0e4088b8",
    );
  });

@@ -230,7 +228,7 @@ describe("project", () => {
          description: "qqq",
          target: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
          oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
-
          tags: [],
+
          labels: [],
        },
        sessionId,
      );
@@ -240,7 +238,7 @@ describe("project", () => {
          title: "ppp",
          state: { status: "open" },
          target: "delegates",
-
          tags: [],
+
          labels: [],
          revisions: [
            {
              description: "qqq",
@@ -262,13 +260,13 @@ describe("project", () => {
      await api.project.updatePatch(
        cobRid,
        patchId,
-
        { type: "tag", add: ["bug"], remove: [] },
+
        { type: "label", labels: ["bug"] },
        sessionId,
      );
      await assertPatch(
        patchId,
        {
-
          tags: ["bug"],
+
          labels: ["bug"],
        },
        api,
      );
modified httpd-client/tests/support/support.ts
@@ -11,7 +11,7 @@ export async function createIssueToBeModified(
) {
  const { id } = await api.project.createIssue(
    cobRid,
-
    { title: "aaa", description: "bbb", assignees: [], tags: [] },
+
    { title: "aaa", description: "bbb", assignees: [], labels: [] },
    sessionId,
  );

@@ -29,7 +29,7 @@ export async function createPatchToBeModified(
      description: "ttt",
      target: "d7dd8cecae16b1108234e09dbdb5d64ae394bc25",
      oid: "38c225e2a0b47ba59def211f4e4825c31d9463ec",
-
      tags: [],
+
      labels: [],
    },
    sessionId,
  );
modified src/config.json
@@ -1,5 +1,4 @@
{
-
  "reactions": ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"],
  "nodes": {
    "defaultHttpdPort": 443,
    "defaultLocalHttpdPort": 8080,
modified src/lib/config.ts
@@ -3,7 +3,6 @@ import type { BaseUrl } from "@httpd-client";
import configJson from "@app/config.json";

export interface Config {
-
  reactions: string[];
  nodes: {
    defaultHttpdPort: number;
    defaultLocalHttpdPort: number;
@@ -23,7 +22,6 @@ export interface Config {
function getConfig(): Config {
  if (window.VITEST) {
    return {
-
      reactions: [],
      nodes: {
        defaultHttpdPort: 8081,
        defaultLocalHttpdPort: 8081,
modified src/lib/utils.ts
@@ -243,21 +243,7 @@ export function twemoji(
  });
}

-
export function createAddRemoveArrays(
-
  currentArray: string[],
-
  newArray: string[],
-
): { add: string[]; remove: string[] } {
-
  return {
-
    add: newArray.filter(item => !currentArray.includes(item)),
-
    remove: currentArray.filter(item => !newArray.includes(item)),
-
  };
-
}
-

// Formats COBs Object Ids
export function formatObjectId(id: string): string {
  return id.substring(0, 7);
}
-

-
export function stripDidPrefix(array: string[]): string[] {
-
  return array.map(id => id.replace("did:key:", ""));
-
}
modified src/views/projects/Cob/AssigneeInput.svelte
@@ -22,10 +22,11 @@

  function addAssignee() {
    if (parsedNodeId) {
-
      if (updatedAssignees.includes(parsedNodeId.pubkey)) {
+
      const assignee = `${parsedNodeId.prefix}${parsedNodeId.pubkey}`;
+
      if (updatedAssignees.includes(assignee)) {
        validationMessage = "This assignee is already added";
      } else {
-
        updatedAssignees = [...updatedAssignees, parsedNodeId.pubkey];
+
        updatedAssignees = [...updatedAssignees, assignee];
        inputValue = "";
        if (action === "create") {
          dispatch("save", updatedAssignees);
added src/views/projects/Cob/LabelInput.svelte
@@ -0,0 +1,117 @@
+
<script lang="ts" strictEvents>
+
  import { createEventDispatcher } from "svelte";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Chip from "@app/components/Chip.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+

+
  const dispatch = createEventDispatcher<{ save: string[] }>();
+

+
  export let action: "create" | "edit" | "view" = "view";
+
  export let editInProgress: boolean = false;
+
  export let labels: string[] = [];
+

+
  let updatedLabels: string[] = labels;
+
  let inputValue = "";
+
  let validationMessage: string | undefined = undefined;
+

+
  $: sanitizedValue = inputValue.trim();
+

+
  function addLabel() {
+
    if (sanitizedValue.length > 0) {
+
      if (updatedLabels.includes(sanitizedValue)) {
+
        validationMessage = "This label is already added";
+
      } else {
+
        updatedLabels = [...updatedLabels, sanitizedValue];
+
        inputValue = "";
+
        if (action === "create") {
+
          dispatch("save", updatedLabels);
+
        }
+
      }
+
    } else {
+
      validationMessage = "This label is not valid";
+
    }
+
  }
+

+
  function removeLabel({ detail: key }: { detail: number }) {
+
    updatedLabels = updatedLabels.filter((_, i) => i !== key);
+
    if (action === "create") {
+
      dispatch("save", updatedLabels);
+
    }
+
  }
+
</script>
+

+
<style>
+
  .metadata-section-header {
+
    display: flex;
+
    gap: 1rem;
+
    align-items: center;
+
    font-size: var(--font-size-small);
+
    margin-bottom: 0.75rem;
+
    color: var(--color-foreground-6);
+
  }
+
  .metadata-section-body {
+
    display: flex;
+
    flex-wrap: wrap;
+
    flex-direction: row;
+
    gap: 0.5rem;
+
    margin-bottom: 1.25rem;
+
  }
+
  .label {
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
    white-space: nowrap;
+
  }
+
</style>
+

+
<div>
+
  <div class="metadata-section-header">
+
    <span>Labels</span>
+
    {#if action === "edit"}
+
      {#if editInProgress}
+
        <Button
+
          size="tiny"
+
          variant="text"
+
          on:click={() => {
+
            dispatch("save", updatedLabels);
+
            editInProgress = !editInProgress;
+
          }}>
+
          save
+
        </Button>
+
      {:else}
+
        <Button
+
          size="tiny"
+
          variant="text"
+
          on:click={() => {
+
            editInProgress = !editInProgress;
+
          }}>
+
          edit
+
        </Button>
+
      {/if}
+
    {/if}
+
  </div>
+
  <div class="metadata-section-body">
+
    {#each updatedLabels as label, key (label)}
+
      <Chip
+
        on:remove={removeLabel}
+
        removeable={editInProgress || action === "create"}
+
        {key}>
+
        <div aria-label="chip" class="label">{label}</div>
+
      </Chip>
+
    {:else}
+
      <div class="txt-missing">No labels</div>
+
    {/each}
+
  </div>
+
  {#if editInProgress || action === "create"}
+
    <div style:margin-bottom="1rem">
+
      <TextInput
+
        bind:value={inputValue}
+
        valid={sanitizedValue.length > 0}
+
        placeholder="Add label"
+
        variant="form"
+
        {validationMessage}
+
        on:submit={addLabel}
+
        on:input={() => (validationMessage = undefined)} />
+
    </div>
+
  {/if}
+
</div>
deleted src/views/projects/Cob/TagInput.svelte
@@ -1,117 +0,0 @@
-
<script lang="ts" strictEvents>
-
  import { createEventDispatcher } from "svelte";
-

-
  import Button from "@app/components/Button.svelte";
-
  import Chip from "@app/components/Chip.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
-

-
  const dispatch = createEventDispatcher<{ save: string[] }>();
-

-
  export let action: "create" | "edit" | "view" = "view";
-
  export let editInProgress: boolean = false;
-
  export let tags: string[] = [];
-

-
  let updatedTags: string[] = tags;
-
  let inputValue = "";
-
  let validationMessage: string | undefined = undefined;
-

-
  $: sanitizedValue = inputValue.trim();
-

-
  function addTag() {
-
    if (sanitizedValue.length > 0) {
-
      if (updatedTags.includes(sanitizedValue)) {
-
        validationMessage = "This tag is already added";
-
      } else {
-
        updatedTags = [...updatedTags, sanitizedValue];
-
        inputValue = "";
-
        if (action === "create") {
-
          dispatch("save", updatedTags);
-
        }
-
      }
-
    } else {
-
      validationMessage = "This tag is not valid";
-
    }
-
  }
-

-
  function removeTag({ detail: key }: { detail: number }) {
-
    updatedTags = updatedTags.filter((_, i) => i !== key);
-
    if (action === "create") {
-
      dispatch("save", updatedTags);
-
    }
-
  }
-
</script>
-

-
<style>
-
  .metadata-section-header {
-
    display: flex;
-
    gap: 1rem;
-
    align-items: center;
-
    font-size: var(--font-size-small);
-
    margin-bottom: 0.75rem;
-
    color: var(--color-foreground-6);
-
  }
-
  .metadata-section-body {
-
    display: flex;
-
    flex-wrap: wrap;
-
    flex-direction: row;
-
    gap: 0.5rem;
-
    margin-bottom: 1.25rem;
-
  }
-
  .tag {
-
    overflow: hidden;
-
    text-overflow: ellipsis;
-
    white-space: nowrap;
-
  }
-
</style>
-

-
<div>
-
  <div class="metadata-section-header">
-
    <span>Tags</span>
-
    {#if action === "edit"}
-
      {#if editInProgress}
-
        <Button
-
          size="tiny"
-
          variant="text"
-
          on:click={() => {
-
            dispatch("save", updatedTags);
-
            editInProgress = !editInProgress;
-
          }}>
-
          save
-
        </Button>
-
      {:else}
-
        <Button
-
          size="tiny"
-
          variant="text"
-
          on:click={() => {
-
            editInProgress = !editInProgress;
-
          }}>
-
          edit
-
        </Button>
-
      {/if}
-
    {/if}
-
  </div>
-
  <div class="metadata-section-body">
-
    {#each updatedTags as tag, key (tag)}
-
      <Chip
-
        on:remove={removeTag}
-
        removeable={editInProgress || action === "create"}
-
        {key}>
-
        <div aria-label="chip" class="tag">{tag}</div>
-
      </Chip>
-
    {:else}
-
      <div class="txt-missing">No tags</div>
-
    {/each}
-
  </div>
-
  {#if editInProgress || action === "create"}
-
    <div style:margin-bottom="1rem">
-
      <TextInput
-
        bind:value={inputValue}
-
        valid={sanitizedValue.length > 0}
-
        placeholder="Add tag"
-
        variant="form"
-
        {validationMessage}
-
        on:submit={addTag}
-
        on:input={() => (validationMessage = undefined)} />
-
    </div>
-
  {/if}
-
</div>
modified src/views/projects/Issue.svelte
@@ -21,7 +21,7 @@
  import ErrorModal from "@app/views/projects/Cob/ErrorModal.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Markdown from "@app/components/Markdown.svelte";
-
  import TagInput from "./Cob/TagInput.svelte";
+
  import LabelInput from "./Cob/LabelInput.svelte";
  import Textarea from "@app/components/Textarea.svelte";
  import ThreadComponent from "@app/components/Thread.svelte";

@@ -56,8 +56,9 @@
        project.id,
        issue.id,
        {
-
          type: "thread",
-
          action: { type: "comment", body: reply.body, replyTo: reply.id },
+
          type: "comment",
+
          body: reply.body,
+
          replyTo: reply.id,
        },
        $httpdStore.session,
        api,
@@ -73,7 +74,7 @@
      const status = await updateIssue(
        project.id,
        issue.id,
-
        { type: "thread", action: { type: "comment", body } },
+
        { type: "comment", body, replyTo: issue.id },
        $httpdStore.session,
        api,
      );
@@ -106,19 +107,17 @@
    }
  }

-
  async function saveTags({ detail: tags }: CustomEvent<string[]>) {
+
  async function saveLabels({ detail: labels }: CustomEvent<string[]>) {
    if ($httpdStore.state === "authenticated") {
-
      const { add, remove } = utils.createAddRemoveArrays(issue.tags, tags);
-
      if (add.length === 0 && remove.length === 0) {
+
      if (isEqual(issue.labels, labels)) {
        return;
      }
      const status = await updateIssue(
        project.id,
        issue.id,
        {
-
          type: "tag",
-
          add,
-
          remove,
+
          type: "label",
+
          labels: labels,
        },
        $httpdStore.session,
        api,
@@ -131,11 +130,7 @@

  async function saveAssignees({ detail: assignees }: CustomEvent<string[]>) {
    if ($httpdStore.state === "authenticated") {
-
      const { add, remove } = utils.createAddRemoveArrays(
-
        issue.assignees,
-
        assignees,
-
      );
-
      if (add.length === 0 && remove.length === 0) {
+
      if (isEqual(issue.assignees, assignees)) {
        return;
      }
      const status = await updateIssue(
@@ -143,8 +138,7 @@
        issue.id,
        {
          type: "assign",
-
          add: utils.stripDidPrefix(add),
-
          remove: utils.stripDidPrefix(remove),
+
          assignees: assignees,
        },
        $httpdStore.session,
        api,
@@ -405,6 +399,6 @@
      {action}
      assignees={issue.assignees}
      on:save={saveAssignees} />
-
    <TagInput {action} tags={issue.tags} on:save={saveTags} />
+
    <LabelInput {action} labels={issue.labels} on:save={saveLabels} />
  </div>
</div>
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -60,12 +60,12 @@
    gap: 0.5rem;
    color: var(--color-foreground-5);
  }
-
  .tags {
+
  .labels {
    display: flex;
    flex-direction: row;
    gap: 0.5rem;
  }
-
  .tag {
+
  .label {
    overflow: hidden;
    text-overflow: ellipsis;
  }
@@ -88,7 +88,7 @@
  }

  @media (max-width: 960px) {
-
    .tags {
+
    .labels {
      display: none;
    }
  }
@@ -114,15 +114,15 @@
          <InlineMarkdown content={issue.title} />
        </span>
      </Link>
-
      <span class="tags">
-
        {#each issue.tags.slice(0, 4) as tag}
+
      <span class="labels">
+
        {#each issue.labels.slice(0, 4) as label}
          <Badge style="max-width:7rem" variant="secondary">
-
            <span class="tag">{tag}</span>
+
            <span class="label">{label}</span>
          </Badge>
        {/each}
-
        {#if issue.tags.length > 4}
+
        {#if issue.labels.length > 4}
          <Badge variant="foreground">
-
            <span class="tag">+{issue.tags.length - 4} more tags</span>
+
            <span class="label">+{issue.labels.length - 4} more labels</span>
          </Badge>
        {/if}
      </span>
modified src/views/projects/Issue/New.svelte
@@ -14,7 +14,7 @@
  import Button from "@app/components/Button.svelte";
  import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
  import Markdown from "@app/components/Markdown.svelte";
-
  import TagInput from "@app/views/projects/Cob/TagInput.svelte";
+
  import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";
  import Textarea from "@app/components/Textarea.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";

@@ -33,7 +33,7 @@
  let issueTitle = "";
  let issueText: string | undefined = undefined;
  let assignees: string[] = [];
-
  let tags: string[] = [];
+
  let labels: string[] = [];

  const api = new HttpdClient(baseUrl);

@@ -44,8 +44,8 @@
        {
          title: issueTitle,
          description: issueText ?? "",
-
          assignees: utils.stripDidPrefix(assignees),
-
          tags: tags,
+
          assignees: assignees,
+
          labels: labels,
        },
        sessionId,
      );
@@ -174,7 +174,7 @@
            {/if}
          </Button>
          <Button
-
            disabled={!issueTitle}
+
            disabled={!issueTitle || !issueText}
            size="small"
            variant="secondary"
            on:click={() => void createIssue(session.id)}>
@@ -187,9 +187,9 @@
          {action}
          on:save={({ detail: updatedAssignees }) =>
            (assignees = updatedAssignees)} />
-
        <TagInput
+
        <LabelInput
          {action}
-
          on:save={({ detail: updatedTags }) => (tags = updatedTags)} />
+
          on:save={({ detail: updatedLabels }) => (labels = updatedLabels)} />
      </div>
    </div>
  {:else}
modified src/views/projects/Patch.svelte
@@ -34,7 +34,7 @@
  import type { PatchView } from "./router";

  import * as utils from "@app/lib/utils";
-
  import { capitalize } from "lodash";
+
  import { capitalize, isEqual } from "lodash";
  import { HttpdClient } from "@httpd-client";
  import { httpdStore } from "@app/lib/httpd";

@@ -52,7 +52,7 @@
  import Placeholder from "@app/components/Placeholder.svelte";
  import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
  import SquareButton from "@app/components/SquareButton.svelte";
-
  import TagInput from "@app/views/projects/Cob/TagInput.svelte";
+
  import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";

  export let baseUrl: BaseUrl;
  export let project: Project;
@@ -69,13 +69,10 @@
        project.id,
        patch.id,
        {
-
          type: "thread",
+
          type: "revision.comment",
          revision: revisionId,
-
          action: {
-
            type: "comment",
-
            body: reply.body,
-
            replyTo: reply.id,
-
          },
+
          body: reply.body,
+
          replyTo: reply.id,
        },
        $httpdStore.session.id,
      );
@@ -96,10 +93,9 @@
    }
  }

-
  async function saveTags({ detail: tags }: CustomEvent<string[]>) {
+
  async function saveLabels({ detail: labels }: CustomEvent<string[]>) {
    if ($httpdStore.state === "authenticated") {
-
      const { add, remove } = utils.createAddRemoveArrays(patch.tags, tags);
-
      if (add.length === 0 && remove.length === 0) {
+
      if (isEqual(patch.labels, labels)) {
        return;
      }

@@ -112,7 +108,7 @@
      await api.project.updatePatch(
        project.id,
        revision,
-
        { type: "tag", add, remove },
+
        { type: "label", labels: labels },
        $httpdStore.session.id,
      );
      patch = await api.project.getPatchById(project.id, patch.id);
@@ -493,6 +489,6 @@
        {/each}
      </div>
    </div>
-
    <TagInput {action} tags={patch.tags} on:save={saveTags} />
+
    <LabelInput {action} labels={patch.labels} on:save={saveLabels} />
  </div>
</div>
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -82,7 +82,7 @@
    align-self: center;
    margin: 0 1rem 0 1.25rem;
  }
-
  .tags {
+
  .labels {
    display: flex;
    flex-direction: row;
    gap: 0.5rem;
@@ -92,7 +92,7 @@
    align-items: center;
    gap: 0.5rem;
  }
-
  .tag {
+
  .label {
    overflow: hidden;
    text-overflow: ellipsis;
  }
@@ -109,7 +109,7 @@
    color: var(--color-primary-6);
  }
  @media (max-width: 960px) {
-
    .tags {
+
    .labels {
      display: none;
    }
  }
@@ -137,15 +137,15 @@
          <InlineMarkdown content={patch.title} />
        </span>
      </Link>
-
      <span class="tags">
-
        {#each patch.tags.slice(0, 4) as tag}
+
      <span class="labels">
+
        {#each patch.labels.slice(0, 4) as label}
          <Badge style="max-width:7rem" variant="secondary">
-
            <span class="tag">{tag}</span>
+
            <span class="label">{label}</span>
          </Badge>
        {/each}
-
        {#if patch.tags.length > 4}
+
        {#if patch.labels.length > 4}
          <Badge variant="foreground">
-
            <span class="tag">+{patch.tags.length - 4} more tags</span>
+
            <span class="label">+{patch.labels.length - 4} more labels</span>
          </Badge>
        {/if}
      </span>
modified tests/e2e/project.spec.ts
@@ -476,10 +476,10 @@ test("internal file markdown link", async ({ page }) => {

test("diff selection de-select", async ({ page }) => {
  await page.goto(
-
    `${cobUrl}/patches/013f8b2734df1840b2e33d52ff5632c8d66b199a?tab=files#README.md:H0L0H0L3`,
+
    `${cobUrl}/patches/e35c10c370de7fb94e95dbdf05ab93000132683f?tab=files#README.md:H0L0H0L3`,
  );
  await page.getByText("Add subtitle to README").click();
  await expect(page).toHaveURL(
-
    `${cobUrl}/patches/013f8b2734df1840b2e33d52ff5632c8d66b199a?tab=files`,
+
    `${cobUrl}/patches/e35c10c370de7fb94e95dbdf05ab93000132683f?tab=files`,
  );
});
modified tests/e2e/project/issues.spec.ts
@@ -15,7 +15,7 @@ test("navigate single issue", async ({ page }) => {
  await page.getByText("This title has markdown").click();

  await expect(page).toHaveURL(
-
    `${cobUrl}/issues/4fc727e722d3979fd2073d9b56b2751658a4ae79`,
+
    `${cobUrl}/issues/9cedac832f0791bea5c9cf8fa32db8a68c592166`,
  );
});

@@ -71,7 +71,7 @@ test("test issue editing failing", async ({ page, authenticatedPeer }) => {
  );

  await page.route(
-
    `**/v1/projects/${rid}/issues/d316f7a90a40dacbfb8728044bad50c9f71d44ba`,
+
    `**/v1/projects/${rid}/issues/ad9114fa910c67f09ce5d42d12c31038eb40fc86`,
    route => {
      if (route.request().method() !== "PATCH") {
        void route.fallback();
@@ -82,7 +82,7 @@ test("test issue editing failing", async ({ page, authenticatedPeer }) => {
  );

  await page.goto(
-
    `${authenticatedPeer.uiUrl()}/${rid}/issues/d316f7a90a40dacbfb8728044bad50c9f71d44ba`,
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/ad9114fa910c67f09ce5d42d12c31038eb40fc86`,
  );

  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
@@ -109,10 +109,10 @@ test("go through the entire ui issue flow", async ({
  await page.getByPlaceholder("Add assignee").fill(authenticatedPeer.nodeId);
  await page.getByPlaceholder("Add assignee").press("Enter");

-
  await page.getByPlaceholder("Add tag").fill("bug");
-
  await page.getByPlaceholder("Add tag").press("Enter");
-
  await page.getByPlaceholder("Add tag").fill("documentation");
-
  await page.getByPlaceholder("Add tag").press("Enter");
+
  await page.getByPlaceholder("Add label").fill("bug");
+
  await page.getByPlaceholder("Add label").press("Enter");
+
  await page.getByPlaceholder("Add label").fill("documentation");
+
  await page.getByPlaceholder("Add label").press("Enter");

  await page.getByRole("button", { name: "Submit" }).click();

modified tests/e2e/project/patches.spec.ts
@@ -17,7 +17,7 @@ test("navigate patch details", async ({ page }) => {
  await page.goto(`${cobUrl}/patches`);
  await page.getByText("Add subtitle to README").click();
  await expect(page).toHaveURL(
-
    `${cobUrl}/patches/013f8b2734df1840b2e33d52ff5632c8d66b199a`,
+
    `${cobUrl}/patches/e35c10c370de7fb94e95dbdf05ab93000132683f`,
  );
  await page.getByRole("link", { name: "Add subtitle to README" }).click();
  await expect(page).toHaveURL(
@@ -27,13 +27,13 @@ test("navigate patch details", async ({ page }) => {
  {
    await page.getByRole("link", { name: "Commits" }).click();
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/013f8b2734df1840b2e33d52ff5632c8d66b199a?tab=commits`,
+
      `${cobUrl}/patches/e35c10c370de7fb94e95dbdf05ab93000132683f?tab=commits`,
    );
  }
  {
    await page.getByRole("link", { name: "Files" }).click();
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/013f8b2734df1840b2e33d52ff5632c8d66b199a?tab=files`,
+
      `${cobUrl}/patches/e35c10c370de7fb94e95dbdf05ab93000132683f?tab=files`,
    );
  }
});
@@ -73,7 +73,7 @@ test("test patches counters", async ({ page, authenticatedPeer }) => {
});

test("use revision selector", async ({ page }) => {
-
  await page.goto(`${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466`);
+
  await page.goto(`${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8`);
  await page.getByRole("link", { name: "Files" }).click();

  // Validating the latest revision state
@@ -87,9 +87,9 @@ test("use revision selector", async ({ page }) => {
  ).toHaveText("Add more text");

  // Switching to the initial revision
-
  await page.getByText("Revision 779ce78").click();
+
  await page.getByText("Revision 0535843").click();
  await expect(page.locator(".dropdown")).toBeVisible();
-
  await page.getByRole("link", { name: "Revision 0f3697f" }).click();
+
  await page.getByRole("link", { name: "Revision 687c326" }).click();
  await expect(page.locator(".dropdown")).toBeHidden();

  // Validating the initial revision
@@ -103,12 +103,12 @@ test("use revision selector", async ({ page }) => {
  ).toBeHidden();

  await expect(page).toHaveURL(
-
    `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466/0f3697fed2743549e3bf531e9fa81284a6de1466?tab=files`,
+
    `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8/687c3268119d23c5da32055c0b44c03e0e4088b8?tab=files`,
  );
});

test("navigate through revision diffs", async ({ page }) => {
-
  await page.goto(`${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466`);
+
  await page.goto(`${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8`);

  const firstRevision = page.locator(".revision").first();
  const secondRevision = page.locator(".revision").nth(1);
@@ -123,19 +123,19 @@ test("navigate through revision diffs", async ({ page }) => {
      page.getByRole("link", { name: "Diff 38c225..9898da" }),
    ).toBeVisible();
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..9898da6155467adad511f63bf0fb5aa4156b92ef`,
+
      `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..9898da6155467adad511f63bf0fb5aa4156b92ef`,
    );
    await page.goBack();
    await secondRevision.locator(".toggle").click();
    await secondRevision
-
      .getByRole("link", { name: "Compare to previous revision (0f3697f)" })
+
      .getByRole("link", { name: "Compare to previous revision (687c326)" })
      .click();
    await expect(
      page.getByRole("link", { name: "Diff 0dc373..9898da" }),
    ).toBeVisible();

    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466?diff=0dc373db601ccbcffa80dec932e4006516709ca6..9898da6155467adad511f63bf0fb5aa4156b92ef`,
+
      `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8?diff=0dc373db601ccbcffa80dec932e4006516709ca6..9898da6155467adad511f63bf0fb5aa4156b92ef`,
    );
    await page.goBack();

@@ -157,7 +157,7 @@ test("navigate through revision diffs", async ({ page }) => {
      page.getByRole("link", { name: "Diff 38c225..0dc373" }),
    ).toBeVisible();
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..0dc373db601ccbcffa80dec932e4006516709ca6`,
+
      `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..0dc373db601ccbcffa80dec932e4006516709ca6`,
    );
  }
});
modified tests/support/cobs/issue.ts
@@ -5,7 +5,7 @@ export async function create(
  peer: RadiclePeer,
  title: string,
  description: string,
-
  tags: string[],
+
  labels: string[],
  options: Options,
): Promise<string> {
  const issueOptions: string[] = [
@@ -15,7 +15,7 @@ export async function create(
    title,
    "--description",
    description,
-
    ...tags.map(tag => ["--tag", tag]).flat(),
+
    ...labels.map(label => ["--label", label]).flat(),
  ];
  const { stdout } = await peer.rad(issueOptions, options);
  const match = stdout.match(/Issue {3}([a-zA-Z0-9]*)/);
modified tests/support/fixtures.ts
@@ -91,7 +91,6 @@ export const test = base.extend<{
        // access to any variables that we have in the test.
        await page.addInitScript(() => {
          window.APP_CONFIG = {
-
            reactions: [],
            nodes: {
              defaultHttpdPort: 8081,
              defaultLocalHttpdPort: 8081,
@@ -229,7 +228,6 @@ function log(text: string, label: string, outputLog: Stream.Writable) {

export function appConfigWithFixture() {
  window.APP_CONFIG = {
-
    reactions: [],
    nodes: {
      defaultHttpdPort: 8081,
      defaultLocalHttpdPort: 8081,
@@ -543,7 +541,7 @@ export async function createCobsFixture(peer: RadiclePeer) {
    { cwd: projectFolder },
  );
  await peer.rad(
-
    ["tag", patchThree, "documentation"],
+
    ["label", patchThree, "documentation"],
    createOptions(projectFolder, 1),
  );
  await peer.rad(
modified tests/support/heartwood-version
@@ -1 +1 @@
-
b953e9f6b57089c162773fa9384b1a3d7df148a5
+
b456d3a401abf5314598cbede49c4fb2864bdf6f
modified tests/unit/utils.test.ts
@@ -265,27 +265,3 @@ describe("Date Manipulation", () => {
    );
  });
});
-

-
describe("createAddRemoveArrays", () => {
-
  test.each([
-
    {
-
      currentArray: ["a", "b"],
-
      newArray: ["a", "b", "c"],
-
      expected: { add: ["c"], remove: [] },
-
    },
-
    {
-
      currentArray: ["a", "b"],
-
      newArray: ["c"],
-
      expected: { add: ["c"], remove: ["a", "b"] },
-
    },
-
    { currentArray: [], newArray: ["c"], expected: { add: ["c"], remove: [] } },
-
    { currentArray: ["a"], newArray: ["a"], expected: { add: [], remove: [] } },
-
  ])(
-
    "createAssigneeArrays $hash => $expected",
-
    ({ currentArray, newArray, expected }) => {
-
      expect(utils.createAddRemoveArrays(currentArray, newArray)).toEqual(
-
        expected,
-
      );
-
    },
-
  );
-
});
modified tests/visual/cob.spec.ts
@@ -23,15 +23,15 @@ test("issues page", async ({ page }) => {
});

test("issue page", async ({ page }) => {
-
  await page.goto(`${cobUrl}/issues/4fc727e722d3979fd2073d9b56b2751658a4ae79`, {
+
  await page.goto(`${cobUrl}/issues/9cedac832f0791bea5c9cf8fa32db8a68c592166`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot({ fullPage: true });
-
  await page.goto(`${cobUrl}/issues/4038cc5bf6d38f0a5606982236e2abb113affaea`, {
+
  await page.goto(`${cobUrl}/issues/278bbe0bf3af51e5de1dfe20fefbbec4e1121343`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot({ fullPage: true });
-
  await page.goto(`${cobUrl}/issues/673c51821aee4b780d9661c20d267d66ec43d7ae`, {
+
  await page.goto(`${cobUrl}/issues/61d2dbe81411ee6a9cce75451bc637541ea6a7c2`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot({ fullPage: true });
@@ -59,40 +59,41 @@ test("patches page", async ({ page }) => {
test("patch page", async ({ page }) => {
  // Draft patch
  await page.goto(
-
    `${cobUrl}/patches/f85dce5dced961ee0f47735401cee72a0ee77900`,
+
    `${cobUrl}/patches/416d2f95f32a5fdee958172b724c8439ce5334e2`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  // Archived patch
  await page.goto(
-
    `${cobUrl}/patches/8d83959d9889da0a94129d9ba06b87c8823972a8`,
+
    `${cobUrl}/patches/43ae785a9ceaf289b2445fb5b8e01036d456b2be`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  // Merged patch
  await page.goto(
-
    `${cobUrl}/patches/a5b1d30035da686ba1c4742f6fd25c43238df671`,
+
    `${cobUrl}/patches/6a51e1d2e350136e7bcfad8f13d16488c1f1c99a`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
+
  // Open patch "Add subtitle to README"
  await page.goto(
-
    `${cobUrl}/patches/013f8b2734df1840b2e33d52ff5632c8d66b199a`,
+
    `${cobUrl}/patches/e35c10c370de7fb94e95dbdf05ab93000132683f`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
-
  // Open patch
+
  // Open patch "Taking another stab at the README"
  await page.goto(
-
    `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466`,
+
    `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  await page.goto(
-
    `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466?tab=commits`,
+
    `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8?tab=commits`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  await page.goto(
-
    `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466?tab=files`,
+
    `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8?tab=files`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
@@ -104,6 +105,6 @@ test("failed diff loading for a specific revision", async ({ page }) => {
    route => route.fulfill({ status: 500 }),
  );

-
  await page.goto(`${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466`);
+
  await page.goto(`${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8`);
  await expect(page).toHaveScreenshot({ fullPage: true });
});
modified tests/visual/mobile/cob.spec.ts
@@ -30,15 +30,15 @@ test("issues page", async ({ page }) => {
});

test("issue page", async ({ page }) => {
-
  await page.goto(`${cobUrl}/issues/4fc727e722d3979fd2073d9b56b2751658a4ae79`, {
+
  await page.goto(`${cobUrl}/issues/9cedac832f0791bea5c9cf8fa32db8a68c592166`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot({ fullPage: true });
-
  await page.goto(`${cobUrl}/issues/4038cc5bf6d38f0a5606982236e2abb113affaea`, {
+
  await page.goto(`${cobUrl}/issues/278bbe0bf3af51e5de1dfe20fefbbec4e1121343`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot({ fullPage: true });
-
  await page.goto(`${cobUrl}/issues/673c51821aee4b780d9661c20d267d66ec43d7ae`, {
+
  await page.goto(`${cobUrl}/issues/61d2dbe81411ee6a9cce75451bc637541ea6a7c2`, {
    waitUntil: "networkidle",
  });
  await expect(page).toHaveScreenshot({ fullPage: true });
@@ -66,35 +66,35 @@ test("patches page", async ({ page }) => {
test("patch page", async ({ page }) => {
  // Draft patch
  await page.goto(
-
    `${cobUrl}/patches/f85dce5dced961ee0f47735401cee72a0ee77900`,
+
    `${cobUrl}/patches/416d2f95f32a5fdee958172b724c8439ce5334e2`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  // Archived patch
  await page.goto(
-
    `${cobUrl}/patches/8d83959d9889da0a94129d9ba06b87c8823972a8`,
+
    `${cobUrl}/patches/43ae785a9ceaf289b2445fb5b8e01036d456b2be`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  // Merged patch
  await page.goto(
-
    `${cobUrl}/patches/a5b1d30035da686ba1c4742f6fd25c43238df671`,
+
    `${cobUrl}/patches/6a51e1d2e350136e7bcfad8f13d16488c1f1c99a`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  // Open patch
  await page.goto(
-
    `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466`,
+
    `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  await page.goto(
-
    `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466?tab=commits`,
+
    `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8?tab=commits`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });
  await page.goto(
-
    `${cobUrl}/patches/0f3697fed2743549e3bf531e9fa81284a6de1466?tab=files`,
+
    `${cobUrl}/patches/687c3268119d23c5da32055c0b44c03e0e4088b8?tab=files`,
    { waitUntil: "networkidle" },
  );
  await expect(page).toHaveScreenshot({ fullPage: true });