Skip to content

Feat/sync local#2

Merged
kooksee merged 84 commits into
mainfrom
feat/sync_local
Jun 11, 2026
Merged

Feat/sync local#2
kooksee merged 84 commits into
mainfrom
feat/sync_local

Conversation

@kooksee

@kooksee kooksee commented Jun 8, 2026

Copy link
Copy Markdown

No description provided.

kooksee added 30 commits June 2, 2026 17:10
…3 support

- Added BackupService to handle backup and restore operations.
- Introduced GitHubStorageProvider and S3StorageProvider for storage options.
- Implemented IPC handlers for creating backups, listing backups, and restoring from backups.
- Created types for backup manifest and progress reporting.
- Added unit tests for BackupService to ensure functionality and integrity.
- Updated package.json and service index to include new backup services.
…etter inline styling

feat(emojis): update emoji elements to use span for improved layout consistency
feat(mentions): enhance mention renderer and node view with navigation support and targetType handling
…ality

- Implemented inline message editing with a new MessageEditInline component.
- Added pinning and unpinning of messages with visual indicators in MessageActions and Message.
- Introduced a MessageForwardDialog for selecting destinations to forward messages.
- Enhanced message context to include canEdit, canPin, and canForward properties.
- Updated message list to show jump-to-message options when search filters are applied.
- Created a PinnedMessagesPopover to display and navigate pinned messages.
- Added search highlighting in text rendering for better visibility of search terms.
- Implemented database migrations for document texts and new mutation handlers for message updates, pinning, and forwarding.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a peer-to-peer 'Local Sync' mechanism for local-only workspaces, a backup and restore utility with S3, GitHub, and local storage providers, and several message-related features like pinning, forwarding, and inline editing. It also recreates the FTS5 search tables as regular tables to fix search and join queries. The review feedback highlights several critical issues: an asynchronous mutation loop in file uploading that will set empty file IDs, a potential null pointer dereference on the account object in the user service, and a test failure in the backup service due to loading better-sqlite3 in a plain Node environment. Additionally, it is recommended to return null instead of 0 when aggregating empty rollup values to avoid misleading results, and to translate several hardcoded Chinese UI strings in the backup panel to English for consistency.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +103 to +125
const newIds: string[] = [];
for (const tempFile of result.files) {
await mutate({
input: {
type: 'file.create',
userId: workspace.userId,
parentId: record.id,
tempFileId: tempFile.id,
},
onSuccess: (output) => {
if (output.id) {
newIds.push(output.id);
}
},
onError: (error) => {
toast.error(error.message);
},
});
}

