Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .work/todo.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,6 @@
{"actor":"unknown","ev":"01M25RY5AMM31SSJHMEGCBEKSX","item":"01M25RY5AM6ZWRGEAJP34X06WN","op":"create","set":{"body":"Stamp CHANGELOG 0.6.5, lockstep Cargo.toml / Cargo.lock / tauri.conf, freeze the roadmap snapshot, tag and GitHub release after merge.","kind":"ops","level":"task","milestone":"v0.6.5","priority":"P2","status":"todo","title":"chore(release): v0.6.5 changelog, version bump, roadmap snapshot"},"ts":"2026-09-10T13:43:25Z"}
{"actor":"unknown","ev":"01M25RYA4X739KYTP12MZ7T78A","item":"01M25RY5AM6ZWRGEAJP34X06WN","op":"update","set":{"status":"in_progress"},"ts":"2026-09-10T13:43:30Z"}
{"actor":"unknown","ev":"01M25RZ3DETFBVRSQRJGJYS1YS","item":"01M25RY5AM6ZWRGEAJP34X06WN","op":"close","set":{"resolution":"v0.6.5 changelog, Cargo.toml / Cargo.lock / tauri.conf bump, roadmap snapshot, index plane re-rendered. Tag and GitHub release after merge.","status":"done"},"ts":"2026-09-10T13:43:56Z"}
{"actor":"unknown","ev":"01M3M9CAJ06T2QQRH16KJM32S3","item":"01M3M9CAJ08DJKX7T6E0EE8SP3","op":"create","set":{"body":"Clicking a markdown link that points at a file on disk loads that note in the editor. Directory paths like nested/note.md must survive TipTap's URI check. Web links stay web links. A miss does not navigate the window away.","kind":"feature","level":"task","milestone":"v0.6.6","priority":"P1","status":"todo","title":"Open on-disk markdown links in the editor"},"ts":"2026-09-28T15:15:50Z"}
{"actor":"unknown","ev":"01M3M9CF706C4JEMVXE0Z4ABMV","item":"01M3M9CAJ08DJKX7T6E0EE8SP3","op":"update","set":{"status":"in_progress"},"ts":"2026-09-28T15:15:55Z"}
{"actor":"unknown","ev":"01M3M9CF8FR5DR5MCD4YPYH4B7","item":"01M3M9CAJ08DJKX7T6E0EE8SP3","op":"close","set":{"resolution":"WYSIWYG click loads an in-folder markdown target. TipTap URI check no longer drops dir/note.md. Miss does not navigate. e2e/links.spec.ts 3 passed. Outside-folder switch is desktop-only and not dogfooded.","status":"done"},"ts":"2026-09-28T15:15:55Z"}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to Motion are recorded here. Dates are UTC.

## Unreleased

### Added

- **Note links.** Click a link in the editor and, if it points at a markdown
file on disk, that note opens. Relative paths resolve from the current note,
then from the folder. Web links stay web links. A link that is not a file
does not navigate the window away.

## 0.6.5 — 2026-09-10

