Skip to content

Notifications, scheduled tasks, global Assets library, and workspace enhancements#4

Merged
kooksee merged 31 commits into
mainfrom
feat/notifications_node
Jun 22, 2026
Merged

Notifications, scheduled tasks, global Assets library, and workspace enhancements#4
kooksee merged 31 commits into
mainfrom
feat/notifications_node

Conversation

@kooksee

@kooksee kooksee commented Jun 22, 2026

Copy link
Copy Markdown

Summary

  • Scheduled Tasks & notifications: record-backed system type, task state, desktop notification fallback/navigation, reminder UX on pages/records
  • Global Assets library: system-level asset_library database (like Scheduled Tasks); folders are filter tags; Settings → Assets with Gallery/Table/List/By folder; migration to meta-types space; collection-safe updates for on-demand node sync
  • Archive: archive/unarchive nodes; hide archived items from sidebar, command palette, and meta-type views
  • Backup: delete local backups from settings panel
  • Docs & editor: Markdown page import/export round-trip, Mermaid in code blocks, meta-type property templates
  • Desktop packaging: faster prePackage (symlink hoisted deps), better-sqlite3 upgrade, ad-hoc macOS signing for notifications

Test plan

  • Desktop LOCAL_ONLY=true npm run package on Node 22; smoke-test app launch
  • Settings → Scheduled Tasks: create/edit/run/delete task; verify notifications
  • Settings → Assets: four views; Gallery thumbnails; no Assets in normal Space sidebar
  • Create Folder → upload/import → asset appears in global Assets and folder filter view
  • Archive a page → hidden from sidebar and meta-type views; restore works
  • Backup panel: delete a backup file
  • Command palette: "Open global Assets" quick action; Assets DB not in resource search
  • npm run test in packages/client and packages/ui

Made with Cursor

kooksee and others added 30 commits June 11, 2026 17:14
Use beautiful-mermaid with markview-style layout scaling, browse/edit source modes, and roadmap docs for upcoming Markdown import/export.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add frontmatter serialization for page meta and fields, wire Export Markdown in page settings, and expose client subpath exports needed by headless editor tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Parse YAML frontmatter into meta type and fields, convert body via Tiptap markdown, and create the page document in one flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
…d normalizing output.

Add a function to prepend the page title as a heading in the exported Markdown body. Normalize the serialized Markdown output to ensure consistent formatting. Update related tests to verify new functionality.
…ore.

Strip exported title headings on import, map colanode links back to resource and block nodes, and add round-trip tests plus roadmap updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ment features

Add new database tables for notifications and scheduled tasks, along with corresponding mutation and query handlers. Integrate notification service into the app and enhance the UI to support notification settings and scheduled task management. Update sidebar to include notifications and scheduled tasks sections, ensuring a cohesive user experience.

Co-authored-by: Cursor <cursoragent@cursor.com>
Refactor scheduled task checks to improve field matching by introducing new helper functions for resolving field definitions and handling date fields. Update the ScheduledTaskConditionEditor to utilize a more streamlined approach for managing condition fields, including better handling of virtual fields and improved user feedback for unsupported properties.

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

Refactor scheduled task checks to introduce a new 'none' type for reminders and improve the handling of group checks. Update the ScheduledTaskConditionEditor and related components to support the new structure, ensuring better user experience and condition management. Introduce utility functions for formatting condition previews and validating task inputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Allow deleting notifications from the sidebar, show relative timestamps,
navigate reminder notifications to scheduled tasks, and dedupe pure
reminders to once per local day with clearer body text.

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

Refactor the DesktopNotificationService to manage notification clicks and navigate to specific targets within the app. Introduce a new method for focusing the main window and expose a callback for notification navigation in the preload script. Update the UI to utilize the new notification navigation feature.

Co-authored-by: Cursor <cursoragent@cursor.com>
Enhance the task execution process by introducing a `forceNotify` option in the `WorkspaceTaskRunInput` type. Update the `executeTask` method to utilize this option, allowing for more flexible notification handling. Modify the UI to reflect changes in success messaging for task execution.
Add a new method to the DesktopNotificationService for sending fallback notifications when desktop notifications fail. Expose an `onNotificationFallback` callback in the preload script and integrate it into the UI components to enhance user experience with in-app notifications. Update the scheduled task form to clarify notification behavior for desktop users.
LOCAL_ONLY packages are now ad-hoc signed after packaging so Electron
can show system notifications during local testing.

Co-authored-by: Cursor <cursoragent@cursor.com>
…k checks

Introduce a new `context` field in the ScheduledTaskTable to store contextual information. Update the mapping logic in `mapScheduledTask` to parse and handle the context. Enhance scheduled task checks by adding new record leaf check types and utility functions for better condition handling. Update UI components to support context-aware reminders and improve validation for scheduled task inputs.
…enhance task handling