if (newIds.length > 0) {
setValue({ type: 'string_array', value: [...fileIds, ...newIds] });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The mutate function returned by useMutation is typically asynchronous and fire-and-forget (returning void). Using await mutate inside the loop will not block execution, and the onSuccess callback will run asynchronously in a future task. As a result, setValue on line 124 will be called with an empty newIds array before any files are actually created, breaking the file upload functionality.\n\nPlease use mutateAsync (if supported by your custom hook) to sequentially await the file creation promises, or wrap the mutation in a Promise to ensure setValue is called with the fully populated array of file IDs.

        const newIds: string[] = [];\n        try {\n            for (const tempFile of result.files) {\n                const output = await mutateAsync({\n                    input: {\n                        type: 'file.create',\n                        userId: workspace.userId,\n                        parentId: record.id,\n                        tempFileId: tempFile.id,\n                    },\n                });\n                if (output?.id) {\n                    newIds.push(output.id);\n                }\n            }\n\n            if (newIds.length > 0) {\n                setValue({ type: 'string_array', value: [...fileIds, ...newIds] });\n            }\n        } catch (error) {\n            toast.error(error instanceof Error ? error.message : 'Failed to upload files');\n        }

Comment on lines +31 to +32
const account = this.workspace.account.account;
const now = new Date().toISOString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The account object retrieved from this.workspace.account.account can be null if the account is not yet loaded or in a transition state. Accessing account.email directly without a null check will cause a runtime crash (TypeError: Cannot read properties of null).\n\nPlease add a defensive guard to ensure account is defined before proceeding.

    const account = this.workspace.account.account;\n    if (!account) {\n        return;\n    }\n\n    const now = new Date().toISOString();

Comment on lines +93 to +94
// Mock checkpoint to avoid requiring better-sqlite3
vi.spyOn(service, 'checkpoint').mockResolvedValue(undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The checkpoint method has been deprecated and turned into a no-op in BackupService. The database snapshotting logic is now handled by snapshotDatabases, which dynamically imports better-sqlite3. Since better-sqlite3 is compiled against the Electron ABI, it cannot be loaded in a plain Node test environment (like Vitest), causing the test to fail.\n\nPlease mock snapshotDatabases instead of checkpoint to prevent loading better-sqlite3 during tests.

Suggested change
// Mock checkpoint to avoid requiring better-sqlite3
vi.spyOn(service, 'checkpoint').mockResolvedValue(undefined);
// Mock snapshotDatabases to avoid requiring better-sqlite3 in tests\n vi.spyOn(service as any, 'snapshotDatabases').mockResolvedValue({\n tmpDir: '',\n snapshots: new Map(),\n });

Comment on lines +79 to +81
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning 0 when there are no numbers to aggregate is misleading for operations like average, min, or max (where 0 is a valid numerical result, but "no values" should be represented as empty/null). Returning null instead will correctly trigger the empty fallback on line 106 and render an empty space.

        if (numbers.length === 0) {\n            return null;\n        }

Comment on lines +90 to +107
if (providerType === 'local') {
if (!localFields.path) {
toast.error('请填写备份目录路径');
return null;
}
return { type: 'local', path: localFields.path };
}
if (providerType === 'github') {
if (!githubFields.token || !githubFields.owner || !githubFields.repo) {
toast.error('请填写所有 GitHub 字段');
return null;
}
return { type: 'github', ...githubFields };
}
if (!s3Fields.accessKeyId || !s3Fields.secretAccessKey || !s3Fields.bucket) {
toast.error('请填写必填的 S3 字段');
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The user-facing toast messages in this file are hardcoded in Chinese, whereas the rest of the application's UI and local sync settings are in English. Please translate these messages to English to maintain consistency across the user interface.

        if (providerType === 'local') {\n            if (!localFields.path) {\n                toast.error('Please enter the backup directory path');\n                return null;\n            }\n            return { type: 'local', path: localFields.path };\n        }\n        if (providerType === 'github') {\n            if (!githubFields.token || !githubFields.owner || !githubFields.repo) {\n                toast.error('Please fill in all GitHub fields');\n                return null;\n            }\n            return { type: 'github', ...githubFields };\n        }\n        if (!s3Fields.accessKeyId || !s3Fields.secretAccessKey || !s3Fields.bucket) {\n            toast.error('Please fill in the required S3 fields');\n            return null;\n        }

Comment on lines +434 to +453
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认恢复</AlertDialogTitle>
<AlertDialogDescription>
恢复将覆盖当前本地数据,并在完成后自动重启应用。此操作不可撤销。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (restoreTarget) {
handleRestore(restoreTarget);
}
}}
>
确认恢复
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The text in the restore confirmation dialog is hardcoded in Chinese, which is inconsistent with the rest of the English UI. Please translate these strings to English.

                <AlertDialogContent>\n                    <AlertDialogHeader>\n                        <AlertDialogTitle>Confirm Restore</AlertDialogTitle>\n                        <AlertDialogDescription>\n                            Restoring will overwrite your current local data and restart the application. This action cannot be undone.\n                        </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                        <AlertDialogCancel>Cancel</AlertDialogCancel>\n                        <AlertDialogAction\n                            onClick={() => {\n                                if (restoreTarget) {\n                                    handleRestore(restoreTarget);\n                                }\n                            }}\n                        >\n                            Confirm Restore\n                        </AlertDialogAction>\n                    </AlertDialogFooter>\n                </AlertDialogContent>

kooksee and others added 23 commits June 8, 2026 18:25
- 新增 resourceLink schema node + Suggestion 扩展
- 输入 [[ 触发候选搜索(复用 mention.search,排除 user/group)
- 选中后插入带 target id/type/label 的 inline node,渲染为可点击方框
- ⌘/Ctrl/Shift + 点击或中键 在新 tab 打开目标
- 缺失目标降级为虚线灰框
- 序列化保留 data-target-* 便于内部复制粘贴,外部退化为 [[Title]] 文本
- 扩展 collectBlockMentions 识别 resourceLink 叶子,沿用 node_references 表
- 新增 node.backlinks.get query/handler:按 reference_id 聚合来源节点 + 父节点信息
- 在 PageContainer 底部展示 'Linked from' 列表,支持 ⌘/Ctrl/Shift + 点击或中键在新窗口打开
- 提取 document.get 中的 heading1/2/3 块构建目录
- xl 屏幕在右侧固定展示,点击平滑滚动到对应 heading
- IntersectionObserver 高亮当前可见标题
- ToggleNode: 可折叠块,含 NodeView (编辑) + Renderer (只读)
- /resource link: 删除 / 并插入 [[ 触发资源链接弹出
- /today: 插入 YYYY-MM-DD 当日日期
- ToggleSummaryNode (inline*) + ToggleBodyNode (block+)
- 标题始终可见,正文随 chevron 折叠
- 占位符 'Toggle title' 引导输入
- 移除 toggleSummary/toggleBody 子节点(与已有数据不兼容会触发 contentMatchAt 异常)
- toggle = block+,setToggle 默认插入两个 paragraph (标题 + 正文)
- NodeView 通过 CSS 隐藏首段之后的子节点实现折叠
- 旧版 toggle 设计将 text 节点持久化为独立 block,加载时触发 ProseMirror 'Invalid text node'
- mapBlocksToContents 跳过 type=text 的 block
- 将 toggleSummary / toggleBody 拆包到父 toggle 下,保留原有内容
- Tailwind JIT 对 [&>*:not(:first-child)]:hidden 编译不可靠
- 改为 editor.css 中 [data-open=false] > .toggle-content > *:not(:first-child) { display:none }
- Tiptap v3 的 NodeViewContent 会再套一层 [data-node-view-content-react]
- 之前选择器少了一层,:not(:first-child) 匹配不到段落导致折叠无效
…ig persistence

Store shared tags on workspaces, let pages assign tagIds, index tag relations in node_references, and add a tagged-nodes view. Also persist Backup & Restore settings across restarts.

Co-authored-by: Cursor <cursoragent@cursor.com>
…lete cleanup

Support tags on channel, folder, and database; add #tag search in the command palette; remove tagIds and node_references when a workspace tag is deleted.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the outdated Space-level planning guide with the current implementation map and maintenance notes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kooksee kooksee merged commit c7e873e into main Jun 11, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant