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.ts — agentnoteLinks(): 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
- 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.
- Bare
http(s)://… URLs in the buffer are underlined and clickable (same styling, optional --bare modifier).
- 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)
- Markdown source in the document / CRDT / sync remains the full
[label](url) string — decorations only; no body rewrite on click.
- Images (
) are untouched; destination URLs inside ]( image/link markup must not double-match as bare links (chasehuh_com already skips ]( prefixes).
- 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.tsx — agentnoteLinks() 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.tsx — import { agentnoteLinks } from "@/lib/editor/links" and include in editorExtensions (edit + readOnly paths)
app/globals.css — port .cm-md-link / :hover
components/agentnote-app.tsx — agentnote: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
- User types or pastes
[Sume](https://www.sume.com) or a bare URL.
agentnoteLinks decorations hide chrome / mark label.
- Click →
openHref → external tab or agentnote:open-note → selectNote.
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
QA Plan
pnpm test including new links.test.ts
- Local: paste
[example](https://example.com) — chrome hidden; click opens new tab
- Local: create/open note A, in note B type
[go](/n/{A’s id}) — click switches to A
- Local: image
 still shows widget, not double link decoration
- Read-only / public view: external link still opens
- 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.
Summary
Port Obsidian-style clickable Markdown links from
chasehuh/chasehuh_cominto 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
chasehuh/chasehuh_com, local/Users/huhchaewon/chase/chasehuh_com), file a self-contained mega-issue, then implement via Grok → PR → merge.lib/editor/links.ts+.cm-md-linkCSS), not oEmbed / OG unfurl cards. One static YouTube<iframe>exists in an old log MD file and is out of scope./n/{id}deep links. No open duplicate for clickable links.Current Behavior
AgentNote (
chasehuh/agentnote@main)components/codemirror-editor.tsxwires markdown, strikethrough, image paste/widgets, list indent, etc.lib/editor/has nolinks.ts.https://…URLs render as plain text (full[label](url)chrome visible)./n/{id}(components/agentnote-app.tsxhelpernotePath).Reference (already shipping):
chasehuh/chasehuh_comlib/editor/links.ts—agentnoteLinks(): ExtensionLink,Autolink)[label](url); keeps label visible with classcm-md-linkhttps?:\/\/…(and site-specific/thoughts|/awardspaths) getcm-md-link--barewindow.open(..., "noopener,noreferrer")components/codemirror-editor.tsxincludesagentnoteLinks()in the extension listapp/globals.css—.cm-md-link/:hoverusing--c-text-accentc2b1a39(“Add clickable Obsidian-style markdown links in the editor”)Desired Behavior
[label](url)shows only the label, underlined/accent-colored and clickable.http(s)://…URLs in the buffer are underlined and clickable (same styling, optional--baremodifier).https?://…/mailto:→ open as today on chasehuh_com (window.open/location.assign)/n/{id}(and/?n=only if still present anywhere) → select that note without full page reload, mirroring chasehuh_com’schasehuh:open-notepattern but adapted to AgentNote’s router/state (/n/{id}+selectNote)[label](url)string — decorations only; no body rewrite on click.) are untouched; destination URLs inside](image/link markup must not double-match as bare links (chasehuh_com already skips](prefixes).--c-text-accentlike chasehuh_com / existing accent).Source Of Truth
Port from (canonical implementation)
/Users/huhchaewon/chase/chasehuh_com/lib/editor/links.ts— full extension (copy then adaptopenHref/ path resolution)/Users/huhchaewon/chase/chasehuh_com/components/codemirror-editor.tsx—agentnoteLinks()wiring/Users/huhchaewon/chase/chasehuh_com/app/globals.css(~L489–498) —.cm-md-linkstylesc2b1a39and follow-ups)AgentNote targets
components/codemirror-editor.tsx— add extensionapp/globals.css— add.cm-md-linkrules next to existing.cm-md-imageblockcomponents/agentnote-app.tsx— listen for in-app open-note custom event or pass a callback into the editor; must call existingselectNote/ navigation for/n/{id}lib/editor/links.ts(+lib/editor/links.test.ts)External
@codemirror/lang-markdown/ Lezer markdown node namesLink,Autolink,URL,LinkMarkProposed API / Schema
No new HTTP API. Editor-only.
Custom event (recommended, mirrors chasehuh_com)
agentnote-app.tsxregisters a listener (same pattern as chasehuh_com’schasehuh:open-note) and runsselectNote/ 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 currentgetNoteclient usage).Href resolution rules (AgentNote)
https://…/http://…window.open(href, "_blank", "noopener,noreferrer")mailto:…window.location.assign/n/{id}{id}/…window.location.assign(or no-op if unsafe)/thoughts…bare pathsValidation Rules
Implementation Notes
Likely files to modify
components/codemirror-editor.tsx—import { agentnoteLinks } from "@/lib/editor/links"and include ineditorExtensions(edit + readOnly paths)app/globals.css— port.cm-md-link/:hovercomponents/agentnote-app.tsx—agentnote:open-notelistener wired toselectNote+ URL sync (/n/{id})New files
lib/editor/links.ts— port from chasehuh_com; rename site-specific helpers; drop/thoughts|/awardsfromBARE_LINK_REunless AgentNote later needs themlib/editor/links.test.ts— unit tests for collect/resolve helpers (export pure functions if needed, or test via EditorState fixtures like otherlib/editor/*.test.ts)Flow
[Sume](https://www.sume.com)or a bare URL.agentnoteLinksdecorations hide chrome / mark label.openHref→ external tab oragentnote:open-note→selectNote.Tests
/n/id)](https://…)of an image or linkEditorStatewith@codemirror/lang-markdownEdge Cases And Risks
](destinations (reference already does)./n/{id}: do not 404-navigate the whole app; prefer stay put (Needs verification of best UX).components/public-note-view.tsx/ readOnly CM6): still allow external link opens.http(s)/mailto/ known in-app paths — rejectjavascript:etc. (referenceresolveHrefalready nulls unknown schemes).Non-Goals
[[Note]]syntax/thoughts→ note id mapAcceptance Criteria
[label](url)in the editor shows label only; URL chrome hiddenhttps://URLs are styled and open in a new tab on click/n/{id}opens that note in-app without losing editor shell](pnpm test/tsc/ build greenQA Plan
pnpm testincluding newlinks.test.ts[example](https://example.com)— chrome hidden; click opens new tab[go](/n/{A’s id})— click switches to Astill shows widget, not double link decorationNEXT_PUBLIC_AGENTNOTE_CRDT=1, edit link label text — peers converge; source still full markdownSuggested PR Scope
S — one PR. Mostly a port of
links.ts+ CSS + small app wiring. No schema/migration.