Add a new `scheduled_task_state` table to manage the state of scheduled tasks, including fields for tracking the last run time and result hash. Update the `ScheduledTaskService` to handle state operations, including upserting and deleting task states. Enhance the mapping logic to incorporate the new state information and improve task loading. Additionally, introduce a `ScheduledTaskScope` type for better context handling in scheduled tasks, ensuring a more robust and flexible task management system.
Use a ref-forwarding value slot for select and multi-select fields so popover triggers work again, and provide consistent clickable empty-state space in page/record properties.

Co-authored-by: Cursor <cursoragent@cursor.com>
…field details

Enhance documentation for page properties, clarifying editable fields and schema edit limitations. Introduce details about the new 'Reminders' system field, its functionality, and its integration with scheduled tasks. Update roadmap to reflect current capabilities and future enhancements related to notifications and task management.
Drop workspace-level tags, tag routes, and node tag UI so classification lives fully in meta-type properties and database fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add one-click property templates for meta types, align reminders with readonly rendering in non-table views, and fix empty-value/date/collaborator plus modal-focus interactions in properties.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add archiving capabilities to database, folder, and page components, allowing users to archive and restore nodes. Introduce UI elements for toggling archive state and update sidebar items to reflect archived status. Enhance query handlers to filter out archived nodes from mention searches.

Co-authored-by: Cursor <cursoragent@cursor.com>
Exclude archived pages from meta-type database views, relation search,
scheduled task checks, and command palette lookup via a shared helper.

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

Add a new feature to allow users to import files from a selected directory into the application. This includes the creation of temporary files with appropriate MIME types and relative paths. Update the UI to support the import dialog and enhance the folder creation process to accommodate asset libraries.

Co-authored-by: Cursor <cursoragent@cursor.com>
…support and improved database handling

- Introduced a new folder field in the asset library to organize assets.
- Updated the asset library bundle to include a folder view and associated fields.
- Enhanced the folder creation process to ensure proper integration with the asset library.
- Implemented functions to ensure folder options are available in the asset library database.
- Updated UI components to reflect changes in asset management and improve user experience.

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

- Added new views (table and list) to the asset library bundle for enhanced asset organization.
- Updated the asset library bundle return structure to include gallery, table, and list views.
- Modified the folder creation process to ensure proper database integration without requiring a root ID.
- Enhanced the folder container component to support dynamic layout switching between gallery, list, and table views.
- Implemented legacy file backfilling functionality to migrate existing files into the asset library structure.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy only main-process native deps during prePackage instead of the full monorepo node_modules, limit electron-rebuild to better-sqlite3, and bump better-sqlite3 for Node 26 support.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Added a new method to the BackupService for deleting backups, ensuring proper management of backup files and associated metadata.
- Introduced IPC handlers for backup deletion in both desktop and web applications, allowing users to delete backups through the UI.
- Updated the BackupPanel component to include a delete option, with confirmation dialogs to prevent accidental deletions.
- Enhanced unit tests to cover the new deleteBackup functionality, ensuring robustness and reliability.

Co-authored-by: Cursor <cursoragent@cursor.com>
…se integration

- Introduced a new function to build missing asset library views, ensuring all necessary layouts are created dynamically.
- Updated the asset library bundle to utilize the new view fields and ensure proper integration of the asset library purpose.
- Enhanced the database container to ensure asset library databases are correctly initialized and updated with the appropriate views.
- Refactored existing code to improve clarity and maintainability, including the use of helper functions for view management.

Co-authored-by: Cursor <cursoragent@cursor.com>
…idebar navigation

- Added functionality to ensure asset library databases are relocated to a dedicated meta space for better organization.
- Introduced a new helper function to identify asset library system databases and redirect users accordingly.
- Updated sidebar components to include links to asset library settings, improving user navigation.
- Refactored existing code to streamline asset library view management and enhance overall maintainability.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Refactored the ensureFolderFieldOption function to use the database object directly instead of the database ID, improving clarity and consistency.
- Updated multiple instances of ensureFolderFieldOption calls across the asset library to align with the new function signature.
- Introduced a new utility function, updateNodeInCollection, to streamline node updates in the asset library, enhancing maintainability and reducing code duplication.

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

Add implementation docs, collection-update tests, global Assets shortcuts in the command palette and folder toolbar, and update the roadmap.

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

