Skip to content

Editor: Obsidian-style clickable Markdown links (port chasehuh_com agentnoteLinks) #64

Description

@chasehuh

Summary

Port Obsidian-style clickable Markdown links from chasehuh/chasehuh_com into AgentNote’s CodeMirror 6 editor: hide [label](url) chrome, style the label / bare URL as a link, and open the destination on click (in-app /n/{id} or external tab). This is the same editor surface users already get for image widgets (#7), not Open Graph preview cards.

Why This Matters

AgentNote already Live-Preview–renders images but still shows raw [text](https://…) markup for links. The personal site shell (chasehuh_com) already shipped the polished behavior (c2b1a39). Porting it removes a daily friction gap between the two CM6 note shells and matches Obsidian Live Preview expectations for a personal notepad.

Conversation Context

Current Behavior

AgentNote (chasehuh/agentnote @ main)

  • components/codemirror-editor.tsx wires markdown, strikethrough, image paste/widgets, list indent, etc.
  • lib/editor/ has no links.ts.
  • Markdown links and bare https://… URLs render as plain text (full [label](url) chrome visible).
  • Clicking a URL in the buffer does not open it (selection/caret only).
  • Deep links elsewhere use /n/{id} (components/agentnote-app.tsx helper notePath).

Reference (already shipping): chasehuh/chasehuh_com

  • lib/editor/links.tsagentnoteLinks(): Extension
    • Parses CM6 markdown syntax tree (Link, Autolink)
    • Hides LinkMark chrome + URL destination for [label](url); keeps label visible with class cm-md-link
    • Bare https?:\/\/… (and site-specific /thoughts|/awards paths) get cm-md-link--bare
    • Click handler (no shift/alt): resolve href → in-app note event or window.open(..., "noopener,noreferrer")
  • components/codemirror-editor.tsx includes agentnoteLinks() in the extension list
  • app/globals.css.cm-md-link / :hover using --c-text-accent
  • Commit: c2b1a39 (“Add clickable Obsidian-style markdown links in the editor”)

Desired Behavior

  1. In the AgentNote editor (edit + read-only public/share views that use the same CM6 component), [label](url) shows only the label, underlined/accent-colored and clickable.
  2. Bare http(s)://… URLs in the buffer are underlined and clickable (same styling, optional --bare modifier).
  3. Click (primary button, no shift/alt):
    • https?://… / mailto: → open as today on chasehuh_com (window.open / location.assign)
    • In-app note path /n/{id} (and /?n= only if still present anywhere) → select that note without full page reload, mirroring chasehuh_com’s chasehuh:open-note pattern but adapted to AgentNote’s router/state (/n/{id} + selectNote)
  4. Markdown source in the document / CRDT / sync remains the full [label](url) string — decorations only; no body rewrite on click.
  5. Images (![…](url)) are untouched; destination URLs inside ]( image/link markup must not double-match as bare links (chasehuh_com already skips ]( prefixes).
  6. Visual tokens match AgentNote theme variables (reuse --c-text-accent like chasehuh_com / existing accent).

Source Of Truth

Port from (canonical implementation)

  • /Users/huhchaewon/chase/chasehuh_com/lib/editor/links.ts — full extension (copy then adapt openHref / path resolution)
  • /Users/huhchaewon/chase/chasehuh_com/components/codemirror-editor.tsxagentnoteLinks() wiring
  • /Users/huhchaewon/chase/chasehuh_com/app/globals.css (~L489–498) — .cm-md-link styles
  • GitHub: https://github.com/chasehuh/chasehuh_com (commit c2b1a39 and follow-ups)

AgentNote targets

  • components/codemirror-editor.tsx — add extension
  • app/globals.css — add .cm-md-link rules next to existing .cm-md-image block
  • components/agentnote-app.tsx — listen for in-app open-note custom event or pass a callback into the editor; must call existing selectNote / navigation for /n/{id}
  • New: lib/editor/links.ts (+ lib/editor/links.test.ts)

External

  • CodeMirror 6 @codemirror/lang-markdown / Lezer markdown node names Link, Autolink, URL, LinkMark
  • Obsidian Live Preview mental model: chrome hidden, label clickable (no need to match Obsidian plugin APIs)

Proposed API / Schema

No new HTTP API. Editor-only.

Custom event (recommended, mirrors chasehuh_com)

// dispatched from lib/editor/links.ts
window.dispatchEvent(
  new CustomEvent("agentnote:open-note", { detail: { id: string } }),
);

agentnote-app.tsx registers a listener (same pattern as chasehuh_com’s chasehuh:open-note) and runs selectNote / router update for that id when the note exists in the sidebar list; if missing, soft no-op or fetch-by-id if that path already exists (Needs verification against current getNote client usage).

Href resolution rules (AgentNote)

Raw href Action
https://… / http://… window.open(href, "_blank", "noopener,noreferrer")
mailto:… window.location.assign
/n/{id} in-app open note {id}
other relative /… window.location.assign (or no-op if unsafe)
chasehuh_com-only /thoughts… bare paths do not port

Validation Rules

  • Only decorate when syntax tree identifies a Link/Autolink or bare URL regex hit outside occupied ranges.
  • Do not hide image alt/url chrome via this extension (image widgets own that).
  • Shift-click / alt-click must not hijack (allow native selection / browser behavior) — keep chasehuh_com guard.
  • Read-only editor: links still clickable.

Implementation Notes

Likely files to modify

  • components/codemirror-editor.tsximport { agentnoteLinks } from "@/lib/editor/links" and include in editorExtensions (edit + readOnly paths)
  • app/globals.css — port .cm-md-link / :hover
  • components/agentnote-app.tsxagentnote:open-note listener wired to selectNote + URL sync (/n/{id})

New files

  • lib/editor/links.ts — port from chasehuh_com; rename site-specific helpers; drop /thoughts|/awards from BARE_LINK_RE unless AgentNote later needs them
  • lib/editor/links.test.ts — unit tests for collect/resolve helpers (export pure functions if needed, or test via EditorState fixtures like other lib/editor/*.test.ts)

Flow

  1. User types or pastes [Sume](https://www.sume.com) or a bare URL.
  2. agentnoteLinks decorations hide chrome / mark label.
  3. Click → openHref → external tab or agentnote:open-noteselectNote.

Tests

  • Resolve href table (external / mailto / /n/id)
  • Bare URL regex does not match URL inside ](https://…) of an image or link
  • Optional: decoration presence on a small EditorState with @codemirror/lang-markdown

Edge Cases And Risks

  • IME / CRDT: decorations are CM6 state fields; must not rewrite doc text (safe with Yjs path).
  • Conflict with image widgets: ensure link collector skips image nodes / ]( destinations (reference already does).
  • Unknown /n/{id}: do not 404-navigate the whole app; prefer stay put (Needs verification of best UX).
  • Public note view (components/public-note-view.tsx / readOnly CM6): still allow external link opens.
  • Security: only open http(s) / mailto / known in-app paths — reject javascript: etc. (reference resolveHref already nulls unknown schemes).

Non-Goals

  • Open Graph / oEmbed / link-unfurl preview cards
  • YouTube / Twitter block embeds or iframes in the editor
  • Wikilinks [[Note]] syntax
  • Changing Markdown persistence format
  • Porting chasehuh_com site-specific /thoughts → note id map
  • Multi-user presence / Phase 3 collab UI

Acceptance Criteria

  • [label](url) in the editor shows label only; URL chrome hidden
  • Bare https:// URLs are styled and open in a new tab on click
  • /n/{id} opens that note in-app without losing editor shell
  • Image widgets and paste-upload path unchanged
  • CRDT / legacy body sync still persist full markdown source
  • CSS uses theme accent tokens; looks correct in light/dark themes already supported
  • Tests cover href resolution + bare-url skip inside ](
  • pnpm test / tsc / build green

QA Plan

  1. pnpm test including new links.test.ts
  2. Local: paste [example](https://example.com) — chrome hidden; click opens new tab
  3. Local: create/open note A, in note B type [go](/n/{A’s id}) — click switches to A
  4. Local: image ![x](https://…) still shows widget, not double link decoration
  5. Read-only / public view: external link still opens
  6. With NEXT_PUBLIC_AGENTNOTE_CRDT=1, edit link label text — peers converge; source still full markdown

Suggested PR Scope

S — one PR. Mostly a port of links.ts + CSS + small app wiring. No schema/migration.

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