Header drag actually works on a Mac; the format toolbar stays put while the note
Expand Down
21 changes: 21 additions & 0 deletions docs/.index/_graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,16 @@
"to": "release/v0.6.5",
"type": "targets"
},
{
"from": "item/01M3M9CAJ08DJKX7T6E0EE8SP3",
"to": "pr/62",
"type": "lands-in"
},
{
"from": "item/01M3M9CAJ08DJKX7T6E0EE8SP3",
"to": "release/v0.6.6",
"type": "targets"
},
{
"from": "plan/2026-07-26-motion-next-phase",
"to": "item/01KYFZ6RBH4J746JDANTQKYVSN",
Expand Down Expand Up @@ -2159,6 +2169,11 @@
"status": "done",
"title": "chore(release): v0.6.5 changelog, version bump, roadmap snapshot"
},
"item/01M3M9CAJ08DJKX7T6E0EE8SP3": {
"doc_type": "item",
"status": "done",
"title": "Open on-disk markdown links in the editor"
},
"plan/2026-07-26-motion-next-phase": {
"doc_type": "plan",
"source": "docs/plans/2026-07-26-motion-next-phase.md",
Expand Down Expand Up @@ -2252,6 +2267,9 @@
"pr/61": {
"doc_type": "pr"
},
"pr/62": {
"doc_type": "pr"
},
"release/v0.1.0": {
"doc_type": "release"
},
Expand All @@ -2276,6 +2294,9 @@
"release/v0.6.5": {
"doc_type": "release"
},
"release/v0.6.6": {
"doc_type": "release"
},
"roadmap": {
"doc_type": "roadmap",
"source": "docs/roadmap.md",
Expand Down
4 changes: 2 additions & 2 deletions docs/.index/_inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,10 @@
{
"canonical_key": "roadmap",
"doc_type": "roadmap",
"generated_at": "2026-09-10T13:43:56Z",
"generated_at": "2026-09-28T15:15:55Z",
"slug": "roadmap",
"source": "docs/roadmap.md",
"source_hash": "084d94a8",
"source_hash": "54c1f073",
"title": "Roadmap",
"truth_state": "current",
"wiki": "https://github.com/SpillwaveSolutions/motion/wiki/Roadmap",
Expand Down
2 changes: 2 additions & 0 deletions docs/.index/item/01M3M9CAJ08DJKX7T6E0EE8SP3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
code:
- {pr: 62}
38 changes: 34 additions & 4 deletions docs/.index/publish-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"frozen": false,
"page_name": "Index-Traceability",
"render": "as-is",
"render_hash": "e34c8294bac6",
"render_hash": "5e42ca21ebac",
"source": "docs/.index/rendered/traceability.md",
"title": "Traceability Index",
"truth_state": "current",
Expand Down Expand Up @@ -240,6 +240,16 @@
"truth_state": "not tracked",
"wiki_key": "pr/61"
},
{
"frozen": false,
"page_name": "PR-62",
"render": "as-is",
"render_hash": "61aee400c1d0",
"source": "docs/.index/rendered/prs/62.md",
"title": "PR #62",
"truth_state": "not tracked",
"wiki_key": "pr/62"
},
{
"frozen": false,
"page_name": "Release-v0.1.0+dogfood",
Expand Down Expand Up @@ -320,6 +330,16 @@
"truth_state": "shipped",
"wiki_key": "release/v0.6.5"
},
{
"frozen": false,
"page_name": "Release-v0.6.6",
"render": "as-is",
"render_hash": "3ee0e9f04a8f",
"source": "docs/.index/rendered/releases/v0.6.6.md",
"title": "Release v0.6.6",
"truth_state": "shipped",
"wiki_key": "release/v0.6.6"
},
{
"frozen": false,
"page_name": "Ticket-01KYDZ4A5HN1AZ7BN8Q2WV3XVN",
Expand Down Expand Up @@ -1630,6 +1650,16 @@
"truth_state": "done",
"wiki_key": "item/01M25RY5AM6ZWRGEAJP34X06WN"
},
{
"frozen": false,
"page_name": "Ticket-01M3M9CAJ08DJKX7T6E0EE8SP3",
"render": "as-is",
"render_hash": "52f730224dd8",
"source": "docs/.index/rendered/tickets/01M3M9CAJ08DJKX7T6E0EE8SP3.md",
"title": "Open on-disk markdown links in the editor",
"truth_state": "done",
"wiki_key": "item/01M3M9CAJ08DJKX7T6E0EE8SP3"
},
{
"banner": "> **Current** \u2014 this is the living version; regenerated at 2026-07-29T23:00:00Z. Historical snapshots are linked from [[Index-Releases]].",
"frozen": false,
Expand Down Expand Up @@ -1719,11 +1749,11 @@
"wiki_key": "plan/2026-09-03-editor-surface-zoom-layout-icons"
},
{
"banner": "> **Current** \u2014 this is the living version; regenerated at 2026-09-10T13:43:56Z. Historical snapshots are linked from [[Index-Releases]].",
"banner": "> **Current** \u2014 this is the living version; regenerated at 2026-09-28T15:15:55Z. Historical snapshots are linked from [[Index-Releases]].",
"frozen": false,
"page_name": "Roadmap",
"render": "doc+banner",
"render_hash": "5ae901bdd074",
"render_hash": "62ddeedd6b15",
"source": "docs/roadmap.md",
"title": "Roadmap",
"truth_state": "current",
Expand Down Expand Up @@ -1811,7 +1841,7 @@
"frozen": false,
"page_name": "User-Guide",
"render": "doc+banner",
"render_hash": "da2058074062",
"render_hash": "02928d595135",
"source": "docs/user_guide/user-guide.md",
"title": "User Guide",
"truth_state": "current",
Expand Down
13 changes: 13 additions & 0 deletions docs/.index/rendered/prs/62.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# PR #62

`pr/62` · status: **not tracked**

Changed files: not tracked. Test/Review status: not tracked (see the deferred PR live-metadata sync item).

## Linked Tickets

- [[Ticket-01M3M9CAJ08DJKX7T6E0EE8SP3]] Open on-disk markdown links in the editor — Clicking a markdown link that points at a file on disk loads that note in the editor.

## Related Releases

- [[Release-v0.6.6]]
17 changes: 17 additions & 0 deletions docs/.index/rendered/releases/v0.6.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Release v0.6.6

`release/v0.6.6` · **shipped**

1 item(s) shipped.

## Change Log

- [[Ticket-01M3M9CAJ08DJKX7T6E0EE8SP3]] Open on-disk markdown links in the editor — Clicking a markdown link that points at a file on disk loads that note in the editor.

## Release Tree

- [[Ticket-01M3M9CAJ08DJKX7T6E0EE8SP3]] Open on-disk markdown links in the editor (task)

## Related PRs

- [[PR-62]]
13 changes: 13 additions & 0 deletions docs/.index/rendered/tickets/01M3M9CAJ08DJKX7T6E0EE8SP3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Open on-disk markdown links in the editor

`01M3M9CAJ08DJKX7T6E0EE8SP3` · task/feature · **done**

Clicking a markdown link that points at a file on disk loads that note in the editor.

## Linked PRs

- [[PR-62]]

## Release

- [[Release-v0.6.6]]
5 changes: 5 additions & 0 deletions docs/.index/rendered/traceability.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

_The evidence chain: plan → item → ticket → code → release, forward and backward. Generated from `docs/.index/_graph.json`; do not edit._

### Open on-disk markdown links in the editor
`01M3M9CAJ08DJKX7T6E0EE8SP3` · status: done
- lands-in: pr/62
- targets: release/v0.6.6

### chore(release): v0.6.5 changelog, version bump, roadmap snapshot
`01M25RY5AM6ZWRGEAJP34X06WN` · status: done
- lands-in: pr/61
Expand Down
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
wiki_key: roadmap
doc_type: roadmap
truth_state: current
source_hash: 084d94a8
generated_at: 2026-09-10T13:43:56Z
source_hash: 54c1f073
generated_at: 2026-09-28T15:15:55Z
---

<!-- GENERATED by worklog roadmap-render. DO NOT EDIT. -->
Expand Down
7 changes: 6 additions & 1 deletion docs/user_guide/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ opens the app.
2. The sidebar is a **collapsible directory tree** of every `.md` underneath
the workspace. Use **Search notes** (⌘K / Ctrl+K) to filter by filename or
by text inside the notes. A content hit shows a short snippet.
3. Click a note to open it.
3. Click a note to open it. In the note itself, click a link and Motion opens
that file when it is markdown on disk. `getting-started.md`, `./notes/topic.md`,
`../welcome.md`, a name without `.md`, and a folder (its `README.md` or
`index.md`) all work. A web address is not opened as a note. A link that
does not match a file leaves you where you are. On the Mac app, an absolute
path to a markdown file outside the open folder opens that file's folder.
4. Edit, then press **⌘S** (Ctrl+S) or click the **Save** icon in the
header. The status area shows **Saving…** / **Saved** / **Save failed**.

Expand Down
41 changes: 41 additions & 0 deletions e2e/links.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* A markdown link to a file on disk loads that note. A miss must not
* navigate the webview (the page guard treats the resulting 404 as a failure).
*/
import { test, expect, gotoApp } from "./fixtures";

test("clicking a relative markdown link opens that note", async ({ page }) => {
await gotoApp(page, "/?open=scratch-links.md");
await expect(page.locator(".ProseMirror")).toContainText("Links");

await page.locator(".ProseMirror a", { hasText: "Getting started" }).click();

await expect(page.getByRole("treeitem", { name: "getting-started.md" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(page.locator(".ProseMirror")).toContainText("Getting started");
await expect(page.getByRole("heading", { name: "Getting started" })).toBeVisible();
});

test("clicking a nested markdown link opens that note", async ({ page }) => {
await gotoApp(page, "/?open=scratch-links.md");
await page.locator(".ProseMirror a", { hasText: "Deeper" }).click();

await expect(page.getByRole("treeitem", { name: "deeper.md" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(page.locator(".ProseMirror")).toContainText("Deeper");
});

test("a link that is not on disk stays on the note", async ({ page }) => {
await gotoApp(page, "/?open=scratch-links.md");
await page.locator(".ProseMirror a", { hasText: "Missing" }).click();

await expect(page.getByRole("treeitem", { name: "scratch-links.md" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(page.locator(".ProseMirror")).toContainText("Links");
});
3 changes: 2 additions & 1 deletion e2e/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const SEED_FILES: Record<string, string> = {
"# Scratch: commands\n\nThe quick brown fox jumps over the lazy dog.\n\n| Name | Role |\n| --- | --- |\n| Ada | Engineer |\n",
// Tight GFM pipes (no padding). The serializer rewrites these to
// `| Name | Role |` — switching views must not treat that as an edit.
"scratch-dirty.md": "# Tight pipes\n\n|Name|Role|\n|---|---|\n|Ada|Engineer|\n",
"scratch-links.md":
"# Links\n\nSee [Getting started](getting-started.md).\n\nSee [Deeper](nested/deeper.md).\n\nSee [Missing](does-not-exist.md).\n",
};

export function createWorkspace(): string {
Expand Down
24 changes: 24 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { storage, rememberWorkspaceRoot, relativeToWorkspace, isTauri } from "./lib/storage";
import { synthesizeWorkspace } from "./lib/workspaceSynthesis";
import { parseOpenQuery, resolveOpenQuery } from "./lib/openFile";
import type { NoteLinkAction } from "./lib/noteLink";
import { loadPersistedWorkspace, persistWorkspace } from "./lib/workspaceMemory";
import { buildCopyPayload, writeCopyPayload } from "./lib/copyNote";
import { useZoom } from "./lib/useZoom";
Expand Down Expand Up @@ -456,6 +457,26 @@ function App() {
setNotesOpen(false);
};

const handleOpenLink = useCallback(
(action: Extract<NoteLinkAction, { action: "open" | "switch" }>) => {
if (action.action === "switch") {
// Browser mode's workspace is fixed by the server. Only the
// desktop app can follow a markdown file outside the folder.
if (isTauri()) void activateWorkspace(action.workspace, action.file);
return;
}
setCurrentFilePath(action.path);
setNotesOpen(false);
setExpanded((prev) => {
if (prev === null || !workspacePath) return prev;
const next = new Set(prev);
for (const folder of ancestorFolders([action.path], workspacePath)) next.add(folder);
return next;
});
},
[activateWorkspace, workspacePath],
);

const handleNewNote = async () => {
if (!workspacePath) {
alert("Open a folder first to create a new note.");
Expand Down Expand Up @@ -1052,6 +1073,9 @@ function App() {
onMarkdownChange={handleMarkdownChange}
splitRatio={splitRatio}
onSplitRatioChange={setSplitRatio}
workspaceRoot={workspacePath}
files={files}
onOpenLink={handleOpenLink}
/>
</main>

Expand Down
Loading
Loading