@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 several major features, including a workspace-level global Asset Library, a robust Scheduled Tasks and Notifications system, Markdown import/export capabilities with frontmatter support, Mermaid diagram rendering in code blocks, and node archiving. It also adds backup deletion functionality and cleans up legacy tag-syncing code. The code review identified several critical issues: potential ReferenceErrors in forge.config.ts and field-create-popover.tsx due to missing variables or imports; performance and memory risks in main.ts and asset-library.ts from in-memory file reading and un-filtered node queries; a timezone bug in task date parsing; a data integrity risk during backup deletion; potential data loss when toggling scheduled tasks; a YAML serialization bug with reserved keywords; and a localization inconsistency in the backup panel.

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.

},
postPackage: async () => {
postPackage: async (_forgeConfig, packageResult) => {
if (isLocalOnly && process.platform === 'darwin') {

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

ReferenceError Risk

isLocalOnly is used here but it does not appear to be defined or imported in this file. This will cause a ReferenceError during the post-package step. Use process.env.LOCAL_ONLY === 'true' directly or define isLocalOnly at the top of the file.

Suggested change
if (isLocalOnly && process.platform === 'darwin') {
if (process.env.LOCAL_ONLY === 'true' && process.platform === 'darwin') {

Comment thread apps/desktop/src/main.ts
Comment on lines +812 to +826
for (const entry of entries) {
try {
const buffer = await fs.promises.readFile(entry.absolutePath);
const mimeType = guessMimeTypeFromName(entry.name);
const tempFile = await createTempFileFromBuffer(app, {
buffer,
name: entry.name,
mimeType,
size: buffer.byteLength,
});

imported.push({
...tempFile,
relativePath: entry.relativePath,
});

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

Performance & Memory Optimization

Reading the entire file into memory using fs.promises.readFile is highly inefficient and can cause out-of-memory (OOM) crashes for large files. Since the files are already on the local disk, you should copy them directly using fs.promises.copyFile instead of loading their contents into memory.

  for (const entry of entries) {
    try {
      const stat = await fs.promises.stat(entry.absolutePath);
      const mimeType = guessMimeTypeFromName(entry.name);
      const id = generateId(IdType.TempFile);
      const extension = app.path.extension(entry.name);
      const subtype = extractFileSubtype(mimeType);
      const filePath = app.path.tempFile(id + extension);

      await fs.promises.copyFile(entry.absolutePath, filePath);
      await app.database
        .insertInto('temp_files')
        .values({
          id,
          name: entry.name,
          size: stat.size,
          mime_type: mimeType,
          subtype,
          path: filePath,
          extension,
          created_at: new Date().toISOString(),
          opened_at: new Date().toISOString(),
        })
        .execute();

      const url = await app.fs.url(filePath);
      if (!url) {
        await app.fs.delete(filePath);
        await app.database
          .deleteFrom('temp_files')
          .where('id', '=', id)
          .execute();
        throw new Error('Failed to save temp file');
      }

      imported.push({
        id,
        name: entry.name,
        size: stat.size,
        mimeType,
        subtype,
        path: filePath,
        extension,
        url,
        relativePath: entry.relativePath,
      });
    } catch (error) {

Comment on lines +55 to +71
const parseFieldDate = (fieldValue: unknown): Date | null => {
if (!fieldValue || typeof fieldValue !== 'object' || !('value' in fieldValue)) {
return null;
}

const raw = (fieldValue as { value: unknown }).value;
if (typeof raw !== 'string' || raw.length === 0) {
return null;
}

const date = new Date(raw);
if (Number.isNaN(date.getTime())) {
return null;
}

return date;
};

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

Timezone Bug

Parsing date-only strings (like 'YYYY-MM-DD') with new Date(raw) parses them as UTC midnight. Comparing this UTC date with a local-time-based endOfToday will cause reminders to trigger a day early or late depending on the user's local timezone offset. For example, a reminder for June 15th might trigger on the evening of June 14th in western timezones. Parse date-only strings in local time (e.g., by replacing hyphens with slashes: new Date(raw.replace(/-/g, '/'))) to avoid timezone shifts.

const parseFieldDate = (fieldValue: unknown): Date | null => {
  if (!fieldValue || typeof fieldValue !== 'object' || !('value' in fieldValue)) {
    return null;
  }

  const raw = (fieldValue as { value: unknown }).value;
  if (typeof raw !== 'string' || raw.length === 0) {
    return null;
  }

  const dateString = /^\d{4}-\d{2}-\d{2}$/.test(raw)
    ? raw.replace(/-/g, '/')
    : raw;

  const date = new Date(dateString);
  if (Number.isNaN(date.getTime())) {
    return null;
  }

  return date;
};

Comment on lines +438 to +448
for (const key of otherManifestKeys) {
try {
const data = await this.provider.download(key);
const otherManifest = JSON.parse(data.toString()) as BackupManifest;
for (const file of otherManifest.files) {
hashesStillNeeded.add(file.sha256);
}
} catch {
debug(`Failed to read manifest while deleting backup: ${key}`);
}
}

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

Data Integrity Risk

If downloading or parsing any of the other manifests fails (e.g., due to a transient network error), the catch block silently ignores it. This means hashesStillNeeded will be incomplete, and you may permanently delete shared blobs that are still referenced by those other backups, corrupting them. You should abort the deletion or avoid deleting blobs if any other manifest cannot be successfully read.

        for (const key of otherManifestKeys) {
            const data = await this.provider.download(key);
            const otherManifest = JSON.parse(data.toString()) as BackupManifest;
            for (const file of otherManifest.files) {
                hashesStillNeeded.add(file.sha256);
            }
        }

Comment on lines +150 to +153
const maxIndex = Object.values(options)
.map((option) => option.index)
.sort((a, b) => -compareString(a, b))[0];
const index = generateFractionalIndex(maxIndex, 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.

high

ReferenceError Risk

compareString is used here but it is not imported from @colanode/core in this file. This will cause a runtime ReferenceError. You can import compareString from @colanode/core, or simply use the native localeCompare method (e.g., b.index.localeCompare(a.index)) which is standard and requires no imports.

Suggested change
const maxIndex = Object.values(options)
.map((option) => option.index)
.sort((a, b) => -compareString(a, b))[0];
const index = generateFractionalIndex(maxIndex, null);
const maxIndex = Object.values(options)
.map((option) => option.index)
.sort((a, b) => b.localeCompare(a))[0];
const index = generateFractionalIndex(maxIndex ?? null, null);

Comment on lines +124 to +130
task: {
name: task.name,
enabled,
schedule: task.schedule,
check: task.check,
notify: task.notify,
},

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

Correctness / Data Loss

In handleToggleEnabled, the scope and context properties of the task are omitted when calling the update mutation. This will cause the task's scope and context to be cleared or incorrectly re-derived, potentially breaking the reminder's target association. Please pass scope: task.scope and context: task.context to preserve them.

        task: {
          name: task.name,
          enabled,
          schedule: task.schedule,
          check: task.check,
          notify: task.notify,
          scope: task.scope,
          context: task.context,
        },

Comment on lines +573 to +576
<AlertDialogTitle>确认删除备份</AlertDialogTitle>
<AlertDialogDescription>
将永久删除该备份,无法恢复。其他备份中仍被引用的文件块会保留以节省空间。
</AlertDialogDescription>

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

i18n Consistency

The new backup deletion dialog and toast messages are written in Chinese, whereas the rest of the file and settings panel (like 'Restore', 'Delete backup', 'Scheduled Tasks') are in English. To maintain consistency, please translate these strings to English or use the project's i18n localization system if available.

Suggested change
<AlertDialogTitle>确认删除备份</AlertDialogTitle>
<AlertDialogDescription>
将永久删除该备份,无法恢复。其他备份中仍被引用的文件块会保留以节省空间。
</AlertDialogDescription>
<AlertDialogTitle>Confirm Backup Deletion</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete the backup. It cannot be undone. Shared file blocks referenced by other backups will be kept to save space.
</AlertDialogDescription>

Comment on lines +25 to +40
const yamlScalar = (value: string | number | boolean): string => {
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}

if (
value === '' ||
/[:#\n\r]/.test(value) ||
value.startsWith(' ') ||
value.endsWith(' ')
) {
return JSON.stringify(value);
}

return value;
};

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

YAML Serialization Bug

In yamlScalar, string values that look like booleans ("true", `

const yamlScalar = (value: string | number | boolean): string => {
  if (typeof value === 'number' || typeof value === 'boolean') {
    return String(value);
  }

  const lower = value.toLowerCase();
  const isReservedKeyword =
    lower === 'true' ||
    lower === 'false' ||
    lower === 'yes' ||
    lower === 'no' ||
    lower === 'on' ||
    lower === 'off' ||
    lower === 'null';

  const isNumeric = /^-?\d+(\.\d+)?$/.test(value);

  if (
    value === '' ||
    isReservedKeyword ||
    isNumeric ||
    /[:#\n\r]/.test(value) ||
    value.startsWith(' ') ||
    value.endsWith(' ')
  ) {
    return JSON.stringify(value);
  }

  return value;
};

Comment on lines +221 to +232
const nodes = await window.colanode.executeQuery({
type: 'node.list',
userId: workspace.userId,
filters: [
{
field: ['type'],
operator: 'in',
value: ['database_view', 'record', 'file'],
},
],
sorts: [],
});

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

Performance & Scalability

relocateAssetLibraryToMetaSpace fetches all database_view, record, and file nodes in the entire workspace and filters them in memory. For workspaces with a large number of files or records, this will be extremely slow and consume significant memory. Consider querying only the nodes associated with the target database by applying filters (e.g., filtering by parentId or databaseId) directly in the node.list query.

@kooksee kooksee merged commit 1d011a9 into main Jun 22, 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