Skip to content

True sub-notes: parent_id tree in sidebar (Zed project-panel feel) #80

Description

@chasehuh

Summary

Give agentnote a real note hierarchy: a durable notes.parent_id edge, set when a note is created from inside another note, rendered as an expandable/collapsible tree in the sidebar with the feel of Zed's project panel.

This revises the lock in #77, which said sub-notes are "not a nested DB hierarchy" and put "nested note DB hierarchy / folders" in Non-Goals. That call was right for #79's authoring layer — it kept the wire format plain Markdown and the CRDT untouched — but it left every note at the same level in the sidebar, so a note tree built with [[ reads as a flat recency bag. Chase has since locked the opposite direction for creation:

"link은 그냥 다른 곳으로 가는 거고, 그 안에서 생성하거나 하는 놈들은 sub note."
(A link just goes somewhere else; the ones you create from inside a note are sub-notes.)

The key distinction, and the whole point of this issue: linking ≠ nesting. Only creation-from-inside nests.

Why This Matters

#79 shipped [[ completion and a / palette, so producing a sub-note is now one gesture. But every note it creates lands as a sibling of its parent in a list sorted purely by updated_at DESC. Ten minutes of linked thinking produces ten top-level rows and no visible structure. The graph exists in the text and nowhere in the UI.

The fix is not "more link chrome" — it is a folder-shaped sidebar. Chase's words: folder-feel dependency in the UI. A note you created while inside "Q3 planning" should live under "Q3 planning", the way a file you create inside a directory in Zed lands inside that directory.

This is also the cheapest moment to do it. #79 already funnels every create-from-editor through one helper (createNoteRow), so the parent edge is a single argument threaded through a single call site. Waiting means retrofitting parentage onto notes created without it.

Conversation Context

Product locks from Chase, authoritative for this issue and superseding #77's flat-graph wording where they conflict:

Create vs link matrix

