Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ archives are vendored under `vendor/zennotes/` with their source identity and
checksums (`manifest.json`), and `package-lock.json` pins the complete install.
A clean checkout installs them with `npm ci`, without a source clone or sibling
repository. 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).
[core-2.69.0-core.h27a2c5df8414d1dd](https://github.com/ZenNotes/zennotes/releases/tag/core-2.69.0-core.h27a2c5df8414d1dd)
(desktop commit `d1344076`, clean tree).

`npm run boundaries:check` verifies the pins, installed versions, singleton
React/CodeMirror peers, and public export usage. `npm run core:adopt -- <core-tag>`
Expand Down
24 changes: 12 additions & 12 deletions ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -604,12 +604,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ZenWidgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.24.0;
MARKETING_VERSION = 1.25.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -733,12 +733,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = WYY7PK57DM;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.24.0;
MARKETING_VERSION = 1.25.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -755,12 +755,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = WYY7PK57DM;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.24.0;
MARKETING_VERSION = 1.25.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
Expand All @@ -775,12 +775,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.24.0;
MARKETING_VERSION = 1.25.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand All @@ -797,12 +797,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ZenWidgets/ZenWidgets.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ZenWidgets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.24.0;
MARKETING_VERSION = 1.25.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ZenWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down Expand Up @@ -835,12 +835,12 @@
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
CURRENT_PROJECT_VERSION = 41;
DEVELOPMENT_TEAM = WYY7PK57DM;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.24.0;
MARKETING_VERSION = 1.25.0;
PRODUCT_BUNDLE_IDENTIFIER = md.zennotes.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
Expand Down
2 changes: 1 addition & 1 deletion ios/App/App/ICloudVaultPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public class ICloudVaultPlugin: CAPPlugin, CAPBridgedPlugin {
}
DispatchQueue.global(qos: .userInitiated).async {
guard let root = self.vaultsRoot() else {
call.reject("iCloud is not available — sign into iCloud and enable iCloud Drive.")
call.reject("iCloud is not available: sign into iCloud and enable iCloud Drive.")
return
}
let localUrl = URL(fileURLWithPath: localPath)
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

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

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
"vscode-oniguruma": "^2.0.1",
"vscode-textmate": "^9.3.2",
"zustand": "^5.0.15",
"@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.68.0-core.h6e3db1e19bc2b662.tgz",
"@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.68.0-boundaries.hd2ce4427b284c18e.tgz",
"@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.68.0-boundaries.hd2ce4427b284c18e.tgz",
"@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.69.0-core.h27a2c5df8414d1dd.tgz",
"@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.69.0-boundaries.h240b8e06d33b0cdc.tgz",
"@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.69.0-boundaries.h240b8e06d33b0cdc.tgz",
"@lezer/common": "^1.5.3"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/bridge/cloud-sync-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024
async function blobToBase64(blob: Blob & { name?: string }): Promise<string> {
if (blob.size > MAX_UPLOAD_BYTES) {
throw new CloudServiceRequestError(
`${blob.name ?? 'An attachment'} is ${Math.round(blob.size / (1024 * 1024))} MB — files over ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB cannot be uploaded from the app yet.`,
`${blob.name ?? 'An attachment'} is ${Math.round(blob.size / (1024 * 1024))} MB. Files over ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB cannot be uploaded from the app yet.`,
413,
'attachment-too-large'
)
Expand Down
2 changes: 1 addition & 1 deletion src/bridge/cloud-sync-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,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.`
)
}
})
Expand Down
64 changes: 50 additions & 14 deletions src/bridge/vault-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
isPathExcludedFromTasks,
normalizeTasksExcludedFolders
Expand Down Expand Up @@ -666,6 +667,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)
Expand All @@ -679,6 +681,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)
Expand All @@ -693,36 +696,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<void> {
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<void> {
/** 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<void> {
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 {
Expand Down Expand Up @@ -836,6 +851,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()]) {
Expand All @@ -847,7 +863,7 @@ export class MobileVault {
async deleteNote(relPath: string): Promise<void> {
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' })
}
Expand All @@ -860,6 +876,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) {
Expand Down Expand Up @@ -946,7 +963,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({
Expand Down Expand Up @@ -981,7 +1000,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)
Expand Down Expand Up @@ -1022,6 +1042,22 @@ export class MobileVault {
return `${INTERNAL_VAULT_DIR}/${NOTE_METADATA_DIR}/${rel}${NOTE_METADATA_SUFFIX}`
}

/** `.zennotes/agent-edits/<rel>.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<void> {
const path = this.agentEditPathFor(rel)
if (await this.fs.statVerified(path) === 'file') await this.fs.deleteFile(path)
}

async readNoteComments(relPath: string): Promise<NoteComment[]> {
const rel = resolveSafeRel(relPath)
const raw = await this.fs.readTextOrNull(this.commentsPathFor(rel))
Expand Down
Loading
Loading