Skip to content

feat(notifications): scheduled tasks and desktop notifications#3

Closed
kooksee wants to merge 26 commits into
mainfrom
feat/notifications
Closed

feat(notifications): scheduled tasks and desktop notifications#3
kooksee wants to merge 26 commits into
mainfrom
feat/notifications

Conversation

@kooksee

@kooksee kooksee commented Jun 15, 2026

Copy link
Copy Markdown

Summary

  • Add scheduled tasks: create timed checks against workspace pages, with optional conditions (single rule, AND group, or no filter for pure reminders).
  • Add in-app notification inbox with read/delete, relative timestamps, and click-through to matched pages or scheduled task settings.
  • Wire desktop system notifications with click-to-navigate, in-app toast fallback when focused, and ad-hoc signing for local macOS builds.

Test plan

  • Create a pure reminder task (no page filter), Run now → in-app notification + macOS system notification (desktop, non-silent)
  • Create a conditional task (field equals/contains, date due, AND rules) → notifies only when pages match
  • Run now on same-day pure reminder → still notifies (forceNotify bypasses daily dedup)
  • Scheduled auto-run → pure reminder deduped to once per local day
  • Delete notification from sidebar
  • Click system notification → app focuses and navigates to page or scheduled tasks
  • CI passes

Made with Cursor

kooksee and others added 14 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>

@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 comprehensive scheduled tasks and notifications system, Markdown import/export capabilities, and Mermaid diagram rendering support. It adds database tables and services for scheduled tasks and notifications, Electron IPC plumbing for desktop notifications, and Tiptap editor extensions for Markdown and Mermaid. The review feedback highlights several key improvement opportunities, including resolving a timezone mismatch when parsing date-only strings, utilizing js-yaml for robust frontmatter serialization, preventing crashes from corrupted page attributes, avoiding duplicate focused notifications, ensuring proper macOS window focusing, allowing empty page exports, removing redundant observers and queries, and handling invalid date strings gracefully.

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 +52 to +68
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

Parsing date-only strings (like "YYYY-MM-DD") with new Date(raw) treats them as UTC midnight, whereas new Date() (reference) is created in local time. This timezone mismatch causes tasks to be considered due a day early in negative timezone offsets (e.g., US timezones). Parsing "YYYY-MM-DD" explicitly as a local date fixes this issue.

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;
  }

  if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
    const parts = raw.split('-');
    const date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
    return Number.isNaN(date.getTime()) ? null : date;
  }

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

  return date;
};

Comment on lines +1 to +96
import { Editor } from '@tiptap/core';

import { buildEditorContent } from '@colanode/client/lib/editor';
import { LocalPageNode } from '@colanode/client/types';
import {
type FieldAttributes,
type FieldValue,
type RichTextContent,
} from '@colanode/core';
import { getDocumentMarkdownExtensions } from '@colanode/ui/lib/document-markdown-extensions';
import { normalizeExportedMarkdown } from '@colanode/ui/lib/markdown-export-normalize';

export interface PageMarkdownExportInput {
page: LocalPageNode;
content: RichTextContent | null | undefined;
metaTypeName?: string | null;
fieldDefinitions?: Record<string, FieldAttributes>;
}

