diff --git a/README.md b/README.md index f7bfe92..aa2dcf2 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ and `@zennotes/shared-domain` packages. The exact archives are vendored under `vendor/zennotes/` with their source identity and checksums (`manifest.json`), and `package-lock.json` pins the complete install. No source checkout is used. The vendored set is the core release -[core-2.68.0-core.h6e3db1e19bc2b662](https://github.com/ZenNotes/zennotes/releases/tag/core-2.68.0-core.h6e3db1e19bc2b662) -(desktop commit `881baa6a`, clean tree). Run `npm run +[core-2.69.0-core.h27a2c5df8414d1dd](https://github.com/ZenNotes/zennotes/releases/tag/core-2.69.0-core.h27a2c5df8414d1dd) +(desktop commit `d1344076`, clean tree). Run `npm run boundaries:check` to verify archives, installed versions, singleton editor/React peers, and imports; it refuses an archive built from a dirty upstream tree unless `ZEN_ALLOW_DIRTY_CORE=1` is set for a local try-out. diff --git a/android/app/build.gradle b/android/app/build.gradle index 8c8014a..61b1d9d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "md.zennotes" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 41 - versionName "1.1.38" + versionCode 42 + versionName "1.1.39" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/package-lock.json b/package-lock.json index 33491da..434777f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zennotes-android", - "version": "1.1.38", + "version": "1.1.39", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zennotes-android", - "version": "1.1.38", + "version": "1.1.39", "dependencies": { "@aparajita/capacitor-secure-storage": "^8.0.1", "@capacitor/android": "^8.5.2", @@ -32,9 +32,9 @@ "@lezer/highlight": "^1.2.5", "@replit/codemirror-vim": "^6.3.0", "@xyflow/react": "^12.12.0", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.68.0-core.h6e3db1e19bc2b662.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.68.0-boundaries.hd2ce4427b284c18e.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.68.0-boundaries.hd2ce4427b284c18e.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.69.0-core.h27a2c5df8414d1dd.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.69.0-boundaries.h240b8e06d33b0cdc.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.69.0-boundaries.h240b8e06d33b0cdc.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.16", "function-plot": "^1.25.3", @@ -3655,9 +3655,9 @@ } }, "node_modules/@zennotes/app-core": { - "version": "2.68.0-core.h6e3db1e19bc2b662", - "resolved": "file:vendor/zennotes/zennotes-app-core-2.68.0-core.h6e3db1e19bc2b662.tgz", - "integrity": "sha512-Qrt0Z2CmP0b9mtdGqJjj1xWxaEsWc9a1LbwE3WJWky0ThjS0FBTTYRHGjfLJD5RWu03XxuKr4GJJFbhHRATz4Q==", + "version": "2.69.0-core.h27a2c5df8414d1dd", + "resolved": "file:vendor/zennotes/zennotes-app-core-2.69.0-core.h27a2c5df8414d1dd.tgz", + "integrity": "sha512-TLxG5aOJ/ma2JMuYnH6aKmMLorX0xEczCR+frYbnQF1vzZa5xDf0tJpPiwQYaZQ3rlfGgeslh6wp/C3r8pWETw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.18.3", @@ -3686,8 +3686,8 @@ "@myriaddreamin/typst.ts": "^0.7.0", "@replit/codemirror-vim": "^6.3.0", "@xyflow/react": "^12.11.2", - "@zennotes/bridge-contract": "2.68.0-boundaries.hd2ce4427b284c18e", - "@zennotes/shared-domain": "2.68.0-boundaries.hd2ce4427b284c18e", + "@zennotes/bridge-contract": "2.69.0-boundaries.h240b8e06d33b0cdc", + "@zennotes/shared-domain": "2.69.0-boundaries.h240b8e06d33b0cdc", "dompurify": "^3.3.4", "function-plot": "^1.25.3", "gray-matter": "^4.0.3", @@ -3731,19 +3731,19 @@ } }, "node_modules/@zennotes/bridge-contract": { - "version": "2.68.0-boundaries.hd2ce4427b284c18e", - "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.68.0-boundaries.hd2ce4427b284c18e.tgz", - "integrity": "sha512-b+njMYCybpZzix7WYDrs73LjF8ff8abDeoQ/ocKJWRn9ZOGNLGiG62eLOlsEvqIO5tpuzjQeUQNjLCIqOrBn8w==", + "version": "2.69.0-boundaries.h240b8e06d33b0cdc", + "resolved": "file:vendor/zennotes/zennotes-bridge-contract-2.69.0-boundaries.h240b8e06d33b0cdc.tgz", + "integrity": "sha512-dOEGi04d2owJwEOk2NmK2fAyLwAQgPM0RNoyGhDKcLh0wrcVg8NPyAvkeTCAQw5JuLYOTfSEjRhHJiHNHm9tdw==", "license": "MIT" }, "node_modules/@zennotes/shared-domain": { - "version": "2.68.0-boundaries.hd2ce4427b284c18e", - "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.68.0-boundaries.hd2ce4427b284c18e.tgz", - "integrity": "sha512-oxluGn1xX+7YLHTt0v04vs4T/cFUOwIrB4aF16OpsoQ8Tm+cfFeRxAE6xYjybTWITT/qDSIbQyBNfiFK0puqQg==", + "version": "2.69.0-boundaries.h240b8e06d33b0cdc", + "resolved": "file:vendor/zennotes/zennotes-shared-domain-2.69.0-boundaries.h240b8e06d33b0cdc.tgz", + "integrity": "sha512-fxtYAl8c/FYfnS7FGZB+mKGVaGtOBuNDUWl2RZmDZcjSBtpuU6QAdWLsqk7JFkd2EZvqlpo7AWtcuGWPn0y81w==", "license": "MIT", "dependencies": { "@scure/bip39": "2.4.0", - "@zennotes/bridge-contract": "2.68.0-boundaries.hd2ce4427b284c18e", + "@zennotes/bridge-contract": "2.69.0-boundaries.h240b8e06d33b0cdc", "hash-wasm": "4.12.0", "lz-string": "^1.5.0" } diff --git a/package.json b/package.json index 4ace23c..33537be 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zennotes-android", "private": true, - "version": "1.1.38", + "version": "1.1.39", "type": "module", "description": "ZenNotes for Android — Capacitor shell over the ZenNotes app core", "homepage": "https://zennotes.org", @@ -43,9 +43,9 @@ "@lezer/highlight": "^1.2.5", "@replit/codemirror-vim": "^6.3.0", "@xyflow/react": "^12.12.0", - "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.68.0-core.h6e3db1e19bc2b662.tgz", - "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.68.0-boundaries.hd2ce4427b284c18e.tgz", - "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.68.0-boundaries.hd2ce4427b284c18e.tgz", + "@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.69.0-core.h27a2c5df8414d1dd.tgz", + "@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.69.0-boundaries.h240b8e06d33b0cdc.tgz", + "@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.69.0-boundaries.h240b8e06d33b0cdc.tgz", "codemirror": "^6.0.1", "dompurify": "^3.4.16", "function-plot": "^1.25.3", diff --git a/src/bridge/cloud-sync-repository.ts b/src/bridge/cloud-sync-repository.ts index dc95ee1..75015e0 100644 --- a/src/bridge/cloud-sync-repository.ts +++ b/src/bridge/cloud-sync-repository.ts @@ -330,7 +330,7 @@ function itemFromCache(path: string, cached: ScanCacheEntry): CloudSyncLocalItem enumerable: true, get(): string { throw new Error( - `Cloud sync tried to push ${path} from the scan cache without reading it — the engine's hash-equal items must never need content.` + `Cloud sync tried to push ${path} from the scan cache without reading it: the engine's hash-equal items must never need content.` ) } }) diff --git a/src/bridge/mobile-bridge.ts b/src/bridge/mobile-bridge.ts index f69654e..ceb2a0c 100644 --- a/src/bridge/mobile-bridge.ts +++ b/src/bridge/mobile-bridge.ts @@ -154,7 +154,7 @@ import { import { folderForRelativePath, posixNormalize, sanitizeNoteTitle } from './vault-core' import { isPhoneViewport } from '../viewport' -let appVersion = '1.1.38' +let appVersion = '1.1.39' export async function loadNativeAppVersion(): Promise { try { diff --git a/src/bridge/vault-fs.ts b/src/bridge/vault-fs.ts index 3ac1c55..cc5187d 100644 --- a/src/bridge/vault-fs.ts +++ b/src/bridge/vault-fs.ts @@ -27,6 +27,7 @@ import type { VaultTask } from '@zennotes/shared-domain/tasks' import { parseTaskFile, parseTasksFromBody } from '@zennotes/shared-domain/tasks' import { normalizeHarperVaultState } from '@zennotes/shared-domain/harper-settings' import { normalizeNoteComments } from '@zennotes/shared-domain/note-comments' +import { AGENT_EDITS_DIR, agentEditSidecarPath } from '@zennotes/shared-domain/agent-edits' import { isFormDirName, isDatabaseInternalPath } from '@zennotes/shared-domain/databases' import { emptyExcalidrawDocument } from '@zennotes/shared-domain/excalidraw' import { DEMO_TOUR_ASSETS, DEMO_TOUR_NOTES } from '@zennotes/shared-domain/demo-tour-data' @@ -660,6 +661,7 @@ export class MobileVault { if (dir) await this.fs.mkdir(dir) const finalTitle = await this.uniqueTitle(dir, base) const rel = joinPath(dir, `${finalTitle}.md`) + await this.removeOrphanAgentEdit(rel) await this.fs.writeText(rel, `# ${finalTitle}\n\n`) emitVaultChange({ kind: 'add', path: rel, folder, scope: 'content' }) return await this.metaForPath(rel) @@ -673,6 +675,7 @@ export class MobileVault { if (dir) await this.fs.mkdir(dir) const finalTitle = await this.uniqueTitle(dir, base, '.excalidraw') const rel = joinPath(dir, `${finalTitle}.excalidraw`) + await this.removeOrphanAgentEdit(rel) await this.fs.writeText(rel, JSON.stringify(emptyExcalidrawDocument(), null, 2)) emitVaultChange({ kind: 'add', path: rel, folder, scope: 'content' }) return await this.metaForPath(rel) @@ -687,36 +690,48 @@ export class MobileVault { } /** - * Move a note with its comments and its creation date, as desktop's - * note-sidecars.ts `relocateNote` does; a sidecar the note lacks is not - * moved. A creation date already at a destination with no note beside it - * belongs to nobody (its note left outside ZenNotes) and is discarded rather - * than inherited (ZenNotes#839). Leftover comments there are another note's - * discussion: taking it over and deleting it are both wrong, so the move is - * refused. + * Move a note with its comments, its creation date and an assistant's + * unreviewed edit record, as desktop's note-sidecars.ts `relocateNote` does; + * a sidecar the note lacks is not moved. A creation date or an edit record + * already at a destination with no note beside it belongs to nobody (its + * note left outside ZenNotes) and is discarded rather than inherited + * (ZenNotes#839). Leftover comments there are another note's discussion: + * taking it over and deleting it are both wrong, so the move is refused. */ private async relocateNote(oldPath: string, newPath: string): Promise { const comments = this.commentsPathFor(newPath) const metadata = this.metadataPathFor(newPath) + const agentEdit = this.agentEditPathFor(newPath) if (await this.fs.statVerified(newPath) === null) { if (await this.fs.statVerified(comments) !== null) throw new Error(leftoverCommentsMessage(newPath)) if (await this.fs.statVerified(metadata) === 'file') await this.fs.deleteFile(metadata) + if (await this.fs.statVerified(agentEdit) === 'file') await this.fs.deleteFile(agentEdit) } await relocateVaultEntries(this.relocationIO(), [ { from: oldPath, to: newPath, required: true }, { from: this.commentsPathFor(oldPath), to: comments }, - { from: this.metadataPathFor(oldPath), to: metadata } + { from: this.metadataPathFor(oldPath), to: metadata }, + { from: this.agentEditPathFor(oldPath), to: agentEdit } ]) this.invalidateMeta(oldPath) } - /** Detach the content and both sidecar trees before cleanup so a failed move can restore the original. */ - private async detachContent(path: string, comments: string, metadata: string, required = true): Promise { + /** Detach the content and its sidecar trees (comments, creation dates, + * assistant edit records) before cleanup so a failed move can restore the + * original. A deleted note has nothing left to review. */ + private async detachContent( + path: string, + comments: string, + metadata: string, + agentEdits: string, + required = true + ): Promise { const temporary = `${INTERNAL_VAULT_DIR}/delete-${uuid()}` const moves: VaultRelocation[] = [ { from: path, to: `${temporary}/content`, required }, { from: comments, to: `${temporary}/comments` }, - { from: metadata, to: `${temporary}/metadata` } + { from: metadata, to: `${temporary}/metadata` }, + { from: agentEdits, to: `${temporary}/agent-edits` } ] await relocateVaultEntries(this.relocationIO(), moves) try { @@ -830,6 +845,7 @@ export class MobileVault { trashDir, `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${trashDir}`, `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${trashDir}`, + `${INTERNAL_VAULT_DIR}/${AGENT_EDITS_DIR}/${trashDir}`, false ) for (const key of [...this.metaCache.keys()]) { @@ -841,7 +857,7 @@ export class MobileVault { async deleteNote(relPath: string): Promise { const rel = resolveSafeRel(relPath) const folder = (await this.folderOf(rel)) ?? 'trash' - await this.detachContent(rel, this.commentsPathFor(rel), this.metadataPathFor(rel)) + await this.detachContent(rel, this.commentsPathFor(rel), this.metadataPathFor(rel), this.agentEditPathFor(rel)) this.invalidateMeta(rel) emitVaultChange({ kind: 'unlink', path: rel, folder, scope: 'content' }) } @@ -854,6 +870,7 @@ export class MobileVault { const copyTitle = await this.uniqueTitle(dir, `${stemName(rel)} copy`, ext) const destRel = joinPath(dir, `${copyTitle}${ext}`) const body = await this.fs.readText(rel) + await this.removeOrphanAgentEdit(destRel) await this.fs.writeText(destRel, body) const comments = await this.readNoteComments(rel) if (comments.length > 0) { @@ -940,7 +957,9 @@ export class MobileVault { { from: `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${oldRel}`, to: `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${newRel}` }, { from: `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${oldRel}`, - to: `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${newRel}` } + to: `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${newRel}` }, + { from: `${INTERNAL_VAULT_DIR}/${AGENT_EDITS_DIR}/${oldRel}`, + to: `${INTERNAL_VAULT_DIR}/${AGENT_EDITS_DIR}/${newRel}` } ], async () => { try { await this.setVaultSettings({ @@ -975,7 +994,8 @@ export class MobileVault { await this.detachContent( rel, `${INTERNAL_VAULT_DIR}/${NOTE_COMMENTS_DIR}/${rel}`, - `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${rel}` + `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${rel}`, + `${INTERNAL_VAULT_DIR}/${AGENT_EDITS_DIR}/${rel}` ) for (const key of [...this.metaCache.keys()]) { if (key.startsWith(`${rel}/`)) this.invalidateMeta(key) @@ -1016,6 +1036,22 @@ export class MobileVault { return `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${rel}${NOTE_METADATA_SUFFIX}` } + /** `.zennotes/agent-edits/.json`, what the note said before an + * assistant edited it through MCP, kept until the edit is reviewed in the + * desktop app (shared-domain agent-edits). This app does not review edits, + * but the record travels with its note and goes when the note does, so a + * move here leaves the review waiting on the note in the desktop app. */ + private agentEditPathFor(rel: string): string { + return agentEditSidecarPath(INTERNAL_VAULT_DIR, rel) + } + + /** A record with no note beside it belongs to nobody (its note left outside + * ZenNotes); a new note at that path must not inherit its review. */ + private async removeOrphanAgentEdit(rel: string): Promise { + const path = this.agentEditPathFor(rel) + if (await this.fs.statVerified(path) === 'file') await this.fs.deleteFile(path) + } + async readNoteComments(relPath: string): Promise { const rel = resolveSafeRel(relPath) const raw = await this.fs.readTextOrNull(this.commentsPathFor(rel)) diff --git a/src/bridge/vault-lifecycle.test.ts b/src/bridge/vault-lifecycle.test.ts index 45a8961..a3d9342 100644 --- a/src/bridge/vault-lifecycle.test.ts +++ b/src/bridge/vault-lifecycle.test.ts @@ -125,3 +125,52 @@ test('a vault display name written by desktop survives a phone settings save (Ze assert.equal(written.displayName, 'Acme API docs') assert.deepEqual(written.favorites, ['Notes/Work/One.md']) }) + +// Assistant edit records (.zennotes/agent-edits/.json), kept until the +// desktop app reviews them, travel with their note and go when it goes. +const editRecord = (path: string) => `.zennotes/agent-edits/${path}.json` +const editRecords = (fs: MemoryFs) => [...fs.files.keys()].filter(key => key.startsWith('.zennotes/agent-edits/')).sort() + +test('an assistant edit record follows its note through rename, move and Trash, and goes with Empty Trash', async () => { + const s = fixture() + s.fs.files.set(editRecord(s.note), '{"record":"One"}') + // An orphaned record at the destination is nobody's review: the note's own replaces it. + s.fs.files.set(editRecord('Notes/Work/Two.md'), '{"record":"orphan"}') + const renamed = await s.vault.renameNote(s.note, 'Two') + assert.equal(renamed.path, 'Notes/Work/Two.md') + assert.deepEqual(editRecords(s.fs), [editRecord('Notes/Work/Two.md')]) + assert.equal(s.fs.files.get(editRecord('Notes/Work/Two.md')), '{"record":"One"}') + const moved = await s.vault.moveNote(renamed.path, 'inbox', '') + assert.deepEqual(editRecords(s.fs), [editRecord(moved.path)]) + const trashed = await s.vault.moveToTrash(moved.path) + assert.deepEqual(editRecords(s.fs), [editRecord(trashed.path)]) + const restored = await s.vault.restoreFromTrash(trashed.path) + assert.deepEqual(editRecords(s.fs), [editRecord(restored.path)]) + await s.vault.moveToTrash(restored.path) + await s.vault.emptyTrash() + assert.deepEqual(editRecords(s.fs), []) +}) + +test('folder rename carries nested edit records; deleting the note or the folder removes them', async () => { + const s = fixture() + s.fs.files.set(editRecord(s.note), '{"record":"One"}') + await s.vault.renameFolder('inbox', 'Work', 'Moved') + assert.deepEqual(editRecords(s.fs), [editRecord('Notes/Moved/One.md')]) + await s.vault.deleteNote('Notes/Moved/One.md') + assert.deepEqual(editRecords(s.fs), []) + s.fs.files.set('Notes/Moved/Three.md', '# Three\n') + s.fs.files.set(editRecord('Notes/Moved/Three.md'), '{"record":"Three"}') + await s.vault.deleteFolder('inbox', 'Moved') + assert.deepEqual(editRecords(s.fs), []) +}) + +test('a new note does not inherit an orphaned edit record at its path', async () => { + const s = fixture() + s.fs.files.set(editRecord('Notes/Untitled.md'), '{"record":"orphan"}') + s.fs.files.set(editRecord('Notes/Work/One copy.md'), '{"record":"orphan copy"}') + const created = await s.vault.createNote('inbox') + assert.equal(created.path, 'Notes/Untitled.md') + const copy = await s.vault.duplicateNote(s.note) + assert.equal(copy.path, 'Notes/Work/One copy.md') + assert.deepEqual(editRecords(s.fs), []) +}) diff --git a/src/bridge/welcome-note.ts b/src/bridge/welcome-note.ts index 77b91c5..d413050 100644 --- a/src/bridge/welcome-note.ts +++ b/src/bridge/welcome-note.ts @@ -38,7 +38,7 @@ Every note is a plain Markdown file: **bold**, *italic*, lists, and checkboxes a Type \`#\` in any note to tag it, like #ideas on this one. Tags appear under **Browse → Tags** the moment you write them. -Folders work the way you expect: create one in **Browse → New folder**, and move any note with the circle → **More** → **Move to…**. +Folders work the way you expect: create one in **Browse → New folder**, start a note inside the folder you are in with **New note** just above it, and move any note with the circle → **More** → **Move to…**. ## Math that renders live diff --git a/src/ui-mobile/MobileDrawer.tsx b/src/ui-mobile/MobileDrawer.tsx index 3500f34..49629c3 100644 --- a/src/ui-mobile/MobileDrawer.tsx +++ b/src/ui-mobile/MobileDrawer.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import ReactDOM from 'react-dom/client' import { getShellSnapshot, useShellSnapshot, setNoteSortOrder, type NoteSortOrder } from '@zennotes/app-core/shell' import { getBrowseSnapshot, useBrowseSnapshot, getBrowseDirectory, requestCreateBrowseFolder, - requestRenameBrowseFolder, requestRenameBrowseDatabase, requestMoveBrowseDirectory, + requestCreateBrowseNote, requestRenameBrowseFolder, requestRenameBrowseDatabase, requestMoveBrowseDirectory, requestDeleteBrowseDirectory } from '@zennotes/app-core/browse' import { useWorkspaceSnapshot, openLocalVault, pickLocalVault, refreshRemoteProfiles, connectRemoteWorkspace, connectRemoteProfile, changeRemoteVaultPath, deleteRemoteProfile } from '@zennotes/app-core/workspace' @@ -79,6 +79,7 @@ const D = { sort: 'M4 6h16M4 12h10M4 18h5', check: 'M20 6L9 17l-5-5', folderPlus: 'M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7zM12 11v6M9 14h6', + notePlus: 'M14 3H6a2 2 0 00-2 2v14a2 2 0 002 2h12a2 2 0 002-2V9l-6-6zM14 3v6h6M12 12v6M9 15h6', move: 'M5 8V6a2 2 0 012-2h3l2 2h7a2 2 0 012 2v10a2 2 0 01-2 2H7a2 2 0 01-2-2v-4M2 13h9m0 0l-3-3m3 3l-3 3', rename: 'M12 20h9M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z', link: 'M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71', @@ -609,7 +610,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele {view.entry.tier === 'external' && (

- This folder belongs to you — removing it from the list never deletes its + This folder belongs to you: removing it from the list never deletes its files. Rename or move it in your file manager.

)} @@ -657,7 +658,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele

