Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
61eac76
Fix(vim): Ctrl+A increments, and Settings and sidebar keys stay in pl…
adibhanna Oct 26, 2026
69f34c0
Feat(tags): the Tags view lists the tagged blocks inside each note (#…
adibhanna Oct 26, 2026
03df3dd
Fix(editor): a link in a list, heading, or emphasis keeps its link color
adibhanna Oct 26, 2026
fa200ea
Fix(tables): a link to a missing note looks unresolved in a table cell
adibhanna Oct 26, 2026
878225d
Feat(search): a #tag in Search notes lists each note's tagged blocks …
adibhanna Oct 26, 2026
dc4210a
Fix(cloud): a conflict review opens while a sync is running, and neve…
adibhanna Oct 26, 2026
0000cc4
Perf(cloud): a pulled change costs the same in a vault of any size
adibhanna Oct 26, 2026
95f48cd
Feat(calendar): a phone opens the calendar from Home, and a second ta…
adibhanna Oct 26, 2026
4050388
Fix(phones): a note moved to Trash leaves you on Home, not in an old …
adibhanna Oct 26, 2026
4f50b8c
Fix(cloud): a phone notices a stalled request in 30 seconds, not 300
adibhanna Oct 26, 2026
7afe7a3
Fix(tags): code in a note saved with Windows line endings stops showi…
adibhanna Oct 26, 2026
37a68a3
Fix(sidebar): the Vim cursor stays on its row when the rows renumber
adibhanna Oct 26, 2026
f69b5fa
Feat(links): every unresolved link in one list, and a popover to writ…
adibhanna Oct 26, 2026
9ab0f1c
Docs(whats-new): the 2.77.0 highlights
adibhanna Oct 26, 2026
69b6b5f
Release: align desktop and shared packages at 2.77.0
adibhanna Oct 26, 2026
3f0356d
Fix(sidebar): the first outside write after a folder click leaves the…
adibhanna Oct 26, 2026
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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/desktop",
"productName": "ZenNotes",
"version": "2.76.0",
"version": "2.77.0",
"description": "ZenNotes desktop shell",
"private": true,
"main": "./out/main/index.js",
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/main/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ const SCALAR_FIELDS: Partial<Record<PortablePrefKey, ScalarFieldMap>> = {
tomlKey: 'fzf_path',
comment: 'absolute path to fzf; empty = look on $PATH'
},
searchTaggedBlocks: {
section: 'search',
tomlKey: 'tagged_blocks',
comment: 'with a #tag in Search notes, list the tagged blocks under each note'
},
// editor
livePreview: {
section: 'editor',
Expand Down Expand Up @@ -179,6 +184,11 @@ const SCALAR_FIELDS: Partial<Record<PortablePrefKey, ScalarFieldMap>> = {
tomlKey: 'list_indent_guides',
comment: 'vertical guide lines at each nested list level'
},
linkPopoverOnHover: {
section: 'editor',
tomlKey: 'link_popover_on_hover',
comment: 'resting the pointer on a [[wikilink]] opens the link popover'
},
editorScrollOff: {
section: 'editor',
tomlKey: 'scroll_off',
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/main/vault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,22 @@ describe('listNotes metadata parsing', () => {
expect(note?.tags).toEqual(['realtag'])
})

it('does not index #tags or [[links]] inside a fence in a CRLF note', async () => {
const root = await makeTempDir('zennotes-meta-fence-crlf-')
await ensureVaultLayout(root)
const rel = 'inbox/windows.md'
await writeFile(
path.join(root, rel),
'# Notes\r\n\r\n```c\r\n#include <stdio.h>\r\n// [[Secret Note]]\r\n```\r\n\r\n#realtag [[Real Link]]\r\n',
'utf8'
)

const notes = await listNotes(root)
const note = notes.find((n) => n.path === rel)
expect(note?.tags).toEqual(['realtag'])
expect(note?.wikilinks).toEqual(['Real Link'])
})

it('indexes frontmatter tags as first-class note tags', async () => {
const root = await makeTempDir('zennotes-meta-frontmatter-tags-')
await ensureVaultLayout(root)
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop/src/main/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1979,8 +1979,9 @@ async function folderOf(root: string, absPath: string): Promise<NoteFolder | nul
* indentation-tolerant: a fence nested under a list item is still a code block,
* so its contents (e.g. a C `#include` line) must not be scanned. A
* column-0-anchored regex missed indented fences and leaked them as tags (#293).
* Mirrors `stripCodeContent` in packages/app-core/src/lib/tags.ts and
* internal/vault/parse.go in ZenNotes/znserver — keep the three in sync.
* Mirrors `stripCodeContent` in packages/shared-domain/src/tag-scan.ts,
* apps/desktop/src/mcp/vault-ops.ts, packages/app-core/src/lib/wikilinks.ts and
* internal/vault/parse.go in ZenNotes/znserver: keep all five in sync.
*/
function stripCodeContent(body: string): string {
if (!body.includes('`') && !body.includes('~')) return body
Expand All @@ -1990,7 +1991,8 @@ function stripCodeContent(body: string): string {
let fenceLen = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i] as string
const m = /^[ \t]*(`{3,}|~{3,})(.*)$/.exec(line)
// `.` never matches `\r`, so the `\r?` is what lets a CRLF-saved fence line match.
const m = /^[ \t]*(`{3,}|~{3,})(.*)\r?$/.exec(line)
if (m) {
const marker = m[1] as string
const char = marker[0] as string
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/mcp/vault-ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ describe('mcp task states', () => {
})
})

describe('list_notes metadata skips fenced code', () => {
it('does not index #tags or [[links]] inside a fence in a CRLF note', async () => {
await writeFile(
path.join(root, 'inbox', 'Windows.md'),
'# Notes\r\n\r\n```c\r\n#include <stdio.h>\r\n// [[Secret Note]]\r\n```\r\n\r\n#realtag [[Real Link]]\r\n'
)

const notes = await listNotes(root)
const note = notes.find((n) => n.path === 'inbox/Windows.md')
expect(note?.tags).toEqual(['realtag'])
expect(note?.wikilinks).toEqual(['Real Link'])
})
})

describe('remapped system folders (#398)', () => {
it('classifies a remapped trash directory as trash and keeps its tasks out', async () => {
await mkdir(path.join(root, '.zennotes'), { recursive: true })
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/mcp/vault-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ async function folderOf(root: string, abs: string): Promise<NoteFolder | null> {
* never read code as content. Line-based and indentation-tolerant: a fence
* nested under a list item is still a code block (#293). Mirrors
* `stripCodeContent` in apps/desktop/src/main/vault.ts,
* packages/app-core/src/lib/{tags,wikilinks}.ts, and
* packages/shared-domain/src/tag-scan.ts, packages/app-core/src/lib/wikilinks.ts, and
* internal/vault/parse.go in ZenNotes/znserver — keep all five in sync.
*/
function stripCodeContent(body: string): string {
Expand All @@ -669,7 +669,8 @@ function stripCodeContent(body: string): string {
let fenceLen = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i] as string
const m = /^[ \t]*(`{3,}|~{3,})(.*)$/.exec(line)
// `.` never matches `\r`, so the `\r?` is what lets a CRLF-saved fence line match.
const m = /^[ \t]*(`{3,}|~{3,})(.*)\r?$/.exec(line)
if (m) {
const marker = m[1] as string
const char = marker[0] as string
Expand Down
2 changes: 1 addition & 1 deletion apps/quicklook/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/quicklook",
"private": true,
"version": "2.76.0",
"version": "2.77.0",
"type": "module",
"description": "The macOS Quick Look preview for Markdown files: the page it renders and the Swift extension that serves it",
"homepage": "https://zennotes.org",
Expand Down
2 changes: 1 addition & 1 deletion apps/share-viewer/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/share-viewer",
"private": true,
"version": "2.76.0",
"version": "2.77.0",
"type": "module",
"description": "Read-only renderer for publicly shared ZenNotes, embedded by the zennotes.org website",
"homepage": "https://zennotes.org",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/web",
"private": true,
"version": "2.76.0",
"version": "2.77.0",
"type": "module",
"description": "ZenNotes web client for self-hosted and hosted deployments",
"homepage": "https://zennotes.org",
Expand Down
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zennotes-monorepo",
"private": true,
"version": "2.76.0",
"version": "2.77.0",
"description": "ZenNotes monorepo for desktop, web, and self-hosted server builds",
"packageManager": "npm@10.9.2",
"engines": {
Expand Down
3 changes: 2 additions & 1 deletion packages/app-core/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "@zennotes/app-core",
"private": true,
"version": "2.76.0",
"version": "2.77.0",
"type": "module",
"exports": {
"./main": "./src/main.tsx",
"./navigation": "./src/navigation.ts",
"./calendar": "./src/calendar.tsx",
"./notes": "./src/notes.ts",
"./shell": "./src/shell.ts",
"./browse": "./src/browse.ts",
Expand Down
10 changes: 10 additions & 0 deletions packages/app-core/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ const Editor = lazy(async () => {
return { default: module.Editor }
})

const LinkPopover = lazy(async () => {
const module = await import('./components/LinkPopover')
return { default: module.LinkPopover }
})
const PinnedReferencePane = lazy(async () => {
const module = await import('./components/PinnedReferencePane')
return { default: module.PinnedReferencePane }
Expand Down Expand Up @@ -370,6 +374,7 @@ function App(): JSX.Element {
const init = useStore((s) => s.init)
const workspaceRestored = useStore((s) => s.workspaceRestored && !s.workspaceTransitioning)
const searchOpen = useStore((s) => s.searchOpen)
const linkPopoverOpen = useStore((s) => s.linkPopover !== null)
const setSearchOpen = useStore((s) => s.setSearchOpen)
const vaultTextSearchOpen = useStore((s) => s.vaultTextSearchOpen)
const setVaultTextSearchOpen = useStore((s) => s.setVaultTextSearchOpen)
Expand Down Expand Up @@ -1340,6 +1345,11 @@ function App(): JSX.Element {
<WhatsNewModal />
</Suspense>
)}
{linkPopoverOpen && (
<Suspense fallback={null}>
<LinkPopover />
</Suspense>
)}
<PromptHost />
<ConfirmHost />
<DatePickerHost />
Expand Down
15 changes: 15 additions & 0 deletions packages/app-core/src/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CalendarPanel } from './components/CalendarPanel'
import { useStore } from './store'

/**
* The date calendar as a surface of its own, for hosts that open it from a
* main screen instead of beside a note (the phone shells' Calendar entry),
* so it works with no note open. It orients on the open note when that is a
* daily or weekly note, else on today. `onOpen` runs once it has opened a
* day, a week or a task's line; a host that shows it as a sheet closes it
* there.
*/
export function DateCalendar({ onOpen }: { onOpen?: () => void }): JSX.Element {
const note = useStore((s) => s.activeNote)
return <CalendarPanel note={note} onOpen={onOpen} />
}
Loading
Loading