const sanitizeFileName = (name: string): string => {
const trimmed = name.trim() || 'Untitled';
return trimmed.replace(/[\\/:*?"<>|]/g, '-');
};

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;
};

const fieldValueToYaml = (value: FieldValue): string | number | boolean | string[] | null => {
switch (value.type) {
case 'boolean':
case 'number':
return value.value;
case 'string':
case 'text':
return value.value;
case 'string_array':
return value.value.length > 0 ? value.value : null;
default:
return null;
}
};

export const buildPageFrontmatter = ({
page,
metaTypeName,
fieldDefinitions = {},
}: Omit<PageMarkdownExportInput, 'content'>): string => {
const lines: string[] = ['---'];

lines.push(`title: ${yamlScalar(page.name)}`);

if (metaTypeName) {
lines.push(`metaType: ${yamlScalar(metaTypeName)}`);
}

lines.push(`colanode:`);
lines.push(` pageId: ${yamlScalar(page.id)}`);
lines.push(` metaTypeId: ${yamlScalar(page.metaTypeId)}`);

const fields = page.fields ?? {};
for (const [fieldId, fieldValue] of Object.entries(fields)) {
const definition = fieldDefinitions[fieldId];
const key = definition?.name?.trim() || fieldId;
const yamlValue = fieldValueToYaml(fieldValue);
if (yamlValue === null || yamlValue === '') {
continue;
}

if (Array.isArray(yamlValue)) {
lines.push(`${key}:`);
for (const item of yamlValue) {
lines.push(` - ${yamlScalar(item)}`);
}
continue;
}

lines.push(`${key}: ${yamlScalar(yamlValue)}`);
}

lines.push('---', '');
return lines.join('\n');
};

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

Using manual string concatenation and a custom yamlScalar function is prone to syntax errors when page titles or field values contain special YAML characters (like [, ], {, }, -, etc.). Since js-yaml is already a dependency of the project, using yaml.dump is 100% robust, handles all escaping and edge cases perfectly, and simplifies the code significantly.

import { Editor } from '@tiptap/core';
import yaml from 'js-yaml';

import { buildEditorContent } from '@colanode/client/lib/editor';
import { LocalPageNode } from '@colanode/client/types';
import {
  type FieldAttributes,
  type FieldValue,
  type RichTextContent,
} from '@colanode/core';
import { getDocumentMarkdownExtensions } from '@colanode/ui/lib/document-markdown-extensions';
import { normalizeExportedMarkdown } from '@colanode/ui/lib/markdown-export-normalize';

export interface PageMarkdownExportInput {
  page: LocalPageNode;
  content: RichTextContent | null | undefined;
  metaTypeName?: string | null;
  fieldDefinitions?: Record<string, FieldAttributes>;
}

const sanitizeFileName = (name: string): string => {
  const trimmed = name.trim() || 'Untitled';
  return trimmed.replace(/[\\/:*?"<>|]/g, '-');
};

const fieldValueToYaml = (value: FieldValue): string | number | boolean | string[] | null => {
  switch (value.type) {
    case 'boolean':
    case 'number':
      return value.value;
    case 'string':
    case 'text':
      return value.value;
    case 'string_array':
      return value.value.length > 0 ? value.value : null;
    default: 
      return null;
  }
};

export const buildPageFrontmatter = ({
  page,
  metaTypeName,
  fieldDefinitions = {},
}: Omit<PageMarkdownExportInput, 'content'>): string => {
  const data: Record<string, any> = {
    title: page.name,
  };

  if (metaTypeName) {
    data.metaType = metaTypeName;
  }

  data.colanode = {
    pageId: page.id,
    metaTypeId: page.metaTypeId,
  };

  const fields = page.fields ?? {};
  for (const [fieldId, fieldValue] of Object.entries(fields)) {
    const definition = fieldDefinitions[fieldId];
    const key = definition?.name?.trim() || fieldId;
    const yamlValue = fieldValueToYaml(fieldValue);
    if (yamlValue === null || yamlValue === '') {
      continue;
    }

    data[key] = yamlValue;
  }

  return `---\n${yaml.dump(data)}---\n`;
};

Comment on lines +246 to +255
for (const row of rows) {
const attributes = JSON.parse(row.attributes) as PageAttributes;
const operator = check.operator ?? 'equals';
if (!pageMatchesFieldValue(attributes, check.fieldId, check.value, operator)) {
continue;
}

matchedPageIds.push(row.id);
matchedPageNames.push(attributes.name);
}

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

If a single page has corrupted or missing attributes, JSON.parse(row.attributes) will throw an unhandled exception, failing the entire scheduled task. Wrapping the parsing and matching logic in a try-catch block ensures that a single corrupted page doesn't prevent other valid pages from being checked.

  for (const row of rows) {
    try {
      const attributes = JSON.parse(row.attributes) as PageAttributes;
      const operator = check.operator ?? 'equals';
      if (!pageMatchesFieldValue(attributes, check.fieldId, check.value, operator)) {
        continue;
      }

      matchedPageIds.push(row.id);
      matchedPageNames.push(attributes.name);
    } catch (error) {
      console.error(`Failed to parse attributes for page ${row.id}:`, error);
    }
  }

Comment on lines +62 to +67
desktopNotification.show();

const mainWindow = this.getMainWindow();
if (mainWindow?.isFocused()) {
this.sendFallbackToast(notification);
}

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

Currently, if the main window is focused, the app shows both the system notification and the in-app fallback toast. To avoid duplicate notifications and provide a better user experience, the system notification should be suppressed when the app is already focused.

Suggested change
desktopNotification.show();
const mainWindow = this.getMainWindow();
if (mainWindow?.isFocused()) {
this.sendFallbackToast(notification);
}
const mainWindow = this.getMainWindow();
if (mainWindow?.isFocused()) {
this.sendFallbackToast(notification);
} else {
desktopNotification.show();
}

Comment on lines +98 to +114
private focusMainWindow(): BrowserWindow | null {
const mainWindow = this.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
return null;
}

if (mainWindow.isMinimized()) {
mainWindow.restore();
}

if (!mainWindow.isVisible()) {
mainWindow.show();
}

mainWindow.focus();
return mainWindow;
}

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

On macOS, calling mainWindow.focus() alone might not bring the application to the foreground if another app currently has focus. Calling app.focus({ steal: true }) ensures that the application is successfully brought to the front when the user clicks on a notification.

  private focusMainWindow(): BrowserWindow | null {
    const mainWindow = this.getMainWindow();
    if (!mainWindow || mainWindow.isDestroyed()) {
      return null;
    }

    const { app } = require('electron');
    app.focus({ steal: true });

    if (mainWindow.isMinimized()) {
      mainWindow.restore();
    }

    if (!mainWindow.isVisible()) {
      mainWindow.show();
    }

    mainWindow.focus();
    return mainWindow;
  }

Comment on lines +53 to +56
if (!content) {
toast.error('Could not load page content for export');
return;
}

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

If a page is newly created and has no content yet, fetchDocumentRichTextContent returns null. Currently, this triggers an error toast and aborts the export. Since buildPageMarkdown and serializeRichTextToMarkdown are already designed to handle null content, we should allow exporting empty pages by passing content ?? null instead of aborting.

      const markdown = buildPageMarkdown({
        page,
        content: content ?? null,
        metaTypeName: metaType?.name,
        fieldDefinitions: metaType?.fields,
      });

      downloadMarkdownFile(exportPageMarkdownFileName(page.name), markdown);

Comment on lines +53 to +102
const { refs, floatingStyles, update } = useFloating({
placement: 'left',
middleware: [offset(-10), shift()],
strategy: 'fixed',
});

useEffect(() => {
if (menuState.rect) {
refs.setPositionReference({
getBoundingClientRect: () => menuState.rect!,
contextElement: menuState.domNode!,
if (!menuState.show || !menuState.domNode || !editor) {
return;
}

const domNode = menuState.domNode;
const editorDom = editor.view.dom;
const virtualReference = createMenuReference(domNode, editorDom);

refs.setPositionReference(virtualReference);

let cleanup: (() => void) | undefined;
let frame = 0;

const startAutoUpdate = () => {
const floatingEl = refs.floating.current;
if (!floatingEl) {
return false;
}

cleanup = autoUpdate(virtualReference, floatingEl, update, {
ancestorScroll: true,
ancestorResize: true,
elementResize: true,
layoutShift: true,
});
return true;
};

if (!startAutoUpdate()) {
frame = requestAnimationFrame(() => {
startAutoUpdate();
});
}
}, [menuState.rect, menuState.domNode]);

return () => {
if (frame) {
cancelAnimationFrame(frame);
}
cleanup?.();
};
// refs/update are managed by useFloating and must not be effect deps.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [menuState.show, menuState.domNode, editor]);

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

Instead of manually managing autoUpdate with useEffect, requestAnimationFrame, and custom cleanup logic, you can use @floating-ui/react's built-in whileElementsMounted: autoUpdate option. This is the standard, idiomatic way to handle floating element updates and completely eliminates the complex manual synchronization logic.

  const { refs, floatingStyles } = useFloating({
    placement: 'left',
    middleware: [offset(-10), shift()],
    strategy: 'fixed',
    whileElementsMounted: autoUpdate,
  });

  useEffect(() => {
    if (!menuState.show || !menuState.domNode || !editor) {
      return;
    }

    const domNode = menuState.domNode;
    const editorDom = editor.view.dom;
    const virtualReference = createMenuReference(domNode, editorDom);

    refs.setPositionReference(virtualReference);
  }, [menuState.show, menuState.domNode, editor]);

Comment on lines +55 to +74
useEffect(() => {
const observer = new MutationObserver(() => {
const element = containerRef.current;
if (!element) {
return;
}

const width = element.clientWidth;
if (width > 0) {
setContainerWidth(width);
}
});

observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style'],
});

return () => observer.disconnect();
}, []);

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

This MutationObserver is redundant because the ResizeObserver in the first useEffect already detects any size changes of the container (including those caused by theme changes). If the theme change does not affect the container's width, updating containerWidth state is unnecessary. You can safely remove this entire useEffect block.

Comment on lines +32 to +97
const [fetchedMetaType, setFetchedMetaType] =
useState<LocalDatabaseNode | null>(null);

const metaTypeNodeQuery = useLiveQuery(
(q) =>
q
.from({ nodes: workspace.collections.nodes })
.where(({ nodes }) => eq(nodes.id, metaTypeId))
.findOne(),
[metaTypeId, workspace.userId]
);

useEffect(() => {
if (!metaTypeId) {
setFetchedMetaType(null);
return;
}

let cancelled = false;

window.colanode
.executeQuery({
type: 'node.list',
userId: workspace.userId,
filters: [
{
field: ['id'],
operator: 'eq',
value: metaTypeId,
},
],
sorts: [],
})
.then((nodes) => {
if (cancelled) {
return;
}

const node = nodes[0];
if (node?.type === 'database') {
setFetchedMetaType(node as LocalDatabaseNode);
}
})
.catch(() => {
if (!cancelled) {
setFetchedMetaType(null);
}
});

return () => {
cancelled = true;
};
}, [metaTypeId, workspace.userId]);

return useMemo(() => {
const liveNode =
metaTypeNodeQuery.data?.type === 'database'
? (metaTypeNodeQuery.data as LocalDatabaseNode)
: null;

return pickRicherMetaType(
fetchedMetaType,
liveNode,
metaTypes.find((metaType) => metaType.id === metaTypeId) ?? null
);
}, [fetchedMetaType, metaTypeNodeQuery.data, metaTypes, metaTypeId]);

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

Since useLiveQuery is already querying the node reactively from the local database, the manual useEffect with window.colanode.executeQuery is redundant and causes duplicate database queries on every metaTypeId change. You can completely remove the fetchedMetaType state and the useEffect block, and rely solely on metaTypeNodeQuery.data.

  const metaTypeNodeQuery = useLiveQuery(
    (q) =>
      q
        .from({ nodes: workspace.collections.nodes })
        .where(({ nodes }) => eq(nodes.id, metaTypeId))
        .findOne(),
    [metaTypeId, workspace.userId]
  );

  return useMemo(() => {
    const liveNode =
      metaTypeNodeQuery.data?.type === 'database'
        ? (metaTypeNodeQuery.data as LocalDatabaseNode)
        : null;

    return pickRicherMetaType(
      liveNode,
      metaTypes.find((metaType) => metaType.id === metaTypeId) ?? null
    );
  }, [metaTypeNodeQuery.data, metaTypes, metaTypeId]);

Comment on lines +15 to +45
const formatTimestamp = (value: string): string => {
try {
const date = new Date(value);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffSec = Math.floor(diffMs / 1000);

if (diffSec < 60) {
return 'Just now';
}

const diffMin = Math.floor(diffSec / 60);
if (diffMin < 60) {
return `${diffMin}m ago`;
}

const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) {
return `${diffHour}h ago`;
}

return date.toLocaleString([], {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
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

If value is an invalid date string, new Date(value) returns an invalid date, but it does not throw an error. The subsequent calculations result in NaN, and date.toLocaleString() returns "Invalid Date" to the user. Checking Number.isNaN(date.getTime()) early ensures we gracefully fall back to the raw string value.

const formatTimestamp = (value: string): string => {
  try {
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) {
      return value;
    }

    const now = new Date();
    const diffMs = now.getTime() - date.getTime();
    const diffSec = Math.floor(diffMs / 1000);

    if (diffSec < 60) {
      return 'Just now';
    }

    const diffMin = Math.floor(diffSec / 60);
    if (diffMin < 60) {
      return `${diffMin}m ago`;
    }

    const diffHour = Math.floor(diffMin / 60);
    if (diffHour < 24) {
      return `${diffHour}h ago`;
    }

    return date.toLocaleString([], {
      month: 'numeric',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
    });
  } catch {
    return value;
  }
};

kooksee and others added 12 commits June 15, 2026 16:58
…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>
@kooksee kooksee closed this Jun 22, 2026
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