Delete “{view.entry.name}”?{' '} {view.entry.tier === 'icloud' - ? 'Its notes will be removed from iCloud Drive — on every device.' + ? 'Its notes will be removed from iCloud Drive on every device.' : 'All of its notes will be deleted from this device.'}{' '} This cannot be undone.

@@ -683,7 +684,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele <> {backRow}

- “{view.name}” — {view.host} + “{view.name}” ({view.host}) {view.current ? ' · connected' : ''}

@@ -732,7 +733,7 @@ export function VaultsSheet({ onClose }: { onClose: () => void }): React.JSX.Ele

- Removing a remote vault only forgets the saved connection — nothing on the + Removing a remote vault only forgets the saved connection: nothing on the server is touched.

@@ -999,6 +1000,14 @@ function MobileDrawerBody(props: { const newFolderHere = (): void => { void requestCreateBrowseFolder(captureMobileWorkspace(), path).catch(reportActionError) } + // A note made here lands in the folder on screen and opens on its title + // (#108). Before, a note could only start at the top and be moved, and + // "New Note in Current Folder" meant the open note's folder, not this one. + const newNoteHere = (): void => { + const host = captureMobileWorkspace() + const directory = path + go(() => requestCreateBrowseNote(host, directory).catch(reportActionError)) + } // Core keeps the leaf name (a database keeps its .base suffix) and carries // tabs, folder icons, favorites and manual order to the new path. Folder // pins are keyed by subpath and are left alone, as Rename leaves them: an @@ -1238,6 +1247,14 @@ function MobileDrawerBody(props: { childFiles.length === 0 && (
No notes here yet
)} +