Action Result
Link to an existing noteLink to note in the / palette, or picking an existing note in [[ Peer hyperlink only. Inserts [Title](/n/{id}). Creates no parent/child edge. Nothing about the target note changes.
Create from inside a note[[FooCreate "Foo", or /New note with a title True sub-note. Still an ordinary notes row, but owned under the active note as parent. Nests under the parent in the sidebar. Still inserts the same Markdown link in the parent body.
⌘N / sidebar + Root-level note (parent_id = null). Current behavior, including navigating to it. Not a sub-note.

Corollary lock, stated because it is the tempting wrong turn: hierarchy is a create-time parent, not "any link". A body containing [X](/n/abc) does not make abc a child. Do not derive the tree by parsing bodies.

UI — actively reference Zed

Primary reference: the Zed project panel (zed-industries/zed, docs docs/src/project-panel.md). Match the feel, not a pixel clone:

  • Sidebar is a tree — parents expandable/collapsible, children indented under the parent.
  • Creating a sub-note from the current note should feel like Zed's "new file inside the selected directory": the child appears under its parent, not at the top of a recency-flattened list.
  • Keyboard expand/collapse (arrows / disclosure chevron) is expected.
  • Auto-reveal the active note's ancestors when it is selected — Zed's project_panel.auto_reveal_entries spirit: "switching to a file in the editor will automatically highlight it in the project panel and scroll it into view."
  • Optional v1 niceties if cheap: sticky ancestor rows (project_panel.sticky_scroll, "ancestor directories pin themselves to the top of the panel as you scroll") and compact single-child chains (project_panel.auto_fold_dirs, "chains of directories that each contain a single child directory are collapsed into one row").
  • Do not block v1 on full Zed parity. Multi-select, drag-and-drop reparenting, and cut/paste between folders are explicit residuals.

Current Behavior

Data model — no parent edge exists

lib/db.ts:86-245 ensureSchema() is the migration mechanism: a sequence of idempotent CREATE TABLE IF NOT EXISTS / ALTER TABLE … ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS statements behind a memoized global.__agentnoteSchemaReady promise. is_public, public_id, published_at, author_handle, and deleted_at were all added exactly this way. There is no parent_id.

lib/notes.ts:66-67:

const NOTE_COLUMNS = `id, title, body, created_at, updated_at, deleted_at,
  is_public, public_id, published_at, author_handle`;

lib/types.ts:1-22 Note has no parent field. lib/notes.ts:274-306 createNote(userId, { title, body }) takes no parent.

Listing is flat and recency-ordered

lib/notes.ts:167-176:

export async function listNotes(userId: string): Promise<Note[]> {
  const result = await query<NoteRow>(
    `SELECT ${NOTE_COLUMNS}
     FROM notes
     WHERE user_id = $1 AND deleted_at IS NULL
     ORDER BY updated_at DESC`,
    [userId],
  );
  return result.rows.map(mapNote);
}

components/agentnote-app.tsx:551-555 re-sorts client-side and applies the #tag filter:

const sortedNotes = useMemo(() => {
  const ordered = sortNotesByRecent(notes);
  if (!tagFilter) return ordered;
  return ordered.filter((note) => noteHasTag(note.body, tagFilter));
}, [notes, tagFilter]);

components/agentnote-app.tsx:1531-1590 renders sortedNotes.map(...) into one flat <nav className="zed-panel__list"> of .zed-note-item rows. No indentation, no disclosure, no nesting. CSS at app/globals.css:599-694.

Create paths (post-#79, the surface this issue changes)

components/agentnote-app.tsx:1180-1228 — one shared helper, two callers:

/**
 * Create a note and register it locally, without touching the active note.
 * Shared by ⌘N (which then navigates) and the editor's `[[` / `/` sub-note
 * flow (which must NOT navigate — the user is mid-sentence in the parent).
 */
const createNoteRow = useCallback(
  async (input?: { body?: string }): Promise<Note | null> => {
    const response = await fetch("/api/notes", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ body: input?.body ?? "" }),
    });
    if (!response.ok) return null;
    const data = (await response.json()) as { note: Note };
    setNotes((prev) => sortNotesByRecent([data.note, ...prev]));
    syncPost.current({ type: "upsert", sourceId: tabId.current, note: data.note });
    return data.note;
  },
  [],
);

const createNote = useCallback(async () => {           // ⌘N + sidebar `+`
  const ok = await ensureSafeToLeaveActive();
  if (!ok) return;
  const note = await createNoteRow();
  if (!note) return;
  await selectNote(note, { skipFlush: true });
  if (!isNarrowViewport()) setSidebarOpen(true);
}, [createNoteRow, ensureSafeToLeaveActive, selectNote]);

const noteLinkOptions = useMemo(                        // `[[` and `/`
  () => ({
    candidates: () => notesRef.current.map((note) => ({ id: note.id, title: previewTitle(note) })),
    createNote: async (title: string) => {
      const note = await createNoteRow({ body: title });
      return note ? { id: note.id, title: previewTitle(note) } : null;
    },
  }),
  [createNoteRow],
);

lib/editor/note-links.ts already routes exactly the two create-from-inside gestures through options.createNotecreateCompletion (the [[Create "…" item, :115-131) and the / palette's New note (:176-198). Picking an existing note goes through linkCompletion (:95-113), which never touches createNote. The create/link split this issue needs already exists in the code; only the parent argument is missing.

app/api/notes/route.ts:22-45 POST reads only { title, body } and ignores everything else.

Sync

lib/tab-sync.ts:29-33 — the upsert message carries a whole Note, so any new column rides along automatically once it is on the type. components/agentnote-app.tsx:1066-1114 merges peer upserts into notes.

CRDT (lib/crdt/*) owns the note body only. app/api/notes/[id]/route.ts:74-85 409s a whole-document PUT on a CRDT-managed note (crdt_managed_body). Nothing in that path reads or writes note metadata columns.

Desired Behavior

  1. Creating a note from inside note P (via [[Create "…" or /New note) stores parent_id = P.id and inserts [Title](/n/{child}) into P's body as it does today.
  2. Linking an existing note leaves parent_id untouched on both notes.
  3. ⌘N and the sidebar + create root notes (parent_id = null).
  4. The sidebar renders a tree: children indented under their parent, parents carrying a disclosure chevron.
  5. Expanding/collapsing works by click and by keyboard (/ on a focused row; ⌘→/⌘← for expand-all/collapse-all), and the state survives reload.
  6. Selecting a note auto-reveals it: every ancestor expands.
  7. A newly created sub-note is immediately visible under its parent, in this tab and in peer tabs.
  8. Archiving a parent does not hide or archive its children — they render at root level until the parent is restored.

Source Of Truth

Internal

Path Why it matters
lib/db.ts:86-245 ensureSchema() — the idempotent migration to extend. Follow the ADD COLUMN IF NOT EXISTS style used for deleted_at / author_handle.
lib/notes.ts:29-40, 66-67, 69-82 NoteRow, NOTE_COLUMNS, mapNote — every read path funnels through these three.
lib/notes.ts:167-176 listNotes — the tree's data source.
lib/notes.ts:194-233 resolveCanonicalNoteId — use it to validate an incoming parent_id (handles aliases, hyphenless ids, and tenant scoping in one call).
lib/notes.ts:274-306 createNote — id-collision retry loop; the insert to extend.
lib/notes.ts:456-479, 481-500, 503-518 archiveNote / restoreNote / permanentlyDeleteNote — the lifecycle events that must have defined parent semantics.
lib/types.ts:1-22 Note. Adding a field here is what makes it flow through upsert broadcasts and SSR props for free.
app/api/notes/route.ts:22-45 POST /api/notes — accepts the new optional parent_id.
components/agentnote-app.tsx:1180-1228 createNoteRow / createNote / noteLinkOptions — the single seam where create-from-inside diverges from ⌘N.
components/agentnote-app.tsx:551-555, 1531-1590 sortedNotes + the flat sidebar list to replace with a tree.
lib/editor/note-links.ts:95-131, 163-229 Confirms createNote is called by exactly the two create gestures and never by link-existing. Should not need to change.
lib/tab-sync.ts:29-33 upsert carries a whole Note.
app/globals.css:599-694 .zed-panel__list / .zed-note-item styles to extend with indentation + chevron.
app/page.tsx, app/n/[id]/page.tsx SSR entry points; both hand listNotes() output to AgentNoteApp as initialNotes.

External

Proposed API / Schema

Migration (lib/db.ts, inside ensureSchema())

Add after the existing deleted_at block, before note_revisions:

-- True sub-notes: a note created from inside another note is owned by it.
-- Peer hyperlinks in the body do NOT create this edge (see #77 / #79).
ALTER TABLE notes
  ADD COLUMN IF NOT EXISTS parent_id TEXT
  REFERENCES notes(id) ON UPDATE CASCADE ON DELETE SET NULL;

CREATE INDEX IF NOT EXISTS notes_user_parent_idx
ON notes (user_id, parent_id);

ON DELETE SET NULL, deliberately not CASCADE: permanently deleting a parent must promote its children to root, never silently delete a subtree the user did not select. ON UPDATE CASCADE matches every other FK here and keeps migrateLegacyNoteIds (lib/db.ts:34-84) working when a PK is rewritten.

Nullable forever — root notes are the common case.

Note type (lib/types.ts)

/**
 * Owning note, set only when this note was CREATED from inside another note
 * (`[[` create / `/` New note). Null = root. Peer links in a body never set
 * this — hierarchy is a create-time edge, not a parse of the text.
 */
parent_id: string | null;

Add parent_id to NOTE_COLUMNS, NoteRow, and mapNote in lib/notes.ts. Do not add it to PublicNote — hierarchy is private and /p/… payloads stay unchanged.

POST /api/notes

Request:

{ "body": "Sprint retro", "parent_id": "abc-defg-hij" }

parent_id is optional; omit or null for a root note.

Response 201 (unchanged shape, one new field):

{
  "note": {
    "id": "kmn-opqr-stu",
    "title": "",
    "body": "Sprint retro",
    "parent_id": "abc-defg-hij",
    "created_at": "2026-08-05T09:00:00.000Z",
    "updated_at": "2026-08-05T09:00:00.000Z",
    "deleted_at": null,
    "is_public": false,
    "public_id": null,
    "published_at": null,
    "author_handle": null
  }
}

Error 400:

{ "error": "Invalid parent_id" }

Validation rules

  • parent_id absent, null, or "" → root note. No error.
  • Non-string parent_id400.
  • parent_id that fails isValidNoteId400.
  • parent_id that resolveCanonicalNoteId(userId, parentId) cannot resolve → 400. That single call enforces tenant isolation (another user's note is unresolvable) and liveness (archived notes are excluded unless includeArchived) at once. Store the canonical id it returns, not the raw input, so aliased ids normalize.
  • Fail closed, not silently-root. A create that was meant to nest must not quietly produce a root note. The client compensates: only send parent_id when the active note is present in the live notes list (see Implementation Notes), so the realistic 400 is a genuine bug or a cross-tab archive race.
  • No cycles are structurally reachable in v1: parent_id is set once at insert, on a row that cannot yet have children, and there is no reparent endpoint. Defence in depth is in the tree builder, which must be cycle-safe (see below) so a hand-edited or corrupted row degrades to root rows instead of hanging the render.
  • Backward compatibility: existing clients that POST { title, body } keep working unchanged; every existing note reads back parent_id: null.

Lifecycle semantics (locked)

Event Parent edge Sidebar
Archive a parent Children keep parent_id. Unchanged rows. Parent leaves the live list; children's parent_id no longer resolves, so the tree builder renders them at root.
Restore that parent Unchanged Children re-nest under it automatically.
Permanently delete a parent ON DELETE SET NULL promotes children to root Children stay, now at root. Irreversible, and correct — the parent row is gone.
Archive a child Unchanged Child moves to the Archived section (flat, as today).

Archiving a parent deliberately does not cascade-archive children. Cascading would make one × click hide an arbitrary amount of work, and restoreNote has no record of which children it archived to restore them precisely.

Implementation Notes

New files

lib/note-tree.ts — pure, dependency-free, fully unit-testable. The load-bearing module.

import type { Note } from "./types";

export type NoteTreeRow = {
  note: Note;
  depth: number;
  hasChildren: boolean;
  expanded: boolean;
};

/**
 * Group notes into parent/child order.
 *
 * Cycle- and orphan-safe by construction: a `parent_id` that is not present in
 * `notes` (archived parent, cross-tenant row) and any node reachable from
 * itself are both demoted to roots. Siblings keep the app's recency order.
 */
export function buildNoteTree(notes: Note[]): Note[/* roots */];

/** Ancestor chain, nearest first. Used to auto-reveal the active note. */
export function ancestorIds(notes: Note[], id: string): string[];

/**
 * Depth-first render order. `collapsed` holds ids whose children are hidden —
 * absent means expanded, so a brand-new sub-note is visible immediately.
 */
export function flattenNoteTree(
  notes: Note[],
  collapsed: ReadonlySet<string>,
): NoteTreeRow[];

Sibling order: updated_at DESC, i.e. today's sortNotesByRecent applied within each level. Recency still drives the eye; it just no longer flattens structure.

lib/note-tree.test.ts — see Tests.

Modified files

  • lib/db.ts — the ALTER TABLE + index above, inside ensureSchema().
  • lib/types.tsparent_id: string | null on Note.
  • lib/notes.tsparent_id in NoteRow, NOTE_COLUMNS, mapNote; createNote(userId, { title, body, parentId }) inserting the fourth column.
  • app/api/notes/route.ts — parse + validate parent_id, resolve to canonical, pass to createNote.
  • components/agentnote-app.tsx
    • createNoteRow(input?: { body?: string; parentId?: string | null }), sending parent_id only when set.
    • noteLinkOptions.createNote passes parentId: activeIdRef.current, guarded: only when that id is present in notesRef.current (a live note). activeIdRef is already in scope (used at :1261).
    • createNote (⌘N / +) unchanged — no parentId, so root.
    • collapsed state: useState<Set<string>>, persisted to localStorage under agentnote.tree.collapsed alongside the existing WRAP_STORAGE_KEY pattern.
    • Auto-reveal: an effect on activeId that deletes ancestorIds(notes, activeId) from collapsed.
    • Replace the sortedNotes.map(...) list with flattenNoteTree(...).map(...), rendering a chevron button when hasChildren and indenting via a --depth custom property.
    • #tag filter stays flat. When tagFilter is set, render today's filtered flat list — a filter is a search view, not a tree. Keep this explicit in a comment so it does not read as an oversight.
  • app/globals.css — indentation on .zed-note-item__hit (padding-left: calc(12px + var(--depth) * 12px)), a .zed-note-item__chevron disclosure control sized to the existing 20px icon-button grammar, and a fixed-width empty slot for childless rows so titles stay aligned.
  • README.md — a short section stating the create-vs-link rule.

Flow

  1. User types [[Retro in note P → CodeMirror [[ source offers Create "Retro" (lib/editor/note-links.ts:151-156).
  2. createAndInsert (:70-93) removes the trigger text, then awaits options.createNote("Retro").
  3. noteLinkOptions.createNote calls createNoteRow({ body: "Retro", parentId: P.id }).
  4. POST /api/notes { body, parent_id }resolveCanonicalNoteId validates ownership + liveness → createNote inserts with parent_id.
  5. Response note (carrying parent_id) is merged into notes and broadcast as upsert; peer tabs rebuild their tree from the same field.
  6. createAndInsert inserts [Retro](/n/{child}) at the caret — through yCollab into Y.Text when CRDT is on, exactly as today. The body write is unchanged; only the row's metadata is new.
  7. Sidebar re-renders: Retro appears indented under P, P gains a chevron.

Tests

lib/note-tree.test.ts (pure, node env):

  • roots and children group correctly; sibling order is updated_at DESC
  • flattenNoteTree emits depth-first order with correct depth / hasChildren
  • a note whose parent_id is absent from the input (archived parent) renders at root, not dropped
  • a 2-cycle (a.parent = b, b.parent = a) and a self-cycle (a.parent = a) both terminate and yield root rows — no infinite recursion, no lost notes
  • collapsed hides a whole subtree, not just direct children
  • ancestorIds returns the chain nearest-first and [] for a root / unknown id

lib/notes-parent.test.ts (mock query in the style of lib/notes-revisions.test.ts):

  • createNote with parentId writes it; without, writes null
  • mapNote surfaces parent_id

app/api/notes route validation (in the style of lib/crdt/doc-routes.test.ts):

  • create-from-editor payload sets the parent
  • link-existing performs no create at all — asserted at the note-links level: linkCompletion's apply inserts Markdown and never calls options.createNote (extends lib/editor/note-links.test.ts)
  • another user's note id as parent_id400, no row written (tenant isolation)
  • an archived note id as parent_id400
  • a malformed parent_id400
  • absent parent_id201, parent_id: null

Edge Cases And Risks

  • Tenant isolation is the sharpest edge: parent_id is a user-supplied foreign key into notes. Routing it through resolveCanonicalNoteId(userId, …) rather than trusting the string is mandatory — a raw insert would let a crafted POST attach a note under another tenant's row and leak its id through the tree.
  • Orphan rendering. The single most likely bug is a child silently disappearing when its parent is archived. buildNoteTree must demote unresolvable-parent notes to roots; this has a dedicated test because "note vanished" reads as data loss to the user, and this repo has a history of exactly that class of report (Data loss: cross-tab upsert with forceBody overwrites an actively-edited note body #51, Data loss: stale-tab LWW PUT still truncates note bodies after #51/#56 dirty guards (RCA 2026-07-30 uvk-cumd-omo) #57, fix: stop 409-rebased stale buffers from silently clobbering newer note bodies (0804 wipe RCA) #73).
  • Cycles. Unreachable through the API today, but the tree builder recurses over user-controlled data. It must be cycle-safe by construction (visited set), not by assuming the API is the only writer.
  • 400 on a mid-typing archive race. If the parent is archived in another tab between the keystroke and the POST, the create fails and createAndInsert has already removed the trigger text — the user loses [[Retro. Rare, and identical to today's behavior for any create failure (offline, 500). The client-side live-note guard closes the common case. Accepted; not worth a retry path in v1.
  • CRDT is untouched. Hierarchy is a notes row field. No Yjs document changes, no new update types, no change to the crdt_managed_body 409 gate. Stated explicitly because the natural instinct is to sync it through the doc.
  • Migration on a live DB. ADD COLUMN … IF NOT EXISTS on a nullable column with no default is a catalog-only operation in Postgres — no table rewrite, safe on the production notes table. The self-referencing FK does require a validation scan; on a table this size that is negligible.
  • Deep nesting overflow. Long titles at depth 5+ will clip in a narrow sidebar. text-overflow: ellipsis already applies; no depth cap in v1.
  • Backward compatibility. Every existing note reads parent_id: null and renders exactly as today. Old bundles that POST without parent_id keep creating root notes. /p/… payloads are byte-identical.

Non-Goals

  • Real filesystem folders, multiple parents, or a graph DB.
  • Drag-and-drop reparenting (residual — implement only if it falls out trivially).
  • Deriving hierarchy from Markdown links. Explicitly rejected: hierarchy is the create-time parent, never a parse of /n/ links in a body.
  • Changing publish URLs, the Meet-style note id format, or turning CRDT off.
  • Rewriting inbound links on rename (still a Wiki-style note links + tags (Obsidian/Notion), muted Offline chrome, and ⌘B bold #77 residual).
  • Exposing hierarchy in /p/… public payloads, or publishing a linked subgraph.
  • Full Zed parity: multi-select, cut/paste between folders, indent guides.
  • Cascade-archiving children with their parent.
  • Server-side tree assembly or a GET /api/notes?parent=… endpoint — the flat list plus parent_id is enough to build the tree client-side without an N+1.

Acceptance Criteria

Data model

  • notes.parent_id TEXT NULL REFERENCES notes(id) ON UPDATE CASCADE ON DELETE SET NULL exists, with notes_user_parent_idx on (user_id, parent_id).
  • ensureSchema() is idempotent — running it against an already-migrated DB is a no-op, and against a pre-migration DB adds the column without touching existing rows.
  • Note.parent_id is returned by GET /api/notes, GET /api/notes/{id}, and SSR initialNotes.
  • PublicNote is unchanged.

Create vs link

  • [[Create "X" inside note P creates a note with parent_id = P.id.
  • /New note with a title inside P creates a note with parent_id = P.id.
  • Picking an existing note in [[, or /Link to note, changes no parent_id on either note.
  • ⌘N and the sidebar + create notes with parent_id = null.
  • Body wire format is unchanged: [Title](/n/{id}), no [[ persisted.

Validation

  • parent_id belonging to another user → 400, no row created.
  • parent_id of an archived note → 400.
  • Malformed parent_id400.
  • A parent_id given as an alias / hyphenless id is stored canonicalized.

Sidebar (Zed feel)

  • Children render indented under their parent; parents show a disclosure chevron.
  • Clicking the chevron collapses/expands; the state persists across reload.
  • / on a focused row expand/collapse it; ⌘→ / ⌘← expand/collapse all.
  • Selecting a nested note auto-expands all its ancestors (auto_reveal_entries spirit).
  • A sub-note created from the editor appears under its parent immediately, without navigating away from the parent.
  • It appears under the same parent in a second tab via the existing upsert broadcast.
  • Archiving a parent leaves its children visible at root level; restoring it re-nests them.
  • With a #tag filter active, the list is flat (documented, not accidental).

Repo

  • pnpm test, pnpm lint, pnpm build, and tsc --noEmit are green.
  • README explains sub-notes vs links.

QA Plan

  1. pnpm test — new lib/note-tree.test.ts and parent-validation tests pass; the existing 312 stay green.
  2. pnpm lint && npx tsc --noEmit && pnpm build.
  3. Migration idempotence: run ensureSchema() twice against a local Postgres; second run is a no-op. Verify with \d notes that the column, FK, and index exist.
  4. Manual, CRDT on (NEXT_PUBLIC_AGENTNOTE_CRDT=1):
    • In note A, [[ChildCreate "Child". Assert: link inserted at the caret, still editing A, Child appears indented under A.
    • In note A, [[ → pick existing note B. Assert: B does not move under A.
    • ⌘N. Assert: new note is at root.
    • Collapse A, reload. Assert: still collapsed. Open /n/{Child} directly. Assert: A auto-expands and Child is selected.
    • Second tab open on /: create a sub-note in tab 1, assert it appears nested in tab 2 without a reload.
    • Archive A. Assert: Child is still listed, now at root. Restore A. Assert: Child is nested again.
  5. Post-deploy: hard-refresh agentnote.dev (service-worker/bundle cache), then re-run step 4's first two checks against production.

Suggested PR Scope

One PR, M/L (~500 lines). Schema, API validation, and the tree renderer are one dependency chain — the column is unobservable without the sidebar, and the sidebar is untestable without the column. Splitting would ship a migration nobody can verify. lib/note-tree.ts carries the logic and is pure, so review effort concentrates in one small, heavily-tested file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions