From 0d0cda68bf8e21fb0583206039fef1308258f444 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 22:09:41 +0300 Subject: [PATCH 01/80] fix: resolve real project ID before creating onboarding task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The task creation step passed a hardcoded 'personal' projectId which is not a real DB record — causes silent FK violation on every first run. Now resolves a valid project: uses first existing project, or creates a default 'Personal' project if none exist. Co-Authored-By: Paperclip --- .../src/components/first-run-onboarding.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/src/components/first-run-onboarding.tsx b/apps/desktop/src/renderer/src/components/first-run-onboarding.tsx index 3a517fc3e..77e9aad32 100644 --- a/apps/desktop/src/renderer/src/components/first-run-onboarding.tsx +++ b/apps/desktop/src/renderer/src/components/first-run-onboarding.tsx @@ -51,7 +51,18 @@ export function FirstRunOnboarding({ onComplete }: FirstRunOnboardingProps): Rea const title = taskTitle.trim() || 'My first task' setIsSubmitting(true) try { - await tasksService.create({ projectId: 'personal', title }) + // Resolve a real project ID — use the first existing project, or create a default one + const listResult = await tasksService.listProjects() + let projectId: string + if (listResult.projects.length > 0) { + projectId = listResult.projects[0].id + } else { + const created = await tasksService.createProject({ name: 'Personal', color: '#6366f1' }) + if (!created.success || !created.project) + throw new Error('Failed to create default project') + projectId = created.project.id + } + await tasksService.create({ projectId, title }) } catch (err) { log.warn('Failed to create onboarding task:', err) } finally { From 628e21f393df777f34b163eb1d92aaca4fc36c5a Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 22:16:02 +0300 Subject: [PATCH 02/80] feat(settings): add shortcuts section and expand editor settings UI - Add ShortcutRegistry with all rebindable app shortcuts, conflict detection, and display helpers (formatBinding, findConflicts, getGroupedShortcuts) - Add ShortcutsSettings section with search, grouped list, inline key capture, conflict warnings, per-shortcut reset, and reset-all button - Expand EditorSettings section to expose width, spellCheck, autoSaveDelay, and showWordCount controls (migrates from useNoteEditorSettings to the richer useEditorSettings hook) - Wire Shortcuts nav item into settings.tsx sidebar - Regenerate IPC invoke map (was stale) Co-Authored-By: Paperclip --- .../src/main/ipc/generated-ipc-invoke-map.ts | 3028 +++++++++++++++-- .../src/renderer/src/lib/shortcut-registry.ts | 258 ++ .../src/renderer/src/pages/settings.tsx | 12 +- .../src/pages/settings/editor-section.tsx | 156 +- .../src/pages/settings/shortcuts-section.tsx | 312 ++ 5 files changed, 3449 insertions(+), 317 deletions(-) create mode 100644 apps/desktop/src/renderer/src/lib/shortcut-registry.ts create mode 100644 apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx diff --git a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts index 861fd65b6..35fa8dd61 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -2,295 +2,2745 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export interface MainIpcInvokeHandlers { - "ai-inline:get-server-port": (...args: []) => Awaited - "ai-inline:get-settings": (...args: []) => Awaited - "ai-inline:set-settings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "ai-inline:start-server": (...args: []) => Awaited> - "ai-inline:stop-server": (...args: []) => Awaited> - "auth:init-oauth": (...args: [{ provider: "google"; }]) => Awaited> - "auth:refresh-token": (...args: []) => Awaited> - "auth:request-otp": (...args: [{ email: string; }]) => Awaited> - "auth:resend-otp": (...args: [{ email: string; }]) => Awaited> - "auth:verify-otp": (...args: [{ email: string; code: string; }]) => Awaited> - "bookmarks:bulk-create": (...args: [{ items: { itemType: string; itemId: string; }[]; }]) => Awaited> - "bookmarks:bulk-delete": (...args: [{ bookmarkIds: string[]; }]) => Awaited> - "bookmarks:create": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "bookmarks:delete": (...args: [string]) => Awaited> - "bookmarks:get": (...args: [string]) => Awaited> - "bookmarks:get-by-item": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "bookmarks:is-bookmarked": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "bookmarks:list": (...args: [{ itemType?: string | undefined; sortBy?: "createdAt" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "bookmarks:list-by-type": (...args: [string]) => Awaited> - "bookmarks:reorder": (...args: [{ bookmarkIds: string[]; }]) => Awaited> - "bookmarks:toggle": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "context-menu:show": (...args: [{ id: string; label: string; accelerator?: string | undefined; disabled?: boolean | undefined; type?: "normal" | "separator" | undefined; }[]]) => Awaited> - "crdt:apply-update": (...args: [unknown]) => Awaited> - "crdt:close-doc": (...args: [unknown]) => Awaited> - "crdt:open-doc": (...args: [unknown]) => Awaited> - "crdt:sync-step-1": (...args: [{ noteId: string; stateVector: number[]; }]) => Awaited> - "crdt:sync-step-2": (...args: [{ noteId: string; diff: number[]; }]) => Awaited> - "crypto:decrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> - "crypto:encrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; content: Record; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> - "crypto:get-rotation-progress": (...args: []) => Awaited - "crypto:rotate-keys": (...args: [{ confirm: boolean; }]) => Awaited> - "crypto:verify-signature": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> - "folder-view:delete-view": (...args: [{ folderPath: string; viewName: string; }]) => Awaited> - "folder-view:folder-exists": (...args: [string]) => Awaited - "folder-view:get-available-properties": (...args: [{ folderPath: string; }]) => Awaited> - "folder-view:get-config": (...args: [{ folderPath: string; }]) => Awaited> - "folder-view:get-folder-suggestions": (...args: [{ noteId: string; }]) => Awaited> - "folder-view:get-views": (...args: [{ folderPath: string; }]) => Awaited> - "folder-view:list-with-properties": (...args: [{ folderPath: string; properties?: string[] | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "folder-view:set-config": (...args: [{ folderPath: string; config: { path?: string | undefined; template?: string | undefined; inherit?: boolean | undefined; formulas?: Record | undefined; properties?: Record | undefined; summaries?: Record | undefined; views?: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }[] | undefined; }; }]) => Awaited> - "folder-view:set-view": (...args: [{ folderPath: string; view: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }; }]) => Awaited> - "graph:get-graph-data": (...args: []) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> - "graph:get-local-graph": (...args: [{ noteId: string; depth?: number | undefined; }]) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> - "inbox:add-tag": (...args: [any, any]) => Awaited> - "inbox:archive": (...args: [any]) => Awaited> - "inbox:bulk-archive": (...args: [any]) => Awaited> - "inbox:bulk-archive-older-than": (...args: [any]) => Awaited> - "inbox:bulk-file": (...args: [any]) => Awaited> - "inbox:bulk-snooze": (...args: [any]) => Awaited> - "inbox:bulk-tag": (...args: [any]) => Awaited> - "inbox:capture-clip": (...args: [any]) => Awaited> - "inbox:capture-image": (...args: [any]) => Awaited> - "inbox:capture-link": (...args: [any]) => Awaited> - "inbox:capture-pdf": (...args: [any]) => Awaited> - "inbox:capture-text": (...args: [any]) => Awaited> - "inbox:capture-voice": (...args: [any]) => Awaited> - "inbox:convert-to-note": (...args: [any]) => Awaited> - "inbox:convert-to-task": (...args: [any]) => Awaited> - "inbox:delete-permanent": (...args: [any]) => Awaited> - "inbox:file": (...args: [any]) => Awaited> - "inbox:file-all-stale": (...args: []) => Awaited> - "inbox:get": (...args: [any]) => Awaited> - "inbox:get-filing-history": (...args: [any]) => Awaited> - "inbox:get-patterns": (...args: []) => Awaited> - "inbox:get-snoozed": (...args: []) => Awaited> - "inbox:get-stale-threshold": (...args: []) => Awaited> - "inbox:get-stats": (...args: []) => Awaited> - "inbox:get-suggestions": (...args: [any]) => Awaited> - "inbox:get-tags": (...args: []) => Awaited> - "inbox:link-to-note": (...args: [any, any, any]) => Awaited> - "inbox:list": (...args: [any]) => Awaited> - "inbox:list-archived": (...args: [any]) => Awaited> - "inbox:mark-viewed": (...args: [any]) => Awaited> - "inbox:remove-tag": (...args: [any, any]) => Awaited> - "inbox:retry-metadata": (...args: [any]) => Awaited> - "inbox:retry-transcription": (...args: [any]) => Awaited> - "inbox:set-stale-threshold": (...args: [any]) => Awaited> - "inbox:snooze": (...args: [any]) => Awaited> - "inbox:track-suggestion": (...args: [any, any, any, any, any, any, any]) => Awaited> - "inbox:unarchive": (...args: [any]) => Awaited> - "inbox:undo-archive": (...args: [any]) => Awaited> - "inbox:undo-file": (...args: [any]) => Awaited> - "inbox:unsnooze": (...args: [any]) => Awaited> - "inbox:update": (...args: [any]) => Awaited> - "journal:createEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> - "journal:deleteEntry": (...args: [{ date: string; }]) => Awaited> - "journal:getAllTags": (...args: []) => Awaited> - "journal:getDayContext": (...args: [{ date: string; }]) => Awaited> - "journal:getEntry": (...args: [{ date: string; }]) => Awaited | undefined; } | null>> - "journal:getHeatmap": (...args: [{ year: number; }]) => Awaited> - "journal:getMonthEntries": (...args: [{ year: number; month: number; }]) => Awaited> - "journal:getStreak": (...args: []) => Awaited> - "journal:getYearStats": (...args: [{ year: number; }]) => Awaited> - "journal:updateEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> - "notes:create": (...args: [{ title: string; content?: string | undefined; folder?: string | undefined; tags?: string[] | undefined; template?: string | undefined; }]) => Awaited> - "notes:create-folder": (...args: [string]) => Awaited> - "notes:create-property-definition": (...args: [{ name: string; type: "number" | "date" | "text" | "checkbox" | "url"; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> - "notes:delete": (...args: [string]) => Awaited> - "notes:delete-attachment": (...args: [{ noteId: string; filename: string; }]) => Awaited> - "notes:delete-folder": (...args: [string]) => Awaited> - "notes:delete-version": (...args: [string]) => Awaited> - "notes:exists": (...args: [string]) => Awaited> - "notes:export-html": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> - "notes:export-pdf": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> - "notes:get": (...args: [string]) => Awaited> - "notes:get-all-positions": (...args: []) => Awaited; error?: undefined; } | { success: boolean; positions: {}; error: string; }>> - "notes:get-by-path": (...args: [string]) => Awaited> - "notes:get-file": (...args: [string]) => Awaited> - "notes:get-folder-config": (...args: [string]) => Awaited> - "notes:get-folder-template": (...args: [string]) => Awaited> - "notes:get-folders": (...args: []) => Awaited> - "notes:get-links": (...args: [string]) => Awaited> - "notes:get-local-only-count": (...args: []) => Awaited> - "notes:get-positions": (...args: [{ folderPath: string; }]) => Awaited> - "notes:get-property-definitions": (...args: []) => Awaited> - "notes:get-tags": (...args: []) => Awaited> - "notes:get-version": (...args: [string]) => Awaited> - "notes:get-versions": (...args: [string]) => Awaited> - "notes:import-files": (...args: [{ sourcePaths: string[]; targetFolder?: string | undefined; }]) => Awaited> - "notes:list": (...args: [{ folder?: string | undefined; tags?: string[] | undefined; sortBy?: "title" | "modified" | "created" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "notes:list-attachments": (...args: [string]) => Awaited> - "notes:move": (...args: [{ id: string; newFolder: string; }]) => Awaited> - "notes:open-external": (...args: [string]) => Awaited> - "notes:rename": (...args: [{ id: string; newTitle: string; }]) => Awaited> - "notes:rename-folder": (...args: [{ oldPath: string; newPath: string; }]) => Awaited> - "notes:reorder": (...args: [{ folderPath: string; notePaths: string[]; }]) => Awaited> - "notes:resolve-by-title": (...args: [string]) => Awaited> - "notes:restore-version": (...args: [string]) => Awaited> - "notes:reveal-in-finder": (...args: [string]) => Awaited> - "notes:set-folder-config": (...args: [{ folderPath: string; config: { template?: string | undefined; inherit?: boolean | undefined; }; }]) => Awaited> - "notes:set-local-only": (...args: [{ id: string; localOnly: boolean; }]) => Awaited> - "notes:show-import-dialog": (...args: []) => Awaited> - "notes:update": (...args: [{ id: string; title?: string | undefined; content?: string | undefined; tags?: string[] | undefined; frontmatter?: Record | undefined; emoji?: string | null | undefined; }]) => Awaited> - "notes:update-property-definition": (...args: [{ name: string; type?: "number" | "date" | "text" | "checkbox" | "url" | undefined; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> - "notes:upload-attachment": (...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer; }]) => Awaited> - "properties:get": (...args: [{ entityId: string; }]) => Awaited> - "properties:rename": (...args: [{ entityId: string; oldName: string; newName: string; }]) => Awaited> - "properties:set": (...args: [{ entityId: string; properties: Record; }]) => Awaited> - "quick-capture:get-clipboard": (...args: []) => Awaited - "reminder:bulk-dismiss": (...args: [{ reminderIds: string[]; }]) => Awaited> - "reminder:count-pending": (...args: []) => Awaited> - "reminder:create": (...args: [{ targetType: "note"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "journal"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "highlight"; targetId: string; highlightText: string; highlightStart: number; highlightEnd: number; remindAt: string; title?: string | undefined; note?: string | undefined; }]) => Awaited> - "reminder:delete": (...args: [string]) => Awaited> - "reminder:dismiss": (...args: [string]) => Awaited> - "reminder:get": (...args: [string]) => Awaited> - "reminder:get-due": (...args: []) => Awaited> - "reminder:get-for-target": (...args: [{ targetType: "note" | "journal" | "highlight"; targetId: string; }]) => Awaited> - "reminder:get-upcoming": (...args: [number | undefined]) => Awaited> - "reminder:list": (...args: [{ targetType?: "note" | "journal" | "highlight" | undefined; targetId?: string | undefined; status?: "pending" | "triggered" | "dismissed" | "snoozed" | ("pending" | "triggered" | "dismissed" | "snoozed")[] | undefined; fromDate?: string | undefined; toDate?: string | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "reminder:snooze": (...args: [{ id: string; snoozeUntil: string; }]) => Awaited> - "reminder:update": (...args: [{ id: string; remindAt?: string | undefined; title?: string | null | undefined; note?: string | null | undefined; }]) => Awaited> - "saved-filters:create": (...args: [{ name: string; config: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; }; }]) => Awaited> - "saved-filters:delete": (...args: [{ id: string; }]) => Awaited> - "saved-filters:list": (...args: []) => Awaited> - "saved-filters:reorder": (...args: [{ ids: string[]; positions: number[]; }]) => Awaited> - "saved-filters:update": (...args: [{ id: string; name?: string | undefined; config?: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; } | undefined; position?: number | undefined; }]) => Awaited> - "search:add-reason": (...args: [{ itemId: string; itemType: "note" | "task" | "journal" | "inbox"; itemTitle: string; searchQuery: string; itemIcon?: string | null | undefined; }]) => Awaited> - "search:clear-reasons": (...args: []) => Awaited> - "search:get-all-tags": (...args: []) => Awaited> - "search:get-reasons": (...args: []) => Awaited> - "search:get-stats": (...args: []) => Awaited> - "search:query": (...args: [{ text: string; types?: ("note" | "task" | "journal" | "inbox")[] | undefined; tags?: string[] | undefined; dateRange?: { from: string; to: string; } | null | undefined; projectId?: string | null | undefined; folderPath?: string | null | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "search:quick": (...args: [string]) => Awaited> - "search:rebuild-index": (...args: []) => Awaited> - "settings:get": (...args: [string]) => Awaited - "settings:getAIModelStatus": (...args: []) => Awaited> - "settings:getAISettings": (...args: []) => Awaited - "settings:getBackupSettings": (...args: []) => Awaited<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }> - "settings:getEditorSettings": (...args: []) => Awaited<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }> - "settings:getGeneralSettings": (...args: []) => Awaited<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }> - "settings:getGraphSettings": (...args: []) => Awaited<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }> - "settings:getJournalSettings": (...args: []) => Awaited<{ defaultTemplate: string | null; showSchedule: boolean; showTasks: boolean; showAIConnections: boolean; showStatsFooter: boolean; }> - "settings:getKeyboardSettings": (...args: []) => Awaited<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }> - "settings:getNoteEditorSettings": (...args: []) => Awaited - "settings:getSyncSettings": (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean; }> - "settings:getTabSettings": (...args: []) => Awaited - "settings:getTaskSettings": (...args: []) => Awaited<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }> - "settings:loadAIModel": (...args: []) => Awaited> - "settings:reindexEmbeddings": (...args: []) => Awaited> - "settings:resetKeyboardSettings": (...args: []) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:set": (...args: [{ key: string; value: string; }]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setAISettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setBackupSettings": (...args: [Partial<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setEditorSettings": (...args: [Partial<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setGeneralSettings": (...args: [Partial<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setGraphSettings": (...args: [Partial<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setJournalSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setKeyboardSettings": (...args: [Partial<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setNoteEditorSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setSyncSettings": (...args: [Partial<{ enabled: boolean; autoSync: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setTabSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setTaskSettings": (...args: [Partial<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "sync:approve-linking": (...args: [{ sessionId: string; }]) => Awaited> - "sync:check-device-status": (...args: []) => Awaited> - "sync:complete-linking-qr": (...args: [{ sessionId: string; }]) => Awaited> - "sync:confirm-recovery-phrase": (...args: [{ confirmed: boolean; }]) => Awaited> - "sync:download-attachment": (...args: [{ attachmentId: string; targetPath?: string | undefined; }]) => Awaited> - "sync:emergency-wipe": (...args: []) => Awaited> - "sync:generate-linking-qr": (...args: []) => Awaited> - "sync:get-devices": (...args: []) => Awaited> - "sync:get-download-progress": (...args: [{ attachmentId: string; }]) => Awaited> - "sync:get-history": (...args: [{ limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "sync:get-linking-sas": (...args: [{ sessionId: string; }]) => Awaited> - "sync:get-quarantined-items": (...args: []) => Awaited - "sync:get-queue-size": (...args: []) => Awaited<{ pending: number; failed: number; }> - "sync:get-recovery-phrase": (...args: []) => Awaited - "sync:get-status": (...args: []) => Awaited - "sync:get-storage-breakdown": (...args: []) => Awaited> - "sync:get-synced-settings": (...args: []) => Awaited<{ general?: { theme?: "light" | "dark" | "white" | "system" | undefined; fontSize?: "small" | "medium" | "large" | undefined; fontFamily?: "system" | "serif" | "sans-serif" | "monospace" | undefined; accentColor?: string | undefined; startOnBoot?: boolean | undefined; language?: string | undefined; } | undefined; editor?: { width?: "medium" | "narrow" | "wide" | undefined; spellCheck?: boolean | undefined; autoSaveDelay?: number | undefined; showWordCount?: boolean | undefined; toolbarMode?: "floating" | "sticky" | undefined; } | undefined; tasks?: { defaultProjectId?: string | null | undefined; defaultSortOrder?: "createdAt" | "priority" | "dueDate" | "manual" | undefined; weekStartDay?: "sunday" | "monday" | undefined; staleInboxDays?: number | undefined; showCompleted?: boolean | undefined; sortBy?: string | undefined; } | undefined; keyboard?: { overrides?: Record | undefined; } | undefined; notes?: { defaultFolder?: string | undefined; editorFontSize?: number | undefined; spellCheck?: boolean | undefined; } | undefined; sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined; } | undefined; } | null> - "sync:get-upload-progress": (...args: [{ sessionId: string; }]) => Awaited> - "sync:link-via-qr": (...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined; }]) => Awaited> - "sync:link-via-recovery": (...args: [{ recoveryPhrase: string; }]) => Awaited> - "sync:logout": (...args: []) => Awaited> - "sync:pause": (...args: []) => Awaited<{ success: boolean; wasPaused: boolean; }> - "sync:remove-device": (...args: [{ deviceId: string; }]) => Awaited> - "sync:rename-device": (...args: [{ deviceId: string; newName: string; }]) => Awaited> - "sync:resume": (...args: []) => Awaited<{ success: boolean; pendingCount: number; }> - "sync:setup-first-device": (...args: [{ oauthToken: string; provider: "google"; state: string; }]) => Awaited> - "sync:setup-new-account": (...args: []) => Awaited> - "sync:trigger-sync": (...args: []) => Awaited> - "sync:update-synced-setting": (...args: [unknown]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "sync:upload-attachment": (...args: [{ noteId: string; filePath: string; }]) => Awaited> - "tags:delete": (...args: [string]) => Awaited> - "tags:get-all-with-counts": (...args: []) => Awaited> - "tags:get-notes-by-tag": (...args: [{ tag: string; sortBy?: "title" | "modified" | "created" | undefined; sortOrder?: "asc" | "desc" | undefined; }]) => Awaited> - "tags:merge": (...args: [{ source: string; target: string; }]) => Awaited> - "tags:pin-note-to-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> - "tags:remove-from-note": (...args: [{ noteId: string; tag: string; }]) => Awaited> - "tags:rename": (...args: [{ oldName: string; newName: string; }]) => Awaited> - "tags:unpin-note-from-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> - "tags:update-color": (...args: [{ tag: string; color: string; }]) => Awaited> - "tasks:archive": (...args: [string]) => Awaited> - "tasks:bulk-archive": (...args: [{ ids: string[]; }]) => Awaited> - "tasks:bulk-complete": (...args: [{ ids: string[]; }]) => Awaited> - "tasks:bulk-delete": (...args: [{ ids: string[]; }]) => Awaited> - "tasks:bulk-move": (...args: [{ ids: string[]; projectId: string; }]) => Awaited> - "tasks:complete": (...args: [{ id: string; completedAt?: string | undefined; }]) => Awaited> - "tasks:convert-to-subtask": (...args: [{ taskId: string; parentId: string; }]) => Awaited> - "tasks:convert-to-task": (...args: [string]) => Awaited> - "tasks:create": (...args: [{ projectId: string; title: string; description?: string | null | undefined; priority?: number | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; sourceNoteId?: string | null | undefined; position?: number | undefined; }]) => Awaited> - "tasks:delete": (...args: [string]) => Awaited> - "tasks:duplicate": (...args: [string]) => Awaited> - "tasks:get": (...args: [string]) => Awaited> - "tasks:get-linked-tasks": (...args: [string]) => Awaited> - "tasks:get-overdue": (...args: []) => Awaited> - "tasks:get-stats": (...args: []) => Awaited> - "tasks:get-subtasks": (...args: [string]) => Awaited> - "tasks:get-tags": (...args: []) => Awaited> - "tasks:get-today": (...args: []) => Awaited> - "tasks:get-upcoming": (...args: [{ days?: number | undefined; }]) => Awaited> - "tasks:list": (...args: [{ projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; includeCompleted?: boolean | undefined; includeArchived?: boolean | undefined; dueBefore?: string | undefined; dueAfter?: string | undefined; tags?: string[] | undefined; search?: string | undefined; sortBy?: "modified" | "created" | "position" | "priority" | "dueDate" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "tasks:move": (...args: [{ taskId: string; position: number; targetProjectId?: string | undefined; targetStatusId?: string | null | undefined; targetParentId?: string | null | undefined; }]) => Awaited> - "tasks:project-archive": (...args: [string]) => Awaited> - "tasks:project-create": (...args: [{ name: string; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; color?: string | undefined; }[] | undefined; }]) => Awaited> - "tasks:project-delete": (...args: [string]) => Awaited> - "tasks:project-get": (...args: [string]) => Awaited> - "tasks:project-list": (...args: []) => Awaited> - "tasks:project-reorder": (...args: [{ projectIds: string[]; positions: number[]; }]) => Awaited> - "tasks:project-update": (...args: [{ id: string; name?: string | undefined; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; id?: string | undefined; color?: string | undefined; }[] | undefined; }]) => Awaited> - "tasks:reorder": (...args: [{ taskIds: string[]; positions: number[]; }]) => Awaited> - "tasks:seed-demo": (...args: []) => Awaited> - "tasks:seed-performance-test": (...args: []) => Awaited> - "tasks:status-create": (...args: [{ projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined; }]) => Awaited> - "tasks:status-delete": (...args: [string]) => Awaited> - "tasks:status-list": (...args: [string]) => Awaited> - "tasks:status-reorder": (...args: [{ statusIds: string[]; positions: number[]; }]) => Awaited> - "tasks:status-update": (...args: [{ id: string; name?: string | undefined; color?: string | undefined; position?: number | undefined; isDefault?: boolean | undefined; isDone?: boolean | undefined; }]) => Awaited> - "tasks:unarchive": (...args: [string]) => Awaited> - "tasks:uncomplete": (...args: [string]) => Awaited> - "tasks:update": (...args: [{ id: string; title?: string | undefined; description?: string | null | undefined; priority?: number | undefined; projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; }]) => Awaited> - "templates:create": (...args: [{ name: string; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> - "templates:delete": (...args: [string]) => Awaited> - "templates:duplicate": (...args: [{ id: string; newName: string; }]) => Awaited> - "templates:get": (...args: [string]) => Awaited> - "templates:list": (...args: []) => Awaited> - "templates:update": (...args: [{ id: string; name?: string | undefined; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> - "vault:close": (...args: []) => Awaited> - "vault:get-all": (...args: []) => Awaited> - "vault:get-config": (...args: []) => Awaited> - "vault:get-status": (...args: []) => Awaited> - "vault:reindex": (...args: []) => Awaited> - "vault:remove": (...args: [string]) => Awaited> - "vault:select": (...args: [{ path?: string | undefined; }]) => Awaited> - "vault:switch": (...args: [string]) => Awaited> - "vault:update-config": (...args: [{ excludePatterns?: string[] | undefined; defaultNoteFolder?: string | undefined; journalFolder?: string | undefined; attachmentsFolder?: string | undefined; }]) => Awaited> + 'ai-inline:get-server-port': (...args: []) => Awaited + 'ai-inline:get-settings': ( + ...args: [] + ) => Awaited + 'ai-inline:set-settings': ( + ...args: [ + Partial + ] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'ai-inline:start-server': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; error: string; port?: undefined } + | { success: boolean; port: number; error?: undefined } + > + > + 'ai-inline:stop-server': (...args: []) => Awaited> + 'auth:init-oauth': (...args: [{ provider: 'google' }]) => Awaited> + 'auth:refresh-token': ( + ...args: [] + ) => Awaited> + 'auth:request-otp': (...args: [{ email: string }]) => Awaited> + 'auth:resend-otp': (...args: [{ email: string }]) => Awaited> + 'auth:verify-otp': ( + ...args: [{ email: string; code: string }] + ) => Awaited< + Promise<{ + success: boolean + isNewUser: boolean + needsSetup: boolean + needsRecoveryInput: boolean + }> + > + 'bookmarks:bulk-create': ( + ...args: [{ items: { itemType: string; itemId: string }[] }] + ) => Awaited> + 'bookmarks:bulk-delete': ( + ...args: [{ bookmarkIds: string[] }] + ) => Awaited> + 'bookmarks:create': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited< + Promise< + | { success: boolean; bookmark: null; error: string } + | { + success: boolean + bookmark: { + id: string + createdAt: string + position: number + itemType: string + itemId: string + } + error?: undefined + } + > + > + 'bookmarks:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'bookmarks:get': ( + ...args: [string] + ) => Awaited< + Promise<{ + id: string + createdAt: string + position: number + itemType: string + itemId: string + } | null> + > + 'bookmarks:get-by-item': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited< + Promise<{ + id: string + createdAt: string + position: number + itemType: string + itemId: string + } | null> + > + 'bookmarks:is-bookmarked': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited> + 'bookmarks:list': ( + ...args: [ + { + itemType?: string | undefined + sortBy?: 'createdAt' | 'position' | undefined + sortOrder?: 'asc' | 'desc' | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise + > + 'bookmarks:list-by-type': ( + ...args: [string] + ) => Awaited< + Promise + > + 'bookmarks:reorder': ( + ...args: [{ bookmarkIds: string[] }] + ) => Awaited> + 'bookmarks:toggle': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited< + Promise<{ + success: boolean + isBookmarked: boolean + bookmark: { + id: string + createdAt: string + position: number + itemType: string + itemId: string + } | null + }> + > + 'context-menu:show': ( + ...args: [ + { + id: string + label: string + accelerator?: string | undefined + disabled?: boolean | undefined + type?: 'normal' | 'separator' | undefined + }[] + ] + ) => Awaited> + 'crdt:apply-update': (...args: [unknown]) => Awaited> + 'crdt:close-doc': (...args: [unknown]) => Awaited> + 'crdt:open-doc': ( + ...args: [unknown] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'crdt:sync-step-1': ( + ...args: [{ noteId: string; stateVector: number[] }] + ) => Awaited< + Promise + > + 'crdt:sync-step-2': (...args: [{ noteId: string; diff: number[] }]) => Awaited> + 'crypto:decrypt-item': ( + ...args: [ + { + itemId: string + type: + | 'note' + | 'filter' + | 'task' + | 'project' + | 'journal' + | 'settings' + | 'inbox' + | 'tag_definition' + encryptedKey: string + keyNonce: string + encryptedData: string + dataNonce: string + signature: string + operation?: 'create' | 'update' | 'delete' | undefined + deletedAt?: number | undefined + metadata?: Record | undefined + } + ] + ) => Awaited< + Promise + > + 'crypto:encrypt-item': ( + ...args: [ + { + itemId: string + type: + | 'note' + | 'filter' + | 'task' + | 'project' + | 'journal' + | 'settings' + | 'inbox' + | 'tag_definition' + content: Record + operation?: 'create' | 'update' | 'delete' | undefined + deletedAt?: number | undefined + metadata?: Record | undefined + } + ] + ) => Awaited< + Promise + > + 'crypto:get-rotation-progress': ( + ...args: [] + ) => Awaited + 'crypto:rotate-keys': ( + ...args: [{ confirm: boolean }] + ) => Awaited> + 'crypto:verify-signature': ( + ...args: [ + { + itemId: string + type: + | 'note' + | 'filter' + | 'task' + | 'project' + | 'journal' + | 'settings' + | 'inbox' + | 'tag_definition' + encryptedKey: string + keyNonce: string + encryptedData: string + dataNonce: string + signature: string + operation?: 'create' | 'update' | 'delete' | undefined + deletedAt?: number | undefined + metadata?: Record | undefined + } + ] + ) => Awaited< + Promise + > + 'folder-view:delete-view': ( + ...args: [{ folderPath: string; viewName: string }] + ) => Awaited< + Promise + > + 'folder-view:folder-exists': (...args: [string]) => Awaited + 'folder-view:get-available-properties': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/folder-view-api').GetAvailablePropertiesResponse + > + > + 'folder-view:get-config': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise + > + 'folder-view:get-folder-suggestions': ( + ...args: [{ noteId: string }] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/folder-view-api').GetFolderSuggestionsResponse + > + > + 'folder-view:get-views': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise + > + 'folder-view:list-with-properties': ( + ...args: [ + { + folderPath: string + properties?: string[] | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/folder-view-api').ListWithPropertiesResponse + > + > + 'folder-view:set-config': ( + ...args: [ + { + folderPath: string + config: { + path?: string | undefined + template?: string | undefined + inherit?: boolean | undefined + formulas?: Record | undefined + properties?: + | Record< + string, + { + displayName?: string | undefined + color?: boolean | undefined + dateFormat?: string | undefined + numberFormat?: string | undefined + hidden?: boolean | undefined + } + > + | undefined + summaries?: + | Record< + string, + { + type: + | 'custom' + | 'count' + | 'sum' + | 'average' + | 'min' + | 'max' + | 'countBy' + | 'countUnique' + label?: string | undefined + expression?: string | undefined + } + > + | undefined + views?: + | { + name: string + type?: 'table' | 'grid' | 'list' | 'kanban' | undefined + default?: boolean | undefined + columns?: + | { + id: string + width?: number | undefined + displayName?: string | undefined + showSummary?: boolean | undefined + }[] + | undefined + filters?: unknown + order?: { property: string; direction: 'asc' | 'desc' }[] | undefined + groupBy?: + | { + property: string + direction?: 'asc' | 'desc' | undefined + collapsed?: boolean | undefined + showSummary?: boolean | undefined + } + | undefined + limit?: number | undefined + showSummaries?: boolean | undefined + }[] + | undefined + } + } + ] + ) => Awaited< + Promise + > + 'folder-view:set-view': ( + ...args: [ + { + folderPath: string + view: { + name: string + type?: 'table' | 'grid' | 'list' | 'kanban' | undefined + default?: boolean | undefined + columns?: + | { + id: string + width?: number | undefined + displayName?: string | undefined + showSummary?: boolean | undefined + }[] + | undefined + filters?: unknown + order?: { property: string; direction: 'asc' | 'desc' }[] | undefined + groupBy?: + | { + property: string + direction?: 'asc' | 'desc' | undefined + collapsed?: boolean | undefined + showSummary?: boolean | undefined + } + | undefined + limit?: number | undefined + showSummaries?: boolean | undefined + } + } + ] + ) => Awaited< + Promise + > + 'graph:get-graph-data': ( + ...args: [] + ) => Awaited<{ + nodes: { + id: string + type: 'note' | 'task' | 'project' | 'journal' + label: string + tags: string[] + wordCount: number + connectionCount: number + emoji: string | null + color: string + isOrphan: boolean + isUnresolved: boolean + }[] + edges: { + id: string + source: string + target: string + type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' + weight: number + }[] + }> + 'graph:get-local-graph': ( + ...args: [{ noteId: string; depth?: number | undefined }] + ) => Awaited<{ + nodes: { + id: string + type: 'note' | 'task' | 'project' | 'journal' + label: string + tags: string[] + wordCount: number + connectionCount: number + emoji: string | null + color: string + isOrphan: boolean + isUnresolved: boolean + }[] + edges: { + id: string + source: string + target: string + type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' + weight: number + }[] + }> + 'inbox:add-tag': ( + ...args: [any, any] + ) => Awaited> + 'inbox:archive': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-archive': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-archive-older-than': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-file': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-snooze': ( + ...args: [any] + ) => Awaited< + Promise<{ + success: boolean + processedCount: number + errors: { itemId: string; error: string }[] + }> + > + 'inbox:bulk-tag': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-clip': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-image': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-link': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-pdf': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-text': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-voice': ( + ...args: [any] + ) => Awaited> + 'inbox:convert-to-note': ( + ...args: [any] + ) => Awaited> + 'inbox:convert-to-task': ( + ...args: [any] + ) => Awaited> + 'inbox:delete-permanent': ( + ...args: [any] + ) => Awaited> + 'inbox:file': ( + ...args: [any] + ) => Awaited> + 'inbox:file-all-stale': ( + ...args: [] + ) => Awaited> + 'inbox:get': ( + ...args: [any] + ) => Awaited> + 'inbox:get-filing-history': ( + ...args: [any] + ) => Awaited< + Promise + > + 'inbox:get-patterns': ( + ...args: [] + ) => Awaited> + 'inbox:get-snoozed': (...args: []) => Awaited> + 'inbox:get-stale-threshold': (...args: []) => Awaited> + 'inbox:get-stats': ( + ...args: [] + ) => Awaited> + 'inbox:get-suggestions': ( + ...args: [any] + ) => Awaited< + Promise + > + 'inbox:get-tags': (...args: []) => Awaited> + 'inbox:link-to-note': ( + ...args: [any, any, any] + ) => Awaited> + 'inbox:list': ( + ...args: [any] + ) => Awaited> + 'inbox:list-archived': ( + ...args: [any] + ) => Awaited< + Promise + > + 'inbox:mark-viewed': ( + ...args: [any] + ) => Awaited> + 'inbox:remove-tag': ( + ...args: [any, any] + ) => Awaited> + 'inbox:retry-metadata': ( + ...args: [any] + ) => Awaited> + 'inbox:retry-transcription': ( + ...args: [any] + ) => Awaited> + 'inbox:set-stale-threshold': (...args: [any]) => Awaited> + 'inbox:snooze': ( + ...args: [any] + ) => Awaited> + 'inbox:track-suggestion': ( + ...args: [any, any, any, any, any, any, any] + ) => Awaited> + 'inbox:unarchive': ( + ...args: [any] + ) => Awaited> + 'inbox:undo-archive': ( + ...args: [any] + ) => Awaited> + 'inbox:undo-file': ( + ...args: [any] + ) => Awaited> + 'inbox:unsnooze': ( + ...args: [any] + ) => Awaited> + 'inbox:update': ( + ...args: [any] + ) => Awaited> + 'journal:createEntry': ( + ...args: [ + { + date: string + content?: string | undefined + tags?: string[] | undefined + properties?: Record | undefined + } + ] + ) => Awaited< + Promise<{ + id: string + date: string + content: string + wordCount: number + characterCount: number + tags: string[] + createdAt: string + modifiedAt: string + properties?: Record | undefined + }> + > + 'journal:deleteEntry': (...args: [{ date: string }]) => Awaited> + 'journal:getAllTags': (...args: []) => Awaited> + 'journal:getDayContext': ( + ...args: [{ date: string }] + ) => Awaited< + Promise<{ + date: string + tasks: { + id: string + title: string + completed: boolean + priority?: 'urgent' | 'high' | 'medium' | 'low' | undefined + isOverdue?: boolean | undefined + }[] + events: { + id: string + time: string + title: string + type: 'meeting' | 'focus' | 'event' + attendeeCount?: number | undefined + }[] + overdueCount: number + }> + > + 'journal:getEntry': ( + ...args: [{ date: string }] + ) => Awaited< + Promise<{ + id: string + date: string + content: string + wordCount: number + characterCount: number + tags: string[] + createdAt: string + modifiedAt: string + properties?: Record | undefined + } | null> + > + 'journal:getHeatmap': ( + ...args: [{ year: number }] + ) => Awaited> + 'journal:getMonthEntries': ( + ...args: [{ year: number; month: number }] + ) => Awaited< + Promise< + { + date: string + preview: string + wordCount: number + characterCount: number + activityLevel: 0 | 1 | 2 | 4 | 3 + tags: string[] + }[] + > + > + 'journal:getStreak': ( + ...args: [] + ) => Awaited< + Promise<{ currentStreak: number; longestStreak: number; lastEntryDate: string | null }> + > + 'journal:getYearStats': ( + ...args: [{ year: number }] + ) => Awaited< + Promise< + { + year: number + month: number + entryCount: number + totalWordCount: number + totalCharacterCount: number + averageLevel: number + }[] + > + > + 'journal:updateEntry': ( + ...args: [ + { + date: string + content?: string | undefined + tags?: string[] | undefined + properties?: Record | undefined + } + ] + ) => Awaited< + Promise<{ + id: string + date: string + content: string + wordCount: number + characterCount: number + tags: string[] + createdAt: string + modifiedAt: string + properties?: Record | undefined + }> + > + 'notes:create': ( + ...args: [ + { + title: string + content?: string | undefined + folder?: string | undefined + tags?: string[] | undefined + template?: string | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:create-folder': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:create-property-definition': ( + ...args: [ + { + name: string + type: 'number' | 'date' | 'text' | 'checkbox' | 'url' + options?: string[] | undefined + defaultValue?: unknown + color?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + definition: { + type: string + name: string + createdAt: string + options: string | null + defaultValue: string | null + color: string | null + } + error?: undefined + } + | { success: boolean; definition: null; error: string } + > + > + 'notes:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:delete-attachment': ( + ...args: [{ noteId: string; filename: string }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:delete-folder': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:delete-version': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:exists': (...args: [string]) => Awaited> + 'notes:export-html': ( + ...args: [ + { + noteId: string + includeMetadata?: boolean | undefined + pageSize?: 'A4' | 'Letter' | 'Legal' | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; error: string; path?: undefined } + | { success: boolean; path: string; error?: undefined } + > + > + 'notes:export-pdf': ( + ...args: [ + { + noteId: string + includeMetadata?: boolean | undefined + pageSize?: 'A4' | 'Letter' | 'Legal' | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; error: string; path?: undefined } + | { success: boolean; path: string; error?: undefined } + > + > + 'notes:get': (...args: [string]) => Awaited> + 'notes:get-all-positions': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; positions: Record; error?: undefined } + | { success: boolean; positions: {}; error: string } + > + > + 'notes:get-by-path': (...args: [string]) => Awaited> + 'notes:get-file': ( + ...args: [string] + ) => Awaited> + 'notes:get-folder-config': ( + ...args: [string] + ) => Awaited< + Promise + > + 'notes:get-folder-template': (...args: [string]) => Awaited> + 'notes:get-folders': (...args: []) => Awaited> + 'notes:get-links': ( + ...args: [string] + ) => Awaited> + 'notes:get-local-only-count': (...args: []) => Awaited> + 'notes:get-positions': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise< + | { + success: boolean + positions: { path: string; position: number; folderPath: string }[] + error?: undefined + } + | { success: boolean; positions: never[]; error: string } + > + > + 'notes:get-property-definitions': ( + ...args: [] + ) => Awaited< + Promise< + { + type: string + name: string + createdAt: string + options: string | null + defaultValue: string | null + color: string | null + }[] + > + > + 'notes:get-tags': ( + ...args: [] + ) => Awaited> + 'notes:get-version': ( + ...args: [string] + ) => Awaited> + 'notes:get-versions': ( + ...args: [string] + ) => Awaited> + 'notes:import-files': ( + ...args: [{ sourcePaths: string[]; targetFolder?: string | undefined }] + ) => Awaited> + 'notes:list': ( + ...args: [ + { + folder?: string | undefined + tags?: string[] | undefined + sortBy?: 'title' | 'modified' | 'created' | 'position' | undefined + sortOrder?: 'asc' | 'desc' | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited> + 'notes:list-attachments': ( + ...args: [string] + ) => Awaited> + 'notes:move': ( + ...args: [{ id: string; newFolder: string }] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:open-external': (...args: [string]) => Awaited> + 'notes:rename': ( + ...args: [{ id: string; newTitle: string }] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:rename-folder': ( + ...args: [{ oldPath: string; newPath: string }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:reorder': ( + ...args: [{ folderPath: string; notePaths: string[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:resolve-by-title': ( + ...args: [string] + ) => Awaited< + Promise<{ + id: string + path: string + title: string + fileType: import('../../../../../packages/shared/src/file-types').FileType + } | null> + > + 'notes:restore-version': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:reveal-in-finder': (...args: [string]) => Awaited> + 'notes:set-folder-config': ( + ...args: [ + { + folderPath: string + config: { template?: string | undefined; inherit?: boolean | undefined } + } + ] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:set-local-only': ( + ...args: [{ id: string; localOnly: boolean }] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:show-import-dialog': ( + ...args: [] + ) => Awaited> + 'notes:update': ( + ...args: [ + { + id: string + title?: string | undefined + content?: string | undefined + tags?: string[] | undefined + frontmatter?: Record | undefined + emoji?: string | null | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:update-property-definition': ( + ...args: [ + { + name: string + type?: 'number' | 'date' | 'text' | 'checkbox' | 'url' | undefined + options?: string[] | undefined + defaultValue?: unknown + color?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + definition: + | { + type: string + name: string + createdAt: string + options: string | null + defaultValue: string | null + color: string | null + } + | undefined + error?: undefined + } + | { success: boolean; definition: null; error: string } + > + > + 'notes:upload-attachment': ( + ...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer }] + ) => Awaited> + 'properties:get': ( + ...args: [{ entityId: string }] + ) => Awaited> + 'properties:rename': ( + ...args: [{ entityId: string; oldName: string; newName: string }] + ) => Awaited< + Promise + > + 'properties:set': ( + ...args: [{ entityId: string; properties: Record }] + ) => Awaited< + Promise + > + 'quick-capture:get-clipboard': (...args: []) => Awaited + 'reminder:bulk-dismiss': ( + ...args: [{ reminderIds: string[] }] + ) => Awaited< + Promise< + | { success: boolean; dismissedCount: number; error?: undefined } + | { success: boolean; dismissedCount: number; error: string } + > + > + 'reminder:count-pending': (...args: []) => Awaited> + 'reminder:create': ( + ...args: [ + | { + targetType: 'note' + targetId: string + remindAt: string + title?: string | undefined + note?: string | undefined + } + | { + targetType: 'journal' + targetId: string + remindAt: string + title?: string | undefined + note?: string | undefined + } + | { + targetType: 'highlight' + targetId: string + highlightText: string + highlightStart: number + highlightEnd: number + remindAt: string + title?: string | undefined + note?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + | { success: boolean; reminder: null; error: string } + > + > + 'reminder:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'reminder:dismiss': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; reminder: null; error: string } + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + > + > + 'reminder:get': ( + ...args: [string] + ) => Awaited< + Promise + > + 'reminder:get-due': ( + ...args: [] + ) => Awaited< + Promise + > + 'reminder:get-for-target': ( + ...args: [{ targetType: 'note' | 'journal' | 'highlight'; targetId: string }] + ) => Awaited> + 'reminder:get-upcoming': ( + ...args: [number | undefined] + ) => Awaited< + Promise<{ + reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] + total: number + hasMore: boolean + }> + > + 'reminder:list': ( + ...args: [ + { + targetType?: 'note' | 'journal' | 'highlight' | undefined + targetId?: string | undefined + status?: + | 'pending' + | 'triggered' + | 'dismissed' + | 'snoozed' + | ('pending' | 'triggered' | 'dismissed' | 'snoozed')[] + | undefined + fromDate?: string | undefined + toDate?: string | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise<{ + reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] + total: number + hasMore: boolean + }> + > + 'reminder:snooze': ( + ...args: [{ id: string; snoozeUntil: string }] + ) => Awaited< + Promise< + | { success: boolean; reminder: null; error: string } + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + > + > + 'reminder:update': ( + ...args: [ + { + id: string + remindAt?: string | undefined + title?: string | null | undefined + note?: string | null | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; reminder: null; error: string } + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + > + > + 'saved-filters:create': ( + ...args: [ + { + name: string + config: { + filters: { + search?: string | undefined + projectIds?: string[] | undefined + priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined + dueDate?: + | { + type: + | 'custom' + | 'any' + | 'none' + | 'overdue' + | 'today' + | 'tomorrow' + | 'this-week' + | 'next-week' + | 'this-month' + customStart?: string | null | undefined + customEnd?: string | null | undefined + } + | undefined + statusIds?: string[] | undefined + completion?: 'active' | 'completed' | 'all' | undefined + repeatType?: 'all' | 'repeating' | 'one-time' | undefined + hasTime?: 'all' | 'with-time' | 'without-time' | undefined + } + sort?: + | { + field: 'title' | 'createdAt' | 'priority' | 'dueDate' | 'completedAt' | 'project' + direction: 'asc' | 'desc' + } + | undefined + starred?: boolean | undefined + } + } + ] + ) => Awaited< + Promise<{ + success: boolean + savedFilter: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter + }> + > + 'saved-filters:delete': ( + ...args: [{ id: string }] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'saved-filters:list': ( + ...args: [] + ) => Awaited< + Promise<{ + savedFilters: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter[] + }> + > + 'saved-filters:reorder': ( + ...args: [{ ids: string[]; positions: number[] }] + ) => Awaited> + 'saved-filters:update': ( + ...args: [ + { + id: string + name?: string | undefined + config?: + | { + filters: { + search?: string | undefined + projectIds?: string[] | undefined + priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined + dueDate?: + | { + type: + | 'custom' + | 'any' + | 'none' + | 'overdue' + | 'today' + | 'tomorrow' + | 'this-week' + | 'next-week' + | 'this-month' + customStart?: string | null | undefined + customEnd?: string | null | undefined + } + | undefined + statusIds?: string[] | undefined + completion?: 'active' | 'completed' | 'all' | undefined + repeatType?: 'all' | 'repeating' | 'one-time' | undefined + hasTime?: 'all' | 'with-time' | 'without-time' | undefined + } + sort?: + | { + field: + | 'title' + | 'createdAt' + | 'priority' + | 'dueDate' + | 'completedAt' + | 'project' + direction: 'asc' | 'desc' + } + | undefined + starred?: boolean | undefined + } + | undefined + position?: number | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; savedFilter: null; error: string } + | { + success: boolean + savedFilter: + | import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter + | null + error?: undefined + } + > + > + 'search:add-reason': ( + ...args: [ + { + itemId: string + itemType: 'note' | 'task' | 'journal' | 'inbox' + itemTitle: string + searchQuery: string + itemIcon?: string | null | undefined + } + ] + ) => Awaited> + 'search:clear-reasons': (...args: []) => Awaited> + 'search:get-all-tags': (...args: []) => Awaited> + 'search:get-reasons': ( + ...args: [] + ) => Awaited> + 'search:get-stats': ( + ...args: [] + ) => Awaited> + 'search:query': ( + ...args: [ + { + text: string + types?: ('note' | 'task' | 'journal' | 'inbox')[] | undefined + tags?: string[] | undefined + dateRange?: { from: string; to: string } | null | undefined + projectId?: string | null | undefined + folderPath?: string | null | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited> + 'search:quick': ( + ...args: [string] + ) => Awaited< + Promise + > + 'search:rebuild-index': ( + ...args: [] + ) => Awaited< + Promise< + | { + notes: number + tasks: number + inbox: number + durationMs: number + started: true + error?: undefined + } + | { started: false; error: string } + > + > + 'settings:get': (...args: [string]) => Awaited + 'settings:getAIModelStatus': ( + ...args: [] + ) => Awaited> + 'settings:getAISettings': (...args: []) => Awaited + 'settings:getBackupSettings': ( + ...args: [] + ) => Awaited<{ + autoBackup: boolean + frequencyHours: 1 | 6 | 12 | 24 + maxBackups: number + lastBackupAt: string | null + }> + 'settings:getEditorSettings': ( + ...args: [] + ) => Awaited<{ + width: 'medium' | 'narrow' | 'wide' + spellCheck: boolean + autoSaveDelay: number + showWordCount: boolean + toolbarMode: 'floating' | 'sticky' + }> + 'settings:getGeneralSettings': ( + ...args: [] + ) => Awaited<{ + theme: 'light' | 'dark' | 'white' | 'system' + fontSize: 'small' | 'medium' | 'large' + fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' + accentColor: string + startOnBoot: boolean + language: string + onboardingCompleted: boolean + }> + 'settings:getGraphSettings': ( + ...args: [] + ) => Awaited<{ + layout: 'forceatlas2' | 'circular' | 'random' + showLabels: boolean + showEdgeLabels: boolean + animateLayout: boolean + showTagEdges: boolean + }> + 'settings:getJournalSettings': ( + ...args: [] + ) => Awaited<{ + defaultTemplate: string | null + showSchedule: boolean + showTasks: boolean + showAIConnections: boolean + showStatsFooter: boolean + }> + 'settings:getKeyboardSettings': ( + ...args: [] + ) => Awaited<{ + overrides: Record< + string, + { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } + > + globalCapture: { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } | null + }> + 'settings:getNoteEditorSettings': ( + ...args: [] + ) => Awaited + 'settings:getSyncSettings': (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean }> + 'settings:getTabSettings': (...args: []) => Awaited + 'settings:getTaskSettings': ( + ...args: [] + ) => Awaited<{ + defaultProjectId: string | null + defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' + weekStartDay: 'sunday' | 'monday' + staleInboxDays: number + }> + 'settings:loadAIModel': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; message: string; error?: undefined } + | { success: boolean; error: string; message?: undefined } + | { success: boolean; message?: undefined; error?: undefined } + > + > + 'settings:reindexEmbeddings': ( + ...args: [] + ) => Awaited< + Promise<{ success: boolean; computed: number; skipped: number; error?: string | undefined }> + > + 'settings:resetKeyboardSettings': ( + ...args: [] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:set': ( + ...args: [{ key: string; value: string }] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setAISettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setBackupSettings': ( + ...args: [ + Partial<{ + autoBackup: boolean + frequencyHours: 1 | 6 | 12 | 24 + maxBackups: number + lastBackupAt: string | null + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setEditorSettings': ( + ...args: [ + Partial<{ + width: 'medium' | 'narrow' | 'wide' + spellCheck: boolean + autoSaveDelay: number + showWordCount: boolean + toolbarMode: 'floating' | 'sticky' + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setGeneralSettings': ( + ...args: [ + Partial<{ + theme: 'light' | 'dark' | 'white' | 'system' + fontSize: 'small' | 'medium' | 'large' + fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' + accentColor: string + startOnBoot: boolean + language: string + onboardingCompleted: boolean + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setGraphSettings': ( + ...args: [ + Partial<{ + layout: 'forceatlas2' | 'circular' | 'random' + showLabels: boolean + showEdgeLabels: boolean + animateLayout: boolean + showTagEdges: boolean + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setJournalSettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setKeyboardSettings': ( + ...args: [ + Partial<{ + overrides: Record< + string, + { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } + > + globalCapture: { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } | null + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setNoteEditorSettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setSyncSettings': ( + ...args: [Partial<{ enabled: boolean; autoSync: boolean }>] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setTabSettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setTaskSettings': ( + ...args: [ + Partial<{ + defaultProjectId: string | null + defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' + weekStartDay: 'sunday' | 'monday' + staleInboxDays: number + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'sync:approve-linking': ( + ...args: [{ sessionId: string }] + ) => Awaited< + Promise + > + 'sync:check-device-status': (...args: []) => Awaited> + 'sync:complete-linking-qr': ( + ...args: [{ sessionId: string }] + ) => Awaited< + Promise + > + 'sync:confirm-recovery-phrase': ( + ...args: [{ confirmed: boolean }] + ) => Awaited> + 'sync:download-attachment': ( + ...args: [{ attachmentId: string; targetPath?: string | undefined }] + ) => Awaited< + Promise< + | { success: boolean; error: string; filePath?: undefined } + | { success: boolean; filePath: string; error?: undefined } + > + > + 'sync:emergency-wipe': (...args: []) => Awaited> + 'sync:generate-linking-qr': ( + ...args: [] + ) => Awaited< + Promise + > + 'sync:get-devices': ( + ...args: [] + ) => Awaited< + Promise<{ + devices: { + id: string + name: string + platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android' + linkedAt: number + lastSyncAt: number | undefined + isCurrentDevice: boolean + }[] + email: string | undefined + }> + > + 'sync:get-download-progress': ( + ...args: [{ attachmentId: string }] + ) => Awaited< + Promise<{ + progress: number + downloadedChunks: number + totalChunks: number + status: 'downloading' + } | null> + > + 'sync:get-history': ( + ...args: [{ limit?: number | undefined; offset?: number | undefined }] + ) => Awaited< + Promise<{ + entries: { + id: string + type: 'error' | 'push' | 'pull' + itemCount: number + direction: string | undefined + details: unknown + durationMs: number | undefined + createdAt: number + }[] + total: number + }> + > + 'sync:get-linking-sas': ( + ...args: [{ sessionId: string }] + ) => Awaited> + 'sync:get-quarantined-items': ( + ...args: [] + ) => Awaited + 'sync:get-queue-size': (...args: []) => Awaited<{ pending: number; failed: number }> + 'sync:get-recovery-phrase': (...args: []) => Awaited + 'sync:get-status': ( + ...args: [] + ) => Awaited< + | import('../../../../../packages/contracts/src/ipc-sync-ops').GetSyncStatusResult + | { status: string; pendingCount: number } + > + 'sync:get-storage-breakdown': ( + ...args: [] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/ipc-sync-ops').StorageBreakdownResult | null + > + > + 'sync:get-synced-settings': ( + ...args: [] + ) => Awaited<{ + general?: + | { + theme?: 'light' | 'dark' | 'white' | 'system' | undefined + fontSize?: 'small' | 'medium' | 'large' | undefined + fontFamily?: 'system' | 'serif' | 'sans-serif' | 'monospace' | undefined + accentColor?: string | undefined + startOnBoot?: boolean | undefined + language?: string | undefined + } + | undefined + editor?: + | { + width?: 'medium' | 'narrow' | 'wide' | undefined + spellCheck?: boolean | undefined + autoSaveDelay?: number | undefined + showWordCount?: boolean | undefined + toolbarMode?: 'floating' | 'sticky' | undefined + } + | undefined + tasks?: + | { + defaultProjectId?: string | null | undefined + defaultSortOrder?: 'createdAt' | 'priority' | 'dueDate' | 'manual' | undefined + weekStartDay?: 'sunday' | 'monday' | undefined + staleInboxDays?: number | undefined + showCompleted?: boolean | undefined + sortBy?: string | undefined + } + | undefined + keyboard?: { overrides?: Record | undefined } | undefined + notes?: + | { + defaultFolder?: string | undefined + editorFontSize?: number | undefined + spellCheck?: boolean | undefined + } + | undefined + sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined } | undefined + } | null> + 'sync:get-upload-progress': ( + ...args: [{ sessionId: string }] + ) => Awaited< + Promise<{ + progress: number + uploadedChunks: number + totalChunks: number + status: 'uploading' + } | null> + > + 'sync:link-via-qr': ( + ...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined }] + ) => Awaited> + 'sync:link-via-recovery': ( + ...args: [{ recoveryPhrase: string }] + ) => Awaited< + Promise< + | { success: boolean; error: string; deviceId?: undefined } + | { success: boolean; deviceId: string; error?: undefined } + > + > + 'sync:logout': ( + ...args: [] + ) => Awaited> + 'sync:pause': (...args: []) => Awaited<{ success: boolean; wasPaused: boolean }> + 'sync:remove-device': ( + ...args: [{ deviceId: string }] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'sync:rename-device': ( + ...args: [{ deviceId: string; newName: string }] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'sync:resume': (...args: []) => Awaited<{ success: boolean; pendingCount: number }> + 'sync:setup-first-device': ( + ...args: [{ oauthToken: string; provider: 'google'; state: string }] + ) => Awaited< + Promise< + | { + success: boolean + needsRecoverySetup: boolean + deviceId: string + needsRecoveryInput?: undefined + } + | { + success: boolean + needsRecoverySetup: boolean + needsRecoveryInput: boolean + deviceId?: undefined + } + > + > + 'sync:setup-new-account': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; error: string; deviceId?: undefined } + | { success: boolean; deviceId: string; error?: undefined } + > + > + 'sync:trigger-sync': ( + ...args: [] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'sync:update-synced-setting': ( + ...args: [unknown] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'sync:upload-attachment': ( + ...args: [{ noteId: string; filePath: string }] + ) => Awaited< + Promise< + | { success: boolean; error: string; attachmentId?: undefined; sessionId?: undefined } + | { success: boolean; attachmentId: string; sessionId: string; error?: undefined } + > + > + 'tags:delete': ( + ...args: [string] + ) => Awaited> + 'tags:get-all-with-counts': ( + ...args: [] + ) => Awaited< + Promise + > + 'tags:get-notes-by-tag': ( + ...args: [ + { + tag: string + sortBy?: 'title' | 'modified' | 'created' | undefined + sortOrder?: 'asc' | 'desc' | undefined + } + ] + ) => Awaited< + Promise + > + 'tags:merge': ( + ...args: [{ source: string; target: string }] + ) => Awaited> + 'tags:pin-note-to-tag': ( + ...args: [{ noteId: string; tag: string }] + ) => Awaited< + Promise + > + 'tags:remove-from-note': ( + ...args: [{ noteId: string; tag: string }] + ) => Awaited< + Promise + > + 'tags:rename': ( + ...args: [{ oldName: string; newName: string }] + ) => Awaited> + 'tags:unpin-note-from-tag': ( + ...args: [{ noteId: string; tag: string }] + ) => Awaited< + Promise + > + 'tags:update-color': ( + ...args: [{ tag: string; color: string }] + ) => Awaited< + Promise + > + 'tasks:archive': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'tasks:bulk-archive': ( + ...args: [{ ids: string[] }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:bulk-complete': ( + ...args: [{ ids: string[] }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:bulk-delete': ( + ...args: [{ ids: string[] }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:bulk-move': ( + ...args: [{ ids: string[]; projectId: string }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:complete': ( + ...args: [{ id: string; completedAt?: string | undefined }] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:convert-to-subtask': ( + ...args: [{ taskId: string; parentId: string }] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:convert-to-task': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:create': ( + ...args: [ + { + projectId: string + title: string + description?: string | null | undefined + priority?: number | undefined + statusId?: string | null | undefined + parentId?: string | null | undefined + dueDate?: string | null | undefined + dueTime?: string | null | undefined + startDate?: string | null | undefined + isRepeating?: boolean | undefined + repeatConfig?: + | { + frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' + endType: 'date' | 'never' | 'count' + createdAt: string + interval?: number | undefined + daysOfWeek?: number[] | undefined + monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined + dayOfMonth?: number | undefined + weekOfMonth?: number | undefined + dayOfWeekForMonth?: number | undefined + endDate?: string | null | undefined + endCount?: number | undefined + completedCount?: number | undefined + } + | null + | undefined + repeatFrom?: 'due' | 'completion' | null | undefined + tags?: string[] | undefined + linkedNoteIds?: string[] | undefined + sourceNoteId?: string | null | undefined + position?: number | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + | { success: boolean; task: null; error: string } + > + > + 'tasks:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:duplicate': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:get': ( + ...args: [string] + ) => Awaited< + Promise<{ + tags: string[] + linkedNoteIds: string[] + hasSubtasks: boolean + subtaskCount: number + completedSubtaskCount: number + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } | null> + > + 'tasks:get-linked-tasks': ( + ...args: [string] + ) => Awaited< + Promise< + { + tags: string[] + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + > + > + 'tasks:get-overdue': ( + ...args: [] + ) => Awaited< + Promise<{ + tasks: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:get-stats': ( + ...args: [] + ) => Awaited< + Promise<{ + total: number + completed: number + overdue: number + dueToday: number + dueThisWeek: number + }> + > + 'tasks:get-subtasks': ( + ...args: [string] + ) => Awaited< + Promise< + { + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + > + > + 'tasks:get-tags': (...args: []) => Awaited> + 'tasks:get-today': ( + ...args: [] + ) => Awaited< + Promise<{ + tasks: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:get-upcoming': ( + ...args: [{ days?: number | undefined }] + ) => Awaited< + Promise<{ + tasks: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:list': ( + ...args: [ + { + projectId?: string | undefined + statusId?: string | null | undefined + parentId?: string | null | undefined + includeCompleted?: boolean | undefined + includeArchived?: boolean | undefined + dueBefore?: string | undefined + dueAfter?: string | undefined + tags?: string[] | undefined + search?: string | undefined + sortBy?: 'modified' | 'created' | 'position' | 'priority' | 'dueDate' | undefined + sortOrder?: 'asc' | 'desc' | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise<{ + tasks: { + tags: string[] + linkedNoteIds: string[] + hasSubtasks: boolean + subtaskCount: number + completedSubtaskCount: number + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:move': ( + ...args: [ + { + taskId: string + position: number + targetProjectId?: string | undefined + targetStatusId?: string | null | undefined + targetParentId?: string | null | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:project-archive': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:project-create': ( + ...args: [ + { + name: string + description?: string | null | undefined + color?: string | undefined + icon?: string | null | undefined + statuses?: + | { + name: string + type: 'todo' | 'in_progress' | 'done' + order: number + color?: string | undefined + }[] + | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + project: { + id: string + name: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + color: string + description: string | null + icon: string | null + isInbox: boolean + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + } + error?: undefined + } + | { success: boolean; project: null; error: string } + > + > + 'tasks:project-delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:project-get': ( + ...args: [string] + ) => Awaited> + 'tasks:project-list': ( + ...args: [] + ) => Awaited> + 'tasks:project-reorder': ( + ...args: [{ projectIds: string[]; positions: number[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:project-update': ( + ...args: [ + { + id: string + name?: string | undefined + description?: string | null | undefined + color?: string | undefined + icon?: string | null | undefined + statuses?: + | { + name: string + type: 'todo' | 'in_progress' | 'done' + order: number + id?: string | undefined + color?: string | undefined + }[] + | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; project: null; error: string } + | { + success: boolean + project: { + id: string + name: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + color: string + description: string | null + icon: string | null + isInbox: boolean + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + } + error?: undefined + } + > + > + 'tasks:reorder': ( + ...args: [{ taskIds: string[]; positions: number[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:seed-demo': (...args: []) => Awaited> + 'tasks:seed-performance-test': ( + ...args: [] + ) => Awaited> + 'tasks:status-create': ( + ...args: [ + { projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined } + ] + ) => Awaited< + Promise< + | { + success: boolean + status: { + id: string + name: string + createdAt: string + position: number + color: string + projectId: string + isDefault: boolean + isDone: boolean + } + error?: undefined + } + | { success: boolean; status: null; error: string } + > + > + 'tasks:status-delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:status-list': ( + ...args: [string] + ) => Awaited< + Promise< + { + id: string + name: string + createdAt: string + position: number + color: string + projectId: string + isDefault: boolean + isDone: boolean + }[] + > + > + 'tasks:status-reorder': ( + ...args: [{ statusIds: string[]; positions: number[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:status-update': ( + ...args: [ + { + id: string + name?: string | undefined + color?: string | undefined + position?: number | undefined + isDefault?: boolean | undefined + isDone?: boolean | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; error: string; status?: undefined } + | { + success: boolean + status: { + id: string + name: string + createdAt: string + position: number + color: string + projectId: string + isDefault: boolean + isDone: boolean + } + error?: undefined + } + > + > + 'tasks:unarchive': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'tasks:uncomplete': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:update': ( + ...args: [ + { + id: string + title?: string | undefined + description?: string | null | undefined + priority?: number | undefined + projectId?: string | undefined + statusId?: string | null | undefined + parentId?: string | null | undefined + dueDate?: string | null | undefined + dueTime?: string | null | undefined + startDate?: string | null | undefined + isRepeating?: boolean | undefined + repeatConfig?: + | { + frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' + endType: 'date' | 'never' | 'count' + createdAt: string + interval?: number | undefined + daysOfWeek?: number[] | undefined + monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined + dayOfMonth?: number | undefined + weekOfMonth?: number | undefined + dayOfWeekForMonth?: number | undefined + endDate?: string | null | undefined + endCount?: number | undefined + completedCount?: number | undefined + } + | null + | undefined + repeatFrom?: 'due' | 'completion' | null | undefined + tags?: string[] | undefined + linkedNoteIds?: string[] | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'templates:create': ( + ...args: [ + { + name: string + description?: string | undefined + icon?: string | null | undefined + tags?: string[] | undefined + properties?: + | { + name: string + type: + | 'number' + | 'date' + | 'text' + | 'select' + | 'checkbox' + | 'url' + | 'multiselect' + | 'rating' + value: unknown + options?: string[] | undefined + }[] + | undefined + content?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + template: import('../../../../../packages/contracts/src/templates-api').Template + error?: undefined + } + | { success: boolean; template: null; error: string } + > + > + 'templates:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'templates:duplicate': ( + ...args: [{ id: string; newName: string }] + ) => Awaited< + Promise< + | { + success: boolean + template: import('../../../../../packages/contracts/src/templates-api').Template + error?: undefined + } + | { success: boolean; template: null; error: string } + > + > + 'templates:get': ( + ...args: [string] + ) => Awaited< + Promise + > + 'templates:list': ( + ...args: [] + ) => Awaited< + Promise<{ + templates: import('../../../../../packages/contracts/src/templates-api').TemplateListItem[] + }> + > + 'templates:update': ( + ...args: [ + { + id: string + name?: string | undefined + description?: string | undefined + icon?: string | null | undefined + tags?: string[] | undefined + properties?: + | { + name: string + type: + | 'number' + | 'date' + | 'text' + | 'select' + | 'checkbox' + | 'url' + | 'multiselect' + | 'rating' + value: unknown + options?: string[] | undefined + }[] + | undefined + content?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + template: import('../../../../../packages/contracts/src/templates-api').Template + error?: undefined + } + | { success: boolean; template: null; error: string } + > + > + 'vault:close': (...args: []) => Awaited> + 'vault:get-all': ( + ...args: [] + ) => Awaited> + 'vault:get-config': ( + ...args: [] + ) => Awaited> + 'vault:get-status': ( + ...args: [] + ) => Awaited> + 'vault:reindex': (...args: []) => Awaited> + 'vault:remove': (...args: [string]) => Awaited> + 'vault:select': ( + ...args: [{ path?: string | undefined }] + ) => Awaited< + Promise + > + 'vault:switch': ( + ...args: [string] + ) => Awaited< + Promise + > + 'vault:update-config': ( + ...args: [ + { + excludePatterns?: string[] | undefined + defaultNoteFolder?: string | undefined + journalFolder?: string | undefined + attachmentsFolder?: string | undefined + } + ] + ) => Awaited> } export type MainIpcInvokeChannel = keyof MainIpcInvokeHandlers -export type MainIpcInvokeArgs = - Parameters -export type MainIpcInvokeResult = - ReturnType +export type MainIpcInvokeArgs = Parameters +export type MainIpcInvokeResult = ReturnType< + MainIpcInvokeHandlers[C] +> diff --git a/apps/desktop/src/renderer/src/lib/shortcut-registry.ts b/apps/desktop/src/renderer/src/lib/shortcut-registry.ts new file mode 100644 index 000000000..b6e15b5fc --- /dev/null +++ b/apps/desktop/src/renderer/src/lib/shortcut-registry.ts @@ -0,0 +1,258 @@ +/** + * Shortcut Registry + * + * Central registry of all rebindable keyboard shortcuts with defaults, + * categories, and conflict detection. + */ + +import type { ShortcutBinding } from '@memry/contracts/settings-schemas' + +export interface ShortcutEntry { + id: string + label: string + description: string + category: string + defaultBinding: ShortcutBinding +} + +export interface ShortcutConflict { + conflictingId: string + conflictingLabel: string +} + +// ============================================================================ +// Platform detection +// ============================================================================ + +export const isMac = + typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC') + +// ============================================================================ +// Default shortcut registry +// ============================================================================ + +export const SHORTCUT_REGISTRY: ShortcutEntry[] = [ + // Navigation + { + id: 'nav.newNote', + label: 'New Note', + description: 'Create a new note', + category: 'Navigation', + defaultBinding: { key: 'n', modifiers: { meta: true } } + }, + { + id: 'nav.newTask', + label: 'New Task', + description: 'Create a new task', + category: 'Navigation', + defaultBinding: { key: 't', modifiers: { meta: true, shift: true } } + }, + { + id: 'nav.goToInbox', + label: 'Go to Inbox', + description: 'Navigate to the inbox', + category: 'Navigation', + defaultBinding: { key: 'i', modifiers: { meta: true, shift: true } } + }, + { + id: 'nav.goToNotes', + label: 'Go to Notes', + description: 'Navigate to notes', + category: 'Navigation', + defaultBinding: { key: 'e', modifiers: { meta: true, shift: true } } + }, + { + id: 'nav.goToTasks', + label: 'Go to Tasks', + description: 'Navigate to tasks', + category: 'Navigation', + defaultBinding: { key: 'k', modifiers: { meta: true, shift: true } } + }, + { + id: 'nav.search', + label: 'Search', + description: 'Open global search', + category: 'Navigation', + defaultBinding: { key: 'f', modifiers: { meta: true } } + }, + { + id: 'nav.settings', + label: 'Open Settings', + description: 'Open the settings panel', + category: 'Navigation', + defaultBinding: { key: ',', modifiers: { meta: true } } + }, + + // Tabs + { + id: 'tabs.closeTab', + label: 'Close Tab', + description: 'Close the current tab', + category: 'Tabs', + defaultBinding: { key: 'w', modifiers: { meta: true } } + }, + { + id: 'tabs.nextTab', + label: 'Next Tab', + description: 'Switch to the next tab', + category: 'Tabs', + defaultBinding: { key: 'Tab', modifiers: { ctrl: true } } + }, + { + id: 'tabs.prevTab', + label: 'Previous Tab', + description: 'Switch to the previous tab', + category: 'Tabs', + defaultBinding: { key: 'Tab', modifiers: { ctrl: true, shift: true } } + }, + { + id: 'tabs.reopenTab', + label: 'Reopen Last Tab', + description: 'Reopen the most recently closed tab', + category: 'Tabs', + defaultBinding: { key: 't', modifiers: { meta: true } } + }, + + // Editor + { + id: 'editor.save', + label: 'Save', + description: 'Save the current note', + category: 'Editor', + defaultBinding: { key: 's', modifiers: { meta: true } } + }, + { + id: 'editor.bold', + label: 'Bold', + description: 'Toggle bold formatting', + category: 'Editor', + defaultBinding: { key: 'b', modifiers: { meta: true } } + }, + { + id: 'editor.italic', + label: 'Italic', + description: 'Toggle italic formatting', + category: 'Editor', + defaultBinding: { key: 'i', modifiers: { meta: true } } + }, + { + id: 'editor.underline', + label: 'Underline', + description: 'Toggle underline formatting', + category: 'Editor', + defaultBinding: { key: 'u', modifiers: { meta: true } } + }, + + // View + { + id: 'view.toggleSidebar', + label: 'Toggle Sidebar', + description: 'Show or hide the sidebar', + category: 'View', + defaultBinding: { key: 's', modifiers: { meta: true, shift: true } } + }, + { + id: 'view.shortcuts', + label: 'Keyboard Shortcuts Help', + description: 'Show keyboard shortcuts reference', + category: 'View', + defaultBinding: { key: '/', modifiers: { meta: true } } + } +] + +// Category order for display +export const CATEGORY_ORDER = ['Navigation', 'Tabs', 'Editor', 'View'] + +// ============================================================================ +// Helpers +// ============================================================================ + +/** + * Format a ShortcutBinding as a human-readable string (e.g., "⌘ Shift N") + */ +export function formatBinding(binding: ShortcutBinding): string { + const parts: string[] = [] + if (binding.modifiers.meta) parts.push(isMac ? '⌘' : 'Ctrl') + if (binding.modifiers.ctrl) parts.push('Ctrl') + if (binding.modifiers.alt) parts.push(isMac ? '⌥' : 'Alt') + if (binding.modifiers.shift) parts.push('Shift') + parts.push(formatKey(binding.key)) + return parts.join(' ') +} + +/** + * Format a raw key to display-friendly string + */ +function formatKey(key: string): string { + const map: Record = { + ArrowUp: '↑', + ArrowDown: '↓', + ArrowLeft: '←', + ArrowRight: '→', + Enter: '↩', + Escape: 'Esc', + Backspace: '⌫', + Delete: '⌦', + Tab: '⇥', + Space: '␣' + } + return map[key] ?? key.toUpperCase() +} + +/** + * Resolve the effective binding for a shortcut (override takes precedence over default) + */ +export function resolveBinding( + entry: ShortcutEntry, + overrides: Record +): ShortcutBinding { + return overrides[entry.id] ?? entry.defaultBinding +} + +/** + * Check if two bindings are identical + */ +export function bindingsEqual(a: ShortcutBinding, b: ShortcutBinding): boolean { + return ( + a.key.toLowerCase() === b.key.toLowerCase() && + Boolean(a.modifiers.meta) === Boolean(b.modifiers.meta) && + Boolean(a.modifiers.ctrl) === Boolean(b.modifiers.ctrl) && + Boolean(a.modifiers.shift) === Boolean(b.modifiers.shift) && + Boolean(a.modifiers.alt) === Boolean(b.modifiers.alt) + ) +} + +/** + * Find conflicts: other shortcuts that use the same binding + */ +export function findConflicts( + id: string, + binding: ShortcutBinding, + overrides: Record +): ShortcutConflict[] { + return SHORTCUT_REGISTRY.filter((entry) => { + if (entry.id === id) return false + const effective = resolveBinding(entry, overrides) + return bindingsEqual(effective, binding) + }).map((entry) => ({ conflictingId: entry.id, conflictingLabel: entry.label })) +} + +/** + * Get shortcuts grouped by category in display order + */ +export function getGroupedShortcuts(): Map { + const grouped = new Map() + for (const cat of CATEGORY_ORDER) { + grouped.set(cat, []) + } + for (const entry of SHORTCUT_REGISTRY) { + const cat = entry.category + if (!grouped.has(cat)) grouped.set(cat, []) + grouped.get(cat)!.push(entry) + } + // Remove empty categories + for (const [key, entries] of grouped) { + if (entries.length === 0) grouped.delete(key) + } + return grouped +} diff --git a/apps/desktop/src/renderer/src/pages/settings.tsx b/apps/desktop/src/renderer/src/pages/settings.tsx index be9007e97..de2de7699 100644 --- a/apps/desktop/src/renderer/src/pages/settings.tsx +++ b/apps/desktop/src/renderer/src/pages/settings.tsx @@ -12,7 +12,8 @@ import { PenLine, Plug, Tags, - ListChecks + ListChecks, + Key } from '@/lib/icons' import { cn } from '@/lib/utils' import { GeneralSettings } from './settings/general-section' @@ -26,6 +27,7 @@ import { SyncSettings } from './settings/sync-section' import { IntegrationsSettings } from './settings/integrations-section' import { TagsSettings } from './settings/tags-section' import { TasksSettings } from './settings/tasks-section' +import { ShortcutsSettings } from './settings/shortcuts-section' type SettingsSection = | 'general' @@ -39,6 +41,7 @@ type SettingsSection = | 'sync' | 'integrations' | 'tags' + | 'shortcuts' export function SettingsPage() { const [activeSection, setActiveSection] = useState(() => { @@ -136,6 +139,12 @@ export function SettingsPage() { isActive={activeSection === 'tags'} onClick={() => setActiveSection('tags')} /> + } + label="Shortcuts" + isActive={activeSection === 'shortcuts'} + onClick={() => setActiveSection('shortcuts')} + /> @@ -154,6 +163,7 @@ export function SettingsPage() { {activeSection === 'sync' && } {activeSection === 'integrations' && } {activeSection === 'tags' && } + {activeSection === 'shortcuts' && } diff --git a/apps/desktop/src/renderer/src/pages/settings/editor-section.tsx b/apps/desktop/src/renderer/src/pages/settings/editor-section.tsx index 68a14493a..c99211a3d 100644 --- a/apps/desktop/src/renderer/src/pages/settings/editor-section.tsx +++ b/apps/desktop/src/renderer/src/pages/settings/editor-section.tsx @@ -2,26 +2,58 @@ import { useCallback } from 'react' import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' import { Label } from '@/components/ui/label' -import { Info } from '@/lib/icons' -import { useNoteEditorSettings } from '@/hooks/use-note-editor-settings' +import { Slider } from '@/components/ui/slider' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { useEditorSettings } from '@/hooks/use-editor-settings' import { toast } from 'sonner' export function EditorSettings() { - const { settings, isLoading, setToolbarMode } = useNoteEditorSettings() + const { settings, isLoading, updateSettings } = useEditorSettings() + + const handleWidthChange = useCallback( + async (value: string) => { + const success = await updateSettings({ width: value as 'narrow' | 'medium' | 'wide' }) + if (!success) toast.error('Failed to update editor width') + }, + [updateSettings] + ) const handleToolbarModeChange = useCallback( async (enabled: boolean) => { - const newMode = enabled ? 'sticky' : 'floating' - const success = await setToolbarMode(newMode) - if (success) { - toast.success( - enabled ? 'Sticky toolbar enabled' : 'Floating toolbar enabled (shows on text selection)' - ) - } else { - toast.error('Failed to update setting') - } + const success = await updateSettings({ toolbarMode: enabled ? 'sticky' : 'floating' }) + if (!success) toast.error('Failed to update toolbar mode') + }, + [updateSettings] + ) + + const handleSpellCheckChange = useCallback( + async (enabled: boolean) => { + const success = await updateSettings({ spellCheck: enabled }) + if (!success) toast.error('Failed to update spell check') + }, + [updateSettings] + ) + + const handleAutoSaveDelayChange = useCallback( + async (value: number[]) => { + const success = await updateSettings({ autoSaveDelay: value[0] }) + if (!success) toast.error('Failed to update auto-save delay') }, - [setToolbarMode] + [updateSettings] + ) + + const handleWordCountChange = useCallback( + async (enabled: boolean) => { + const success = await updateSettings({ showWordCount: enabled }) + if (!success) toast.error('Failed to update word count display') + }, + [updateSettings] ) if (isLoading) { @@ -35,6 +67,8 @@ export function EditorSettings() { ) } + const autoSaveSeconds = Math.round(settings.autoSaveDelay / 1000) + return (
@@ -44,15 +78,38 @@ export function EditorSettings() { - {/* Toolbar Mode Section */} + {/* Layout */}
-
-

- Toolbar -

+

+ Layout +

+ +
+ +

+ Controls the maximum width of the writing area +

+
+
+ + + + {/* Toolbar */} +
+

+ Toolbar +

- {/* Sticky Toolbar Toggle */}
@@ -66,15 +123,60 @@ export function EditorSettings() { onCheckedChange={handleToolbarModeChange} />
+
- {/* Info hint */} -
- -

- When disabled (floating mode), the formatting toolbar appears only when you select text. - Enable sticky mode to always have quick access to Bold, Italic, and other formatting - options. -

+ + + {/* Writing */} +
+

+ Writing +

+ +
+
+ +

Underline misspelled words while typing

+
+ +
+ +
+
+
+ +

+ How long to wait after typing stops before saving +

+
+ + {autoSaveSeconds === 0 ? 'Instant' : `${autoSaveSeconds}s`} + +
+ +
+ +
+
+ +

Show word count in the editor footer

+
+
diff --git a/apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx b/apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx new file mode 100644 index 000000000..69a2ff8d0 --- /dev/null +++ b/apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx @@ -0,0 +1,312 @@ +import { useState, useCallback, useRef, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Separator } from '@/components/ui/separator' +import { Kbd, KbdGroup } from '@/components/ui/kbd' +import { Badge } from '@/components/ui/badge' +import { Search, RotateCcw, X } from '@/lib/icons' +import { useKeyboardSettings } from '@/hooks/use-keyboard-settings' +import { toast } from 'sonner' +import type { ShortcutBinding } from '@memry/contracts/settings-schemas' +import { + SHORTCUT_REGISTRY, + CATEGORY_ORDER, + formatBinding, + resolveBinding, + findConflicts, + bindingsEqual, + getGroupedShortcuts, + type ShortcutEntry +} from '@/lib/shortcut-registry' + +// ============================================================================ +// Key Capture Row +// ============================================================================ + +interface ShortcutRowProps { + entry: ShortcutEntry + effectiveBinding: ShortcutBinding + isDefault: boolean + overrides: Record + onRebind: (id: string, binding: ShortcutBinding) => Promise + onClearOverride: (id: string) => Promise +} + +function ShortcutRow({ + entry, + effectiveBinding, + isDefault, + overrides, + onRebind, + onClearOverride +}: ShortcutRowProps) { + const [isCapturing, setIsCapturing] = useState(false) + const [conflict, setConflict] = useState(null) + const captureRef = useRef(null) + + const startCapture = useCallback(() => { + setIsCapturing(true) + setConflict(null) + }, []) + + const stopCapture = useCallback(() => { + setIsCapturing(false) + setConflict(null) + }, []) + + useEffect(() => { + if (!isCapturing) return + + const handleKeyDown = (e: KeyboardEvent): void => { + e.preventDefault() + e.stopPropagation() + + // Escape cancels capture + if (e.key === 'Escape') { + stopCapture() + return + } + + // Ignore bare modifier presses + if (['Meta', 'Control', 'Alt', 'Shift'].includes(e.key)) return + + const newBinding: ShortcutBinding = { + key: e.key, + modifiers: { + meta: e.metaKey || e.ctrlKey, + shift: e.shiftKey || undefined, + alt: e.altKey || undefined + } + } + + const conflicts = findConflicts(entry.id, newBinding, overrides) + if (conflicts.length > 0) { + setConflict(`Conflicts with: ${conflicts.map((c) => c.conflictingLabel).join(', ')}`) + return + } + + setIsCapturing(false) + setConflict(null) + void onRebind(entry.id, newBinding) + } + + window.addEventListener('keydown', handleKeyDown, { capture: true }) + return () => window.removeEventListener('keydown', handleKeyDown, { capture: true }) + }, [isCapturing, entry.id, overrides, onRebind, stopCapture]) + + // Close on outside click + useEffect(() => { + if (!isCapturing) return + const handleClick = (e: MouseEvent): void => { + if (captureRef.current && !captureRef.current.contains(e.target as Node)) { + stopCapture() + } + } + document.addEventListener('mousedown', handleClick) + return () => document.removeEventListener('mousedown', handleClick) + }, [isCapturing, stopCapture]) + + return ( +
+
+
+
+ {entry.label} + {!isDefault && ( + + Custom + + )} +
+

{entry.description}

+
+ +
+ {isCapturing ? ( +
+
+ Press shortcut… +
+ +
+ ) : ( +
+ + {!isDefault && ( + + )} +
+ )} +
+
+ + {conflict &&

{conflict}

} +
+ ) +} + +// ============================================================================ +// Shortcuts Section +// ============================================================================ + +export function ShortcutsSettings() { + const { settings, isLoading, updateSettings, resetToDefaults } = useKeyboardSettings() + const [query, setQuery] = useState('') + + const overrides = settings.overrides + + const handleRebind = useCallback( + async (id: string, binding: ShortcutBinding): Promise => { + const entry = SHORTCUT_REGISTRY.find((e) => e.id === id) + if (!entry) return + + // If new binding equals default, clear the override + if (bindingsEqual(binding, entry.defaultBinding)) { + const newOverrides = { ...overrides } + delete newOverrides[id] + const success = await updateSettings({ overrides: newOverrides }) + if (!success) toast.error('Failed to save shortcut') + return + } + + const success = await updateSettings({ overrides: { ...overrides, [id]: binding } }) + if (!success) toast.error('Failed to save shortcut') + }, + [overrides, updateSettings] + ) + + const handleClearOverride = useCallback( + async (id: string): Promise => { + const newOverrides = { ...overrides } + delete newOverrides[id] + const success = await updateSettings({ overrides: newOverrides }) + if (!success) toast.error('Failed to reset shortcut') + }, + [overrides, updateSettings] + ) + + const handleResetAll = useCallback(async () => { + const success = await resetToDefaults() + if (success) toast.success('All shortcuts reset to defaults') + else toast.error('Failed to reset shortcuts') + }, [resetToDefaults]) + + const lowerQuery = query.toLowerCase() + const grouped = getGroupedShortcuts() + + const filteredGroups: [string, ShortcutEntry[]][] = CATEGORY_ORDER.flatMap((cat) => { + const entries = grouped.get(cat) ?? [] + const filtered = query + ? entries.filter( + (e) => + e.label.toLowerCase().includes(lowerQuery) || + e.description.toLowerCase().includes(lowerQuery) + ) + : entries + return filtered.length > 0 ? [[cat, filtered] as [string, ShortcutEntry[]]] : [] + }) + + const hasCustomBindings = Object.keys(overrides).length > 0 + + if (isLoading) { + return ( +
+
+

Keyboard Shortcuts

+

Loading settings...

+
+
+ ) + } + + return ( +
+
+
+

Keyboard Shortcuts

+

+ Click any shortcut to rebind it. Press Escape to cancel. +

+
+ {hasCustomBindings && ( + + )} +
+ + + + {/* Search */} +
+ + setQuery(e.target.value)} + className="pl-8" + /> +
+ + {filteredGroups.length === 0 && ( +

+ No shortcuts match your search +

+ )} + + {filteredGroups.map(([category, entries]) => ( +
+

+ {category} +

+
+ {entries.map((entry) => { + const effectiveBinding = resolveBinding(entry, overrides) + const isDefault = !overrides[entry.id] + return ( + + ) + })} +
+
+ ))} +
+ ) +} From 5d7a022d60b806cf1b8d173ab6fe56337f935992 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 22:34:55 +0300 Subject: [PATCH 03/80] feat(settings): implement account section (T029-T033) - account-handlers.ts: GET_INFO (email+joinedAt from store/DB) + SIGN_OUT - Register account handlers in IPC index; regenerate invoke map - Preload: add account namespace + update index.d.ts types - useAccountInfo hook (GET_INFO), useDevices hook (wraps syncDevices) - AccountSection: identity card, storage usage bar, recovery key stub, sign-out dialog - Wire AccountSection into settings.tsx sidebar Co-Authored-By: Paperclip --- apps/desktop/src/main/ipc/account-handlers.ts | 67 ++++++ .../src/main/ipc/generated-ipc-invoke-map.ts | 188 +++++----------- apps/desktop/src/main/ipc/index.ts | 5 + apps/desktop/src/preload/index.d.ts | 7 + apps/desktop/src/preload/index.ts | 9 +- .../renderer/src/hooks/use-account-info.ts | 48 ++++ .../src/renderer/src/hooks/use-devices.ts | 76 +++++++ .../src/renderer/src/pages/settings.tsx | 10 + .../src/pages/settings/account-section.tsx | 210 ++++++++++++++++++ 9 files changed, 481 insertions(+), 139 deletions(-) create mode 100644 apps/desktop/src/main/ipc/account-handlers.ts create mode 100644 apps/desktop/src/renderer/src/hooks/use-account-info.ts create mode 100644 apps/desktop/src/renderer/src/hooks/use-devices.ts create mode 100644 apps/desktop/src/renderer/src/pages/settings/account-section.tsx diff --git a/apps/desktop/src/main/ipc/account-handlers.ts b/apps/desktop/src/main/ipc/account-handlers.ts new file mode 100644 index 000000000..6767ce0af --- /dev/null +++ b/apps/desktop/src/main/ipc/account-handlers.ts @@ -0,0 +1,67 @@ +/** + * Account IPC Handlers + * + * Handles account-level IPC requests: account info, sign-out. + * Device management (list/remove) uses existing SYNC_CHANNELS in sync-handlers. + * + * @module main/ipc/account-handlers + */ + +import { ipcMain } from 'electron' +import { AccountChannels } from '@memry/contracts/ipc-channels' +import { asc } from 'drizzle-orm' +import { syncDevices } from '@memry/db-schema/schema/sync-devices' +import { createLogger } from '../lib/logger' +import { getDatabase, isDatabaseInitialized } from '../database/client' +import { store } from '../store' +import { teardownSession } from '../sync/session-teardown' + +const log = createLogger('IPC:Account') + +export interface AccountInfo { + email: string | null + joinedAt: number | null +} + +function getAccountInfo(): AccountInfo { + const email = store.get('sync').email ?? null + + let joinedAt: number | null = null + if (isDatabaseInitialized()) { + const db = getDatabase() + const earliest = db + .select({ linkedAt: syncDevices.linkedAt }) + .from(syncDevices) + .orderBy(asc(syncDevices.linkedAt)) + .limit(1) + .get() + if (earliest) { + joinedAt = earliest.linkedAt.getTime() + } + } + + return { email, joinedAt } +} + +export function registerAccountHandlers(): void { + ipcMain.handle(AccountChannels.invoke.GET_INFO, () => { + log.info('account:getInfo requested') + return getAccountInfo() + }) + + ipcMain.handle(AccountChannels.invoke.SIGN_OUT, async () => { + log.info('account:signOut requested') + const result = await teardownSession('logout') + return { + success: true, + ...(result.keychainFailures.length > 0 && { + keychainWarning: `Failed to remove: ${result.keychainFailures.join(', ')}` + }) + } + }) +} + +export function unregisterAccountHandlers(): void { + ipcMain.removeHandler(AccountChannels.invoke.GET_INFO) + ipcMain.removeHandler(AccountChannels.invoke.SIGN_OUT) +} diff --git a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts index 35fa8dd61..af406eb8a 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -2,6 +2,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export interface MainIpcInvokeHandlers { + 'account:getInfo': (...args: []) => Awaited + 'account:signOut': ( + ...args: [] + ) => Awaited> 'ai-inline:get-server-port': (...args: []) => Awaited 'ai-inline:get-settings': ( ...args: [] @@ -26,9 +30,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited> 'auth:request-otp': (...args: [{ email: string }]) => Awaited> 'auth:resend-otp': (...args: [{ email: string }]) => Awaited> - 'auth:verify-otp': ( - ...args: [{ email: string; code: string }] - ) => Awaited< + 'auth:verify-otp': (...args: [{ email: string; code: string }]) => Awaited< Promise<{ success: boolean isNewUser: boolean @@ -42,9 +44,7 @@ export interface MainIpcInvokeHandlers { 'bookmarks:bulk-delete': ( ...args: [{ bookmarkIds: string[] }] ) => Awaited> - 'bookmarks:create': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited< + 'bookmarks:create': (...args: [{ itemType: string; itemId: string }]) => Awaited< Promise< | { success: boolean; bookmark: null; error: string } | { @@ -65,9 +65,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> > - 'bookmarks:get': ( - ...args: [string] - ) => Awaited< + 'bookmarks:get': (...args: [string]) => Awaited< Promise<{ id: string createdAt: string @@ -76,9 +74,7 @@ export interface MainIpcInvokeHandlers { itemId: string } | null> > - 'bookmarks:get-by-item': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited< + 'bookmarks:get-by-item': (...args: [{ itemType: string; itemId: string }]) => Awaited< Promise<{ id: string createdAt: string @@ -111,9 +107,7 @@ export interface MainIpcInvokeHandlers { 'bookmarks:reorder': ( ...args: [{ bookmarkIds: string[] }] ) => Awaited> - 'bookmarks:toggle': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited< + 'bookmarks:toggle': (...args: [{ itemType: string; itemId: string }]) => Awaited< Promise<{ success: boolean isBookmarked: boolean @@ -380,9 +374,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise > - 'graph:get-graph-data': ( - ...args: [] - ) => Awaited<{ + 'graph:get-graph-data': (...args: []) => Awaited<{ nodes: { id: string type: 'note' | 'task' | 'project' | 'journal' @@ -403,9 +395,7 @@ export interface MainIpcInvokeHandlers { weight: number }[] }> - 'graph:get-local-graph': ( - ...args: [{ noteId: string; depth?: number | undefined }] - ) => Awaited<{ + 'graph:get-local-graph': (...args: [{ noteId: string; depth?: number | undefined }]) => Awaited<{ nodes: { id: string type: 'note' | 'task' | 'project' | 'journal' @@ -441,9 +431,7 @@ export interface MainIpcInvokeHandlers { 'inbox:bulk-file': ( ...args: [any] ) => Awaited> - 'inbox:bulk-snooze': ( - ...args: [any] - ) => Awaited< + 'inbox:bulk-snooze': (...args: [any]) => Awaited< Promise<{ success: boolean processedCount: number @@ -577,9 +565,7 @@ export interface MainIpcInvokeHandlers { > 'journal:deleteEntry': (...args: [{ date: string }]) => Awaited> 'journal:getAllTags': (...args: []) => Awaited> - 'journal:getDayContext': ( - ...args: [{ date: string }] - ) => Awaited< + 'journal:getDayContext': (...args: [{ date: string }]) => Awaited< Promise<{ date: string tasks: { @@ -599,9 +585,7 @@ export interface MainIpcInvokeHandlers { overdueCount: number }> > - 'journal:getEntry': ( - ...args: [{ date: string }] - ) => Awaited< + 'journal:getEntry': (...args: [{ date: string }]) => Awaited< Promise<{ id: string date: string @@ -617,9 +601,7 @@ export interface MainIpcInvokeHandlers { 'journal:getHeatmap': ( ...args: [{ year: number }] ) => Awaited> - 'journal:getMonthEntries': ( - ...args: [{ year: number; month: number }] - ) => Awaited< + 'journal:getMonthEntries': (...args: [{ year: number; month: number }]) => Awaited< Promise< { date: string @@ -636,9 +618,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ currentStreak: number; longestStreak: number; lastEntryDate: string | null }> > - 'journal:getYearStats': ( - ...args: [{ year: number }] - ) => Awaited< + 'journal:getYearStats': (...args: [{ year: number }]) => Awaited< Promise< { year: number @@ -793,9 +773,7 @@ export interface MainIpcInvokeHandlers { ...args: [string] ) => Awaited> 'notes:get-local-only-count': (...args: []) => Awaited> - 'notes:get-positions': ( - ...args: [{ folderPath: string }] - ) => Awaited< + 'notes:get-positions': (...args: [{ folderPath: string }]) => Awaited< Promise< | { success: boolean @@ -805,9 +783,7 @@ export interface MainIpcInvokeHandlers { | { success: boolean; positions: never[]; error: string } > > - 'notes:get-property-definitions': ( - ...args: [] - ) => Awaited< + 'notes:get-property-definitions': (...args: []) => Awaited< Promise< { type: string @@ -873,9 +849,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> > - 'notes:resolve-by-title': ( - ...args: [string] - ) => Awaited< + 'notes:resolve-by-title': (...args: [string]) => Awaited< Promise<{ id: string path: string @@ -1027,9 +1001,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> > - 'reminder:dismiss': ( - ...args: [string] - ) => Awaited< + 'reminder:dismiss': (...args: [string]) => Awaited< Promise< | { success: boolean; reminder: null; error: string } | { @@ -1052,9 +1024,7 @@ export interface MainIpcInvokeHandlers { 'reminder:get-for-target': ( ...args: [{ targetType: 'note' | 'journal' | 'highlight'; targetId: string }] ) => Awaited> - 'reminder:get-upcoming': ( - ...args: [number | undefined] - ) => Awaited< + 'reminder:get-upcoming': (...args: [number | undefined]) => Awaited< Promise<{ reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] total: number @@ -1086,9 +1056,7 @@ export interface MainIpcInvokeHandlers { hasMore: boolean }> > - 'reminder:snooze': ( - ...args: [{ id: string; snoozeUntil: string }] - ) => Awaited< + 'reminder:snooze': (...args: [{ id: string; snoozeUntil: string }]) => Awaited< Promise< | { success: boolean; reminder: null; error: string } | { @@ -1168,9 +1136,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> > - 'saved-filters:list': ( - ...args: [] - ) => Awaited< + 'saved-filters:list': (...args: []) => Awaited< Promise<{ savedFilters: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter[] }> @@ -1278,9 +1244,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise > - 'search:rebuild-index': ( - ...args: [] - ) => Awaited< + 'search:rebuild-index': (...args: []) => Awaited< Promise< | { notes: number @@ -1298,26 +1262,20 @@ export interface MainIpcInvokeHandlers { ...args: [] ) => Awaited> 'settings:getAISettings': (...args: []) => Awaited - 'settings:getBackupSettings': ( - ...args: [] - ) => Awaited<{ + 'settings:getBackupSettings': (...args: []) => Awaited<{ autoBackup: boolean frequencyHours: 1 | 6 | 12 | 24 maxBackups: number lastBackupAt: string | null }> - 'settings:getEditorSettings': ( - ...args: [] - ) => Awaited<{ + 'settings:getEditorSettings': (...args: []) => Awaited<{ width: 'medium' | 'narrow' | 'wide' spellCheck: boolean autoSaveDelay: number showWordCount: boolean toolbarMode: 'floating' | 'sticky' }> - 'settings:getGeneralSettings': ( - ...args: [] - ) => Awaited<{ + 'settings:getGeneralSettings': (...args: []) => Awaited<{ theme: 'light' | 'dark' | 'white' | 'system' fontSize: 'small' | 'medium' | 'large' fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' @@ -1326,27 +1284,21 @@ export interface MainIpcInvokeHandlers { language: string onboardingCompleted: boolean }> - 'settings:getGraphSettings': ( - ...args: [] - ) => Awaited<{ + 'settings:getGraphSettings': (...args: []) => Awaited<{ layout: 'forceatlas2' | 'circular' | 'random' showLabels: boolean showEdgeLabels: boolean animateLayout: boolean showTagEdges: boolean }> - 'settings:getJournalSettings': ( - ...args: [] - ) => Awaited<{ + 'settings:getJournalSettings': (...args: []) => Awaited<{ defaultTemplate: string | null showSchedule: boolean showTasks: boolean showAIConnections: boolean showStatsFooter: boolean }> - 'settings:getKeyboardSettings': ( - ...args: [] - ) => Awaited<{ + 'settings:getKeyboardSettings': (...args: []) => Awaited<{ overrides: Record< string, { @@ -1374,9 +1326,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited 'settings:getSyncSettings': (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean }> 'settings:getTabSettings': (...args: []) => Awaited - 'settings:getTaskSettings': ( - ...args: [] - ) => Awaited<{ + 'settings:getTaskSettings': (...args: []) => Awaited<{ defaultProjectId: string | null defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' weekStartDay: 'sunday' | 'monday' @@ -1527,9 +1477,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise > - 'sync:get-devices': ( - ...args: [] - ) => Awaited< + 'sync:get-devices': (...args: []) => Awaited< Promise<{ devices: { id: string @@ -1542,9 +1490,7 @@ export interface MainIpcInvokeHandlers { email: string | undefined }> > - 'sync:get-download-progress': ( - ...args: [{ attachmentId: string }] - ) => Awaited< + 'sync:get-download-progress': (...args: [{ attachmentId: string }]) => Awaited< Promise<{ progress: number downloadedChunks: number @@ -1589,9 +1535,7 @@ export interface MainIpcInvokeHandlers { import('../../../../../packages/contracts/src/ipc-sync-ops').StorageBreakdownResult | null > > - 'sync:get-synced-settings': ( - ...args: [] - ) => Awaited<{ + 'sync:get-synced-settings': (...args: []) => Awaited<{ general?: | { theme?: 'light' | 'dark' | 'white' | 'system' | undefined @@ -1631,9 +1575,7 @@ export interface MainIpcInvokeHandlers { | undefined sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined } | undefined } | null> - 'sync:get-upload-progress': ( - ...args: [{ sessionId: string }] - ) => Awaited< + 'sync:get-upload-progress': (...args: [{ sessionId: string }]) => Awaited< Promise<{ progress: number uploadedChunks: number @@ -1791,9 +1733,7 @@ export interface MainIpcInvokeHandlers { | { success: boolean; count: number; error: string } > > - 'tasks:complete': ( - ...args: [{ id: string; completedAt?: string | undefined }] - ) => Awaited< + 'tasks:complete': (...args: [{ id: string; completedAt?: string | undefined }]) => Awaited< Promise< | { success: boolean; task: null; error: string } | { @@ -1826,9 +1766,7 @@ export interface MainIpcInvokeHandlers { } > > - 'tasks:convert-to-subtask': ( - ...args: [{ taskId: string; parentId: string }] - ) => Awaited< + 'tasks:convert-to-subtask': (...args: [{ taskId: string; parentId: string }]) => Awaited< Promise< | { success: boolean; task: null; error: string } | { @@ -1861,9 +1799,7 @@ export interface MainIpcInvokeHandlers { } > > - 'tasks:convert-to-task': ( - ...args: [string] - ) => Awaited< + 'tasks:convert-to-task': (...args: [string]) => Awaited< Promise< | { success: boolean; task: null; error: string } | { @@ -1971,9 +1907,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> > - 'tasks:duplicate': ( - ...args: [string] - ) => Awaited< + 'tasks:duplicate': (...args: [string]) => Awaited< Promise< | { success: boolean; task: null; error: string } | { @@ -2006,9 +1940,7 @@ export interface MainIpcInvokeHandlers { } > > - 'tasks:get': ( - ...args: [string] - ) => Awaited< + 'tasks:get': (...args: [string]) => Awaited< Promise<{ tags: string[] linkedNoteIds: string[] @@ -2038,9 +1970,7 @@ export interface MainIpcInvokeHandlers { completedAt: string | null } | null> > - 'tasks:get-linked-tasks': ( - ...args: [string] - ) => Awaited< + 'tasks:get-linked-tasks': (...args: [string]) => Awaited< Promise< { tags: string[] @@ -2069,9 +1999,7 @@ export interface MainIpcInvokeHandlers { }[] > > - 'tasks:get-overdue': ( - ...args: [] - ) => Awaited< + 'tasks:get-overdue': (...args: []) => Awaited< Promise<{ tasks: { linkedNoteIds: string[] @@ -2101,9 +2029,7 @@ export interface MainIpcInvokeHandlers { hasMore: boolean }> > - 'tasks:get-stats': ( - ...args: [] - ) => Awaited< + 'tasks:get-stats': (...args: []) => Awaited< Promise<{ total: number completed: number @@ -2112,9 +2038,7 @@ export interface MainIpcInvokeHandlers { dueThisWeek: number }> > - 'tasks:get-subtasks': ( - ...args: [string] - ) => Awaited< + 'tasks:get-subtasks': (...args: [string]) => Awaited< Promise< { id: string @@ -2142,9 +2066,7 @@ export interface MainIpcInvokeHandlers { > > 'tasks:get-tags': (...args: []) => Awaited> - 'tasks:get-today': ( - ...args: [] - ) => Awaited< + 'tasks:get-today': (...args: []) => Awaited< Promise<{ tasks: { linkedNoteIds: string[] @@ -2174,9 +2096,7 @@ export interface MainIpcInvokeHandlers { hasMore: boolean }> > - 'tasks:get-upcoming': ( - ...args: [{ days?: number | undefined }] - ) => Awaited< + 'tasks:get-upcoming': (...args: [{ days?: number | undefined }]) => Awaited< Promise<{ tasks: { linkedNoteIds: string[] @@ -2443,9 +2363,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> > - 'tasks:status-list': ( - ...args: [string] - ) => Awaited< + 'tasks:status-list': (...args: [string]) => Awaited< Promise< { id: string @@ -2499,9 +2417,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> > - 'tasks:uncomplete': ( - ...args: [string] - ) => Awaited< + 'tasks:uncomplete': (...args: [string]) => Awaited< Promise< | { success: boolean; task: null; error: string } | { @@ -2644,9 +2560,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> > - 'templates:duplicate': ( - ...args: [{ id: string; newName: string }] - ) => Awaited< + 'templates:duplicate': (...args: [{ id: string; newName: string }]) => Awaited< Promise< | { success: boolean @@ -2661,9 +2575,7 @@ export interface MainIpcInvokeHandlers { ) => Awaited< Promise > - 'templates:list': ( - ...args: [] - ) => Awaited< + 'templates:list': (...args: []) => Awaited< Promise<{ templates: import('../../../../../packages/contracts/src/templates-api').TemplateListItem[] }> diff --git a/apps/desktop/src/main/ipc/index.ts b/apps/desktop/src/main/ipc/index.ts index 9400f51bc..d27e1b44d 100644 --- a/apps/desktop/src/main/ipc/index.ts +++ b/apps/desktop/src/main/ipc/index.ts @@ -19,6 +19,7 @@ import { registerCryptoHandlers, unregisterCryptoHandlers } from './crypto-handl import { registerSearchHandlers, unregisterSearchHandlers } from './search-handlers' import { registerGraphHandlers, unregisterGraphHandlers } from './graph-handlers' import { registerAIInlineHandlers, unregisterAIInlineHandlers } from './ai-inline-handlers' +import { registerAccountHandlers, unregisterAccountHandlers } from './account-handlers' import { createLogger } from '../lib/logger' const ipcLog = createLogger('IPC') @@ -101,6 +102,9 @@ export function registerAllHandlers(): void { // Register AI inline editing handlers registerAIInlineHandlers() + // Register account handlers + registerAccountHandlers() + handlersRegistered = true ipcLog.info('all handlers registered') } @@ -132,6 +136,7 @@ export function unregisterAllHandlers(): void { unregisterSearchHandlers() unregisterGraphHandlers() unregisterAIInlineHandlers() + unregisterAccountHandlers() handlersRegistered = false ipcLog.info('all handlers unregistered') diff --git a/apps/desktop/src/preload/index.d.ts b/apps/desktop/src/preload/index.d.ts index 3c2e27ef5..bb1851931 100644 --- a/apps/desktop/src/preload/index.d.ts +++ b/apps/desktop/src/preload/index.d.ts @@ -2249,6 +2249,12 @@ interface SyncLinkingClientAPI { }> } +// Account API +interface AccountClientAPI { + getInfo: () => Promise<{ email: string | null; joinedAt: number | null }> + signOut: () => Promise<{ success: boolean; keychainWarning?: string }> +} + // Device Management API interface SyncDevicesClientAPI { getDevices: () => Promise<{ @@ -2443,6 +2449,7 @@ interface API extends WindowAPI { syncAuth: SyncAuthClientAPI syncSetup: SyncSetupClientAPI syncLinking: SyncLinkingClientAPI + account: AccountClientAPI syncDevices: SyncDevicesClientAPI syncOps: SyncOpsClientAPI crypto: CryptoClientAPI diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 2ee6ebef9..3d6a68843 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -17,7 +17,8 @@ import { FolderViewChannels, PropertiesChannels, SearchChannels, - GraphChannels + GraphChannels, + AccountChannels } from '@memry/contracts/ipc-channels' import { SYNC_CHANNELS, SYNC_EVENTS } from '@memry/contracts/ipc-sync' import type { @@ -1492,6 +1493,12 @@ export const api = { invoke(SYNC_CHANNELS.COMPLETE_LINKING_QR, input) }, + // Account API + account: { + getInfo: () => invoke(AccountChannels.invoke.GET_INFO), + signOut: () => invoke(AccountChannels.invoke.SIGN_OUT) + }, + // Device Management API syncDevices: { getDevices: () => invoke(SYNC_CHANNELS.GET_DEVICES), diff --git a/apps/desktop/src/renderer/src/hooks/use-account-info.ts b/apps/desktop/src/renderer/src/hooks/use-account-info.ts new file mode 100644 index 000000000..111e3faa5 --- /dev/null +++ b/apps/desktop/src/renderer/src/hooks/use-account-info.ts @@ -0,0 +1,48 @@ +import { useState, useEffect } from 'react' +import { extractErrorMessage } from '@/lib/ipc-error' + +export interface AccountInfo { + email: string | null + joinedAt: number | null +} + +interface UseAccountInfoReturn { + accountInfo: AccountInfo | null + isLoading: boolean + error: string | null + refresh: () => void +} + +export function useAccountInfo(): UseAccountInfoReturn { + const [accountInfo, setAccountInfo] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [refreshKey, setRefreshKey] = useState(0) + + useEffect(() => { + let mounted = true + const load = async (): Promise => { + try { + setIsLoading(true) + setError(null) + const result = await window.api.account.getInfo() + if (mounted) setAccountInfo(result) + } catch (err) { + if (mounted) setError(extractErrorMessage(err, 'Failed to load account info')) + } finally { + if (mounted) setIsLoading(false) + } + } + void load() + return () => { + mounted = false + } + }, [refreshKey]) + + return { + accountInfo, + isLoading, + error, + refresh: () => setRefreshKey((k) => k + 1) + } +} diff --git a/apps/desktop/src/renderer/src/hooks/use-devices.ts b/apps/desktop/src/renderer/src/hooks/use-devices.ts new file mode 100644 index 000000000..851c1831b --- /dev/null +++ b/apps/desktop/src/renderer/src/hooks/use-devices.ts @@ -0,0 +1,76 @@ +import { useState, useEffect, useCallback } from 'react' +import { extractErrorMessage } from '@/lib/ipc-error' +import { deviceService } from '@/services/device-service' + +export interface Device { + id: string + name: string + platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android' + linkedAt: number + lastSyncAt?: number + isCurrentDevice: boolean +} + +interface UseDevicesReturn { + devices: Device[] + email: string | undefined + isLoading: boolean + error: string | null + removeDevice: (deviceId: string) => Promise + refresh: () => void +} + +export function useDevices(): UseDevicesReturn { + const [devices, setDevices] = useState([]) + const [email, setEmail] = useState(undefined) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [refreshKey, setRefreshKey] = useState(0) + + useEffect(() => { + let mounted = true + const load = async (): Promise => { + try { + setIsLoading(true) + setError(null) + const result = await deviceService.getDevices() + if (mounted) { + setDevices(result.devices as Device[]) + setEmail(result.email) + } + } catch (err) { + if (mounted) setError(extractErrorMessage(err, 'Failed to load devices')) + } finally { + if (mounted) setIsLoading(false) + } + } + void load() + return () => { + mounted = false + } + }, [refreshKey]) + + const removeDevice = useCallback(async (deviceId: string): Promise => { + try { + const result = await deviceService.removeDevice({ deviceId }) + if (result.success) { + setDevices((prev) => prev.filter((d) => d.id !== deviceId)) + return true + } + setError(result.error ?? 'Failed to remove device') + return false + } catch (err) { + setError(extractErrorMessage(err, 'Failed to remove device')) + return false + } + }, []) + + return { + devices, + email, + isLoading, + error, + removeDevice, + refresh: () => setRefreshKey((k) => k + 1) + } +} diff --git a/apps/desktop/src/renderer/src/pages/settings.tsx b/apps/desktop/src/renderer/src/pages/settings.tsx index de2de7699..c243d709a 100644 --- a/apps/desktop/src/renderer/src/pages/settings.tsx +++ b/apps/desktop/src/renderer/src/pages/settings.tsx @@ -28,6 +28,8 @@ import { IntegrationsSettings } from './settings/integrations-section' import { TagsSettings } from './settings/tags-section' import { TasksSettings } from './settings/tasks-section' import { ShortcutsSettings } from './settings/shortcuts-section' +import { AccountSettings } from './settings/account-section' +import { User } from '@/lib/icons' type SettingsSection = | 'general' @@ -42,6 +44,7 @@ type SettingsSection = | 'integrations' | 'tags' | 'shortcuts' + | 'account' export function SettingsPage() { const [activeSection, setActiveSection] = useState(() => { @@ -121,6 +124,12 @@ export function SettingsPage() { isActive={activeSection === 'ai'} onClick={() => setActiveSection('ai')} /> + } + label="Account" + isActive={activeSection === 'account'} + onClick={() => setActiveSection('account')} + /> } label="Sync" @@ -164,6 +173,7 @@ export function SettingsPage() { {activeSection === 'integrations' && } {activeSection === 'tags' && } {activeSection === 'shortcuts' && } + {activeSection === 'account' && }
diff --git a/apps/desktop/src/renderer/src/pages/settings/account-section.tsx b/apps/desktop/src/renderer/src/pages/settings/account-section.tsx new file mode 100644 index 000000000..03d6abf53 --- /dev/null +++ b/apps/desktop/src/renderer/src/pages/settings/account-section.tsx @@ -0,0 +1,210 @@ +import { useState, useEffect, useCallback } from 'react' +import { Button } from '@/components/ui/button' +import { Separator } from '@/components/ui/separator' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog' +import { User, LogOut, HardDrive, CalendarDays, Key } from '@/lib/icons' +import { toast } from 'sonner' +import { format } from 'date-fns' +import { extractErrorMessage } from '@/lib/ipc-error' +import { useAuth } from '@/contexts/auth-context' +import { useAccountInfo } from '@/hooks/use-account-info' +import type { StorageBreakdownResult } from '@memry/contracts/ipc-sync-ops' + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +export function AccountSettings() { + const { state, logout } = useAuth() + const { accountInfo, isLoading: infoLoading } = useAccountInfo() + const [storage, setStorage] = useState(null) + const [showSignOutDialog, setShowSignOutDialog] = useState(false) + const [signingOut, setSigningOut] = useState(false) + + useEffect(() => { + if (state.status !== 'authenticated') return + window.api.syncOps + .getStorageBreakdown() + .then(setStorage) + .catch(() => null) + }, [state.status]) + + const handleSignOut = useCallback(async () => { + setSigningOut(true) + try { + await logout() + toast.success('Signed out successfully') + } catch (error: unknown) { + toast.error(extractErrorMessage(error, 'Failed to sign out')) + } finally { + setSigningOut(false) + setShowSignOutDialog(false) + } + }, [logout]) + + if (state.status === 'checking' || infoLoading) { + return ( +
+
+

Account

+

Loading...

+
+
+ ) + } + + if (state.status !== 'authenticated') { + return ( +
+
+

Account

+

Not signed in

+
+ +

+ Sign in via the Sync section to access account settings. +

+
+ ) + } + + const email = accountInfo?.email ?? state.email + const joinedAt = accountInfo?.joinedAt + + const storageUsedPct = + storage && storage.limit > 0 ? Math.min(100, (storage.used / storage.limit) * 100) : null + + return ( +
+
+

Account

+

Manage your account and sign out

+
+ + + + {/* Identity */} +
+

Identity

+
+
+ +
+
+

{email ?? 'Unknown'}

+ {joinedAt && ( +
+ + Member since {format(new Date(joinedAt), 'MMMM yyyy')} +
+ )} +
+
+
+ + {/* Storage */} + {storage && ( + <> + +
+

+ + Storage +

+
+
+ {formatBytes(storage.used)} used + {formatBytes(storage.limit)} total +
+
+
+
+
+ {Object.entries(storage.breakdown).map(([key, bytes]) => ( +
+

{formatBytes(bytes)}

+

{key}

+
+ ))} +
+
+
+ + )} + + + + {/* Actions */} +
+

Account actions

+
+
+
+

Recovery key

+

+ View your encrypted recovery key after re-authentication +

+
+ +
+
+
+ + + +
+ +

+ Your notes stay on this device. Sync will stop until you sign in again. +

+
+ + + + + Sign out of sync? + + Sync will stop and encryption keys will be removed from this device. Your notes will + remain. You'll need your recovery phrase to set up sync again. + + + + Cancel + void handleSignOut()} + disabled={signingOut} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {signingOut ? 'Signing out...' : 'Sign out'} + + + + +
+ ) +} From 9fc5185445ac7a36d46799bdee0a4b1d0e8db67d Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 22:42:01 +0300 Subject: [PATCH 04/80] feat(settings): implement recovery key dialog (T034-T036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - account-handlers: add GET_RECOVERY_KEY handler — retrieves master key from OS keychain (requires valid access token), returns base64 encoded - RecoveryKeyDialog: blurred display until reveal, copy button, auto-clear on close - AccountSection: wire View button → RecoveryKeyDialog, remove disabled state Co-Authored-By: Paperclip --- apps/desktop/src/main/ipc/account-handlers.ts | 24 +++ .../src/main/ipc/generated-ipc-invoke-map.ts | 8 + apps/desktop/src/preload/index.d.ts | 1 + apps/desktop/src/preload/index.ts | 3 +- .../settings/recovery-key-dialog.tsx | 143 ++++++++++++++++++ .../src/pages/settings/account-section.tsx | 11 +- 6 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/renderer/src/components/settings/recovery-key-dialog.tsx diff --git a/apps/desktop/src/main/ipc/account-handlers.ts b/apps/desktop/src/main/ipc/account-handlers.ts index 6767ce0af..0cc9ad21d 100644 --- a/apps/desktop/src/main/ipc/account-handlers.ts +++ b/apps/desktop/src/main/ipc/account-handlers.ts @@ -8,13 +8,17 @@ */ import { ipcMain } from 'electron' +import sodium from 'libsodium-wrappers-sumo' import { AccountChannels } from '@memry/contracts/ipc-channels' +import { KEYCHAIN_ENTRIES } from '@memry/contracts/crypto' import { asc } from 'drizzle-orm' import { syncDevices } from '@memry/db-schema/schema/sync-devices' import { createLogger } from '../lib/logger' import { getDatabase, isDatabaseInitialized } from '../database/client' import { store } from '../store' import { teardownSession } from '../sync/session-teardown' +import { retrieveKey } from '../crypto' +import { getValidAccessToken } from '../sync/token-manager' const log = createLogger('IPC:Account') @@ -59,9 +63,29 @@ export function registerAccountHandlers(): void { }) } }) + + ipcMain.handle(AccountChannels.invoke.GET_RECOVERY_KEY, async () => { + log.info('account:getRecoveryKey requested') + const token = await getValidAccessToken() + if (!token) { + return { success: false, error: 'Not authenticated' } + } + try { + const masterKey = await retrieveKey(KEYCHAIN_ENTRIES.MASTER_KEY) + if (!masterKey) { + return { success: false, error: 'Recovery key not available on this device' } + } + const encoded = sodium.to_base64(masterKey, sodium.base64_variants.URLSAFE_NO_PADDING) + return { success: true, key: encoded } + } catch (err) { + log.error('Failed to retrieve recovery key', err) + return { success: false, error: 'Failed to retrieve recovery key' } + } + }) } export function unregisterAccountHandlers(): void { ipcMain.removeHandler(AccountChannels.invoke.GET_INFO) ipcMain.removeHandler(AccountChannels.invoke.SIGN_OUT) + ipcMain.removeHandler(AccountChannels.invoke.GET_RECOVERY_KEY) } diff --git a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts index af406eb8a..d12470f52 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -3,6 +3,14 @@ export interface MainIpcInvokeHandlers { 'account:getInfo': (...args: []) => Awaited + 'account:getRecoveryKey': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; error: string; key?: undefined } + | { success: boolean; key: string; error?: undefined } + > + > 'account:signOut': ( ...args: [] ) => Awaited> diff --git a/apps/desktop/src/preload/index.d.ts b/apps/desktop/src/preload/index.d.ts index bb1851931..cfa7e723f 100644 --- a/apps/desktop/src/preload/index.d.ts +++ b/apps/desktop/src/preload/index.d.ts @@ -2253,6 +2253,7 @@ interface SyncLinkingClientAPI { interface AccountClientAPI { getInfo: () => Promise<{ email: string | null; joinedAt: number | null }> signOut: () => Promise<{ success: boolean; keychainWarning?: string }> + getRecoveryKey: () => Promise<{ success: boolean; key?: string; error?: string }> } // Device Management API diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 3d6a68843..80af7084b 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -1496,7 +1496,8 @@ export const api = { // Account API account: { getInfo: () => invoke(AccountChannels.invoke.GET_INFO), - signOut: () => invoke(AccountChannels.invoke.SIGN_OUT) + signOut: () => invoke(AccountChannels.invoke.SIGN_OUT), + getRecoveryKey: () => invoke(AccountChannels.invoke.GET_RECOVERY_KEY) }, // Device Management API diff --git a/apps/desktop/src/renderer/src/components/settings/recovery-key-dialog.tsx b/apps/desktop/src/renderer/src/components/settings/recovery-key-dialog.tsx new file mode 100644 index 000000000..1bafe1f6f --- /dev/null +++ b/apps/desktop/src/renderer/src/components/settings/recovery-key-dialog.tsx @@ -0,0 +1,143 @@ +import { useState, useCallback } from 'react' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Copy, Eye, EyeOff, Key } from '@/lib/icons' +import { toast } from 'sonner' +import { extractErrorMessage } from '@/lib/ipc-error' + +interface RecoveryKeyDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function RecoveryKeyDialog({ open, onOpenChange }: RecoveryKeyDialogProps) { + const [recoveryKey, setRecoveryKey] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [revealed, setRevealed] = useState(false) + + const handleOpen = useCallback( + async (nextOpen: boolean) => { + if (!nextOpen) { + setRecoveryKey(null) + setRevealed(false) + onOpenChange(false) + return + } + setIsLoading(true) + onOpenChange(true) + try { + const result = await window.api.account.getRecoveryKey() + if (!result.success || !result.key) { + toast.error(result.error ?? 'Failed to retrieve recovery key') + onOpenChange(false) + return + } + setRecoveryKey(result.key) + } catch (err) { + toast.error(extractErrorMessage(err, 'Failed to retrieve recovery key')) + onOpenChange(false) + } finally { + setIsLoading(false) + } + }, + [onOpenChange] + ) + + const handleCopy = useCallback(async () => { + if (!recoveryKey) return + try { + await navigator.clipboard.writeText(recoveryKey) + toast.success('Recovery key copied to clipboard') + } catch { + toast.error('Failed to copy to clipboard') + } + }, [recoveryKey]) + + return ( + void handleOpen(next)}> + + +
+ + Recovery Key +
+ + Store this key securely. It can restore your vault if you lose access to all devices. + +
+ +
+ {isLoading ? ( +
+

Loading...

+
+ ) : recoveryKey ? ( +
+
+
setRevealed(true)} + onKeyDown={(e) => e.key === 'Enter' && setRevealed(true)} + role={revealed ? undefined : 'button'} + tabIndex={revealed ? undefined : 0} + > + {recoveryKey} +
+ {!revealed && ( +
+
+ + Click to reveal +
+
+ )} +
+ +
+ + +
+ +

+ This key is shown once per session. Close this dialog to clear it from memory. +

+
+ ) : null} +
+
+
+ ) +} diff --git a/apps/desktop/src/renderer/src/pages/settings/account-section.tsx b/apps/desktop/src/renderer/src/pages/settings/account-section.tsx index 03d6abf53..0923c182f 100644 --- a/apps/desktop/src/renderer/src/pages/settings/account-section.tsx +++ b/apps/desktop/src/renderer/src/pages/settings/account-section.tsx @@ -17,6 +17,7 @@ import { format } from 'date-fns' import { extractErrorMessage } from '@/lib/ipc-error' import { useAuth } from '@/contexts/auth-context' import { useAccountInfo } from '@/hooks/use-account-info' +import { RecoveryKeyDialog } from '@/components/settings/recovery-key-dialog' import type { StorageBreakdownResult } from '@memry/contracts/ipc-sync-ops' function formatBytes(bytes: number): string { @@ -30,6 +31,7 @@ export function AccountSettings() { const { accountInfo, isLoading: infoLoading } = useAccountInfo() const [storage, setStorage] = useState(null) const [showSignOutDialog, setShowSignOutDialog] = useState(false) + const [showRecoveryKey, setShowRecoveryKey] = useState(false) const [signingOut, setSigningOut] = useState(false) useEffect(() => { @@ -159,7 +161,12 @@ export function AccountSettings() { View your encrypted recovery key after re-authentication

- @@ -184,6 +191,8 @@ export function AccountSettings() {

+ + From bc4540ef7db6d49def779dbc8a296f6e73324f7b Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 23:11:55 +0300 Subject: [PATCH 05/80] feat(settings): add global capture shortcut configuration - Add REGISTER_GLOBAL_CAPTURE IPC channel to SettingsChannels - Implement applyGlobalCaptureShortcut() in settings-handlers for OS-level global shortcut registration/unregistration via electron globalShortcut - Wire settings keyboard changes to re-apply shortcut live on update - Expose REGISTER_GLOBAL_CAPTURE in preload bridge - Add GlobalCaptureRow component in shortcuts-section with live capture, macOS accessibility permission detection, and clear/reset UI - Update app startup to use applyGlobalCaptureShortcut with fallback to legacy registerQuickCaptureShortcut when no setting saved Co-Authored-By: Paperclip --- apps/desktop/src/main/index.ts | 8 +- .../desktop/src/main/ipc/settings-handlers.ts | 77 ++++++- apps/desktop/src/preload/index.d.ts | 6 + apps/desktop/src/preload/index.ts | 3 +- .../src/pages/settings/shortcuts-section.tsx | 191 +++++++++++++++++- packages/contracts/src/ipc-channels.ts | 4 +- 6 files changed, 281 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 6605ac96d..c74a16e1a 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -18,6 +18,7 @@ import { existsSync, readdirSync } from 'node:fs' import { config } from 'dotenv' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { registerAllHandlers } from './ipc' +import { applyGlobalCaptureShortcut } from './ipc/settings-handlers' import { autoOpenLastVault, closeVault } from './vault' import { getCurrentVaultPath } from './store' import { startSnoozeScheduler, stopSnoozeScheduler, checkDueItemsOnStartup } from './inbox/snooze' @@ -582,8 +583,11 @@ void app.whenReady().then(async () => { .initPersistence() .catch((err) => mainLog.warn('Early CRDT persistence init failed (non-fatal)', err)) - // Register global shortcut for quick capture (Cmd+Shift+Space) - registerQuickCaptureShortcut() + // Register global shortcut for quick capture from keyboard settings (fallback: hardcoded default) + const globalCaptureResult = applyGlobalCaptureShortcut() + if (!globalCaptureResult.registered) { + registerQuickCaptureShortcut() + } // Auto-open the last vault if one was previously open await autoOpenLastVault() diff --git a/apps/desktop/src/main/ipc/settings-handlers.ts b/apps/desktop/src/main/ipc/settings-handlers.ts index 42317f6b4..c1eb7fd7b 100644 --- a/apps/desktop/src/main/ipc/settings-handlers.ts +++ b/apps/desktop/src/main/ipc/settings-handlers.ts @@ -7,7 +7,7 @@ * @module main/ipc/settings-handlers */ -import { ipcMain, BrowserWindow, app } from 'electron' +import { ipcMain, BrowserWindow, app, globalShortcut, systemPreferences } from 'electron' import { SettingsChannels } from '@memry/contracts/ipc-channels' import { GENERAL_SETTINGS_DEFAULTS, @@ -552,8 +552,13 @@ export function registerSettingsHandlers(): void { ) ipcMain.handle( SettingsChannels.invoke.SET_KEYBOARD_SETTINGS, - (_event, updates: Partial) => - writeGroupSettings('keyboard', KEYBOARD_SHORTCUTS_DEFAULTS, updates) + (_event, updates: Partial) => { + const result = writeGroupSettings('keyboard', KEYBOARD_SHORTCUTS_DEFAULTS, updates) + if ('globalCapture' in updates) { + applyGlobalCaptureShortcut() + } + return result + } ) ipcMain.handle(SettingsChannels.invoke.GET_SYNC_SETTINGS, () => @@ -602,9 +607,74 @@ export function registerSettingsHandlers(): void { return { success: true } }) + ipcMain.handle(SettingsChannels.invoke.REGISTER_GLOBAL_CAPTURE, async () => { + return applyGlobalCaptureShortcut() + }) + logger.info('Settings handlers registered') } +// ============================================================================ +// Global Capture Shortcut +// ============================================================================ + +function toElectronAccelerator(binding: { + key: string + modifiers: { meta?: boolean; ctrl?: boolean; shift?: boolean; alt?: boolean } +}): string { + const parts: string[] = [] + if (binding.modifiers.meta) parts.push('CommandOrControl') + if (binding.modifiers.ctrl && !binding.modifiers.meta) parts.push('Control') + if (binding.modifiers.alt) parts.push('Alt') + if (binding.modifiers.shift) parts.push('Shift') + parts.push(binding.key) + return parts.join('+') +} + +export interface GlobalCaptureResult { + success: boolean + registered: boolean + permissionRequired?: boolean + error?: string +} + +/** + * Read keyboard.globalCapture from settings and register/unregister OS shortcut. + * Safe to call at startup and on settings change. + */ +export function applyGlobalCaptureShortcut(): GlobalCaptureResult { + globalShortcut.unregisterAll() + + const settings = readGroupSettings('keyboard', KEYBOARD_SHORTCUTS_DEFAULTS) + const binding = settings.globalCapture + if (!binding) { + return { success: true, registered: false } + } + + if (process.platform === 'darwin') { + const hasPerm = systemPreferences.isTrustedAccessibilityClient(false) + if (!hasPerm) { + logger.warn('Global capture: accessibility permission not granted on macOS') + return { success: false, registered: false, permissionRequired: true } + } + } + + const accelerator = toElectronAccelerator(binding) + const registered = globalShortcut.register(accelerator, () => { + BrowserWindow.getAllWindows().forEach((win) => { + if (!win.isDestroyed()) win.webContents.send('quick-capture:open') + }) + }) + + if (!registered) { + logger.warn(`Global capture: failed to register ${accelerator} (may be in use)`) + return { success: false, registered: false, error: `Shortcut ${accelerator} is already in use` } + } + + logger.info(`Global capture: registered ${accelerator}`) + return { success: true, registered: true } +} + /** * Unregister all settings-related IPC handlers. */ @@ -639,6 +709,7 @@ export function unregisterSettingsHandlers(): void { ipcMain.removeHandler(SettingsChannels.invoke.SET_BACKUP_SETTINGS) ipcMain.removeHandler(SettingsChannels.invoke.GET_GRAPH_SETTINGS) ipcMain.removeHandler(SettingsChannels.invoke.SET_GRAPH_SETTINGS) + ipcMain.removeHandler(SettingsChannels.invoke.REGISTER_GLOBAL_CAPTURE) logger.info('Settings handlers unregistered') } diff --git a/apps/desktop/src/preload/index.d.ts b/apps/desktop/src/preload/index.d.ts index cfa7e723f..b3dd0e5ff 100644 --- a/apps/desktop/src/preload/index.d.ts +++ b/apps/desktop/src/preload/index.d.ts @@ -2157,6 +2157,12 @@ export interface SettingsClientAPI { setGraphSettings( settings: Partial ): Promise<{ success: boolean; error?: string }> + registerGlobalCapture(): Promise<{ + success: boolean + registered: boolean + permissionRequired?: boolean + error?: string + }> } // Sync Auth API diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 80af7084b..3d14263d5 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -574,7 +574,8 @@ export const api = { getGraphSettings: () => invoke(SettingsChannels.invoke.GET_GRAPH_SETTINGS), setGraphSettings: (settings: Record) => - invoke(SettingsChannels.invoke.SET_GRAPH_SETTINGS, settings) + invoke(SettingsChannels.invoke.SET_GRAPH_SETTINGS, settings), + registerGlobalCapture: () => invoke(SettingsChannels.invoke.REGISTER_GLOBAL_CAPTURE) }, // Bookmarks API diff --git a/apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx b/apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx index 69a2ff8d0..85187350a 100644 --- a/apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx +++ b/apps/desktop/src/renderer/src/pages/settings/shortcuts-section.tsx @@ -4,10 +4,11 @@ import { Input } from '@/components/ui/input' import { Separator } from '@/components/ui/separator' import { Kbd, KbdGroup } from '@/components/ui/kbd' import { Badge } from '@/components/ui/badge' -import { Search, RotateCcw, X } from '@/lib/icons' +import { Search, RotateCcw, X, AlertTriangle, Info } from '@/lib/icons' import { useKeyboardSettings } from '@/hooks/use-keyboard-settings' import { toast } from 'sonner' import type { ShortcutBinding } from '@memry/contracts/settings-schemas' +import type { ShortcutBindingDTO } from '../../../../preload/index.d' import { SHORTCUT_REGISTRY, CATEGORY_ORDER, @@ -173,6 +174,180 @@ function ShortcutRow({ ) } +// ============================================================================ +// Global Capture Row +// ============================================================================ + +const PLATFORM = window.navigator.platform.toLowerCase() +const IS_MACOS = PLATFORM.includes('mac') + +function getGlobalCaptureParts(binding: ShortcutBindingDTO): string[] { + const { key, modifiers } = binding + const parts: string[] = [] + if (modifiers.meta) parts.push(IS_MACOS ? '⌘' : 'Ctrl') + if (modifiers.ctrl && !modifiers.meta) parts.push('Ctrl') + if (modifiers.alt) parts.push(IS_MACOS ? '⌥' : 'Alt') + if (modifiers.shift) parts.push(IS_MACOS ? '⇧' : 'Shift') + parts.push(key.toUpperCase()) + return parts +} + +function GlobalCaptureRow({ + binding, + onSave +}: { + binding: ShortcutBindingDTO | null + onSave: (binding: ShortcutBindingDTO | null) => Promise +}): React.JSX.Element { + const [isCapturing, setIsCapturing] = useState(false) + const [permissionStatus, setPermissionStatus] = useState<'unknown' | 'granted' | 'required'>( + 'unknown' + ) + const captureRef = useRef(null) + + const checkAndRegister = useCallback(async () => { + const result = await window.api.settings.registerGlobalCapture() + if (result.permissionRequired) { + setPermissionStatus('required') + } else if (result.registered) { + setPermissionStatus('granted') + } + }, []) + + useEffect(() => { + void checkAndRegister() + }, [checkAndRegister, binding]) + + const startCapture = useCallback(() => setIsCapturing(true), []) + const stopCapture = useCallback(() => setIsCapturing(false), []) + + useEffect(() => { + if (!isCapturing) return + const handleKeyDown = (e: KeyboardEvent): void => { + e.preventDefault() + e.stopPropagation() + if (e.key === 'Escape') { + stopCapture() + return + } + if (['Meta', 'Control', 'Alt', 'Shift'].includes(e.key)) return + const newBinding: ShortcutBindingDTO = { + key: e.key, + modifiers: { + meta: e.metaKey || e.ctrlKey || undefined, + shift: e.shiftKey || undefined, + alt: e.altKey || undefined + } + } + setIsCapturing(false) + void onSave(newBinding) + } + window.addEventListener('keydown', handleKeyDown, { capture: true }) + return () => window.removeEventListener('keydown', handleKeyDown, { capture: true }) + }, [isCapturing, onSave, stopCapture]) + + useEffect(() => { + if (!isCapturing) return + const handleClick = (e: MouseEvent): void => { + if (captureRef.current && !captureRef.current.contains(e.target as Node)) { + stopCapture() + } + } + document.addEventListener('mousedown', handleClick) + return () => document.removeEventListener('mousedown', handleClick) + }, [isCapturing, stopCapture]) + + return ( +
+
+
+
+ Global Capture + {permissionStatus === 'required' && ( + + + Permission needed + + )} + {permissionStatus === 'granted' && binding && ( + + Active + + )} +
+

+ Capture a note from anywhere, even when memry is in the background +

+
+ +
+ {isCapturing ? ( +
+
+ Press shortcut… +
+ +
+ ) : ( +
+ {binding ? ( + + ) : ( + + )} + {binding && ( + + )} +
+ )} +
+
+ + {permissionStatus === 'required' && IS_MACOS && ( +
+ + + Global shortcuts require Accessibility permission on macOS. Go to{' '} + System Settings → Privacy & Security → Accessibility and enable + memry. + +
+ )} +
+ ) +} + // ============================================================================ // Shortcuts Section // ============================================================================ @@ -182,6 +357,15 @@ export function ShortcutsSettings() { const [query, setQuery] = useState('') const overrides = settings.overrides + const globalCapture = settings.globalCapture ?? null + + const handleGlobalCaptureSave = useCallback( + async (binding: ShortcutBindingDTO | null): Promise => { + const success = await updateSettings({ globalCapture: binding }) + if (!success) toast.error('Failed to save global capture shortcut') + }, + [updateSettings] + ) const handleRebind = useCallback( async (id: string, binding: ShortcutBinding): Promise => { @@ -266,6 +450,11 @@ export function ShortcutsSettings() { + {/* Global Capture */} + + + + {/* Search */}
diff --git a/packages/contracts/src/ipc-channels.ts b/packages/contracts/src/ipc-channels.ts index 024c5bbc9..d6a726bc6 100644 --- a/packages/contracts/src/ipc-channels.ts +++ b/packages/contracts/src/ipc-channels.ts @@ -463,7 +463,9 @@ export const SettingsChannels = { /** Reset all settings to defaults */ RESET_ALL: 'settings:resetAll', /** Trigger manual sync */ - TRIGGER_SYNC: 'settings:triggerSync' + TRIGGER_SYNC: 'settings:triggerSync', + /** Register (or unregister) the OS-level global capture shortcut */ + REGISTER_GLOBAL_CAPTURE: 'settings:registerGlobalCapture' }, sync: { /** Get the saved startup theme synchronously for first-paint bootstrap */ From bf87e01cc165fe3e604343ce57abf0793b6beb27 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 23:12:01 +0300 Subject: [PATCH 06/80] fix(ci): regenerate IPC invoke map and fix account-handlers test mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Regenerate generated-ipc-invoke-map.ts to include new settings channels (REGISTER_GLOBAL_CAPTURE) added by recent settings feature commits - Add missing account-handlers mock to ipc/index.test.ts — the account section feature introduced registerAccountHandlers but didn't update the registration lifecycle test, causing 3 test failures Co-Authored-By: Paperclip --- apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts | 3 +++ apps/desktop/src/main/ipc/index.test.ts | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts index d12470f52..bb920ddce 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -1349,6 +1349,9 @@ export interface MainIpcInvokeHandlers { | { success: boolean; message?: undefined; error?: undefined } > > + 'settings:registerGlobalCapture': ( + ...args: [] + ) => Awaited> 'settings:reindexEmbeddings': ( ...args: [] ) => Awaited< diff --git a/apps/desktop/src/main/ipc/index.test.ts b/apps/desktop/src/main/ipc/index.test.ts index 7a457cc5b..dbdc495cf 100644 --- a/apps/desktop/src/main/ipc/index.test.ts +++ b/apps/desktop/src/main/ipc/index.test.ts @@ -37,7 +37,9 @@ const hoisted = vi.hoisted(() => ({ registerGraphHandlers: vi.fn(), unregisterGraphHandlers: vi.fn(), registerAIInlineHandlers: vi.fn(), - unregisterAIInlineHandlers: vi.fn() + unregisterAIInlineHandlers: vi.fn(), + registerAccountHandlers: vi.fn(), + unregisterAccountHandlers: vi.fn() })) vi.mock('./vault-handlers', () => ({ @@ -113,6 +115,10 @@ vi.mock('./ai-inline-handlers', () => ({ registerAIInlineHandlers: hoisted.registerAIInlineHandlers, unregisterAIInlineHandlers: hoisted.unregisterAIInlineHandlers })) +vi.mock('./account-handlers', () => ({ + registerAccountHandlers: hoisted.registerAccountHandlers, + unregisterAccountHandlers: hoisted.unregisterAccountHandlers +})) import { areHandlersRegistered, registerAllHandlers, unregisterAllHandlers } from './index' From dcc19304ee6a83098d9e4839706e32b87426432e Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 23:14:08 +0300 Subject: [PATCH 07/80] perf: reduce cold-start time by parallelizing vault open with window creation Two changes to improve startup performance toward the <2s cold-start target: 1. Move createWindow() before autoOpenLastVault() in the app.whenReady() handler. The vault open path (SQLite init, migrations, FTS setup, file watcher, sync runtime) was blocking window creation for ~300-600ms. The renderer already handles vault-not-open state via VaultOnboarding and useVault(isLoading), so the window can appear immediately while the vault opens in the background. 2. Eliminate synchronous IPC in preload startup theme resolution. The getStartupThemeSync() function used ipcRenderer.sendSync on every launch, blocking the renderer until the main process responded. Now reads from localStorage cache first (populated on the previous run), falling back to sendSync only on first launch or corrupted storage. Co-Authored-By: Paperclip --- apps/desktop/src/main/index.ts | 44 +++++++++++++++---------------- apps/desktop/src/preload/index.ts | 11 ++++++++ 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index c74a16e1a..93f851829 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -589,33 +589,33 @@ void app.whenReady().then(async () => { registerQuickCaptureShortcut() } - // Auto-open the last vault if one was previously open - await autoOpenLastVault() - - // Start the snooze scheduler for inbox items - // This checks for due items on startup and then every minute - try { - checkDueItemsOnStartup() - startSnoozeScheduler() - } catch (error) { - // Snooze scheduler is non-critical - log and continue - mainLog.warn('snooze scheduler failed to start:', error) - } - - // Start the reminder scheduler for notes/journal/highlights - // This checks for due reminders on startup and then every minute - try { - startReminderScheduler() - } catch (error) { - // Reminder scheduler is non-critical - log and continue - mainLog.warn('reminder scheduler failed to start:', error) - } - + // Configure CSP and cert pinning before the window loads configureCsp() configureCertificatePinning() + // Create the window immediately — the renderer handles the vault-not-open state + // (VaultOnboarding / loading spinner) while the vault opens in the background. + // This moves window creation ~300–600ms earlier on cold start. createWindow() + // Open the last vault and start schedulers concurrently with renderer load. + // The renderer subscribes to vault status events and updates automatically. + void autoOpenLastVault() + .then(() => { + try { + checkDueItemsOnStartup() + startSnoozeScheduler() + } catch (error) { + mainLog.warn('snooze scheduler failed to start:', error) + } + try { + startReminderScheduler() + } catch (error) { + mainLog.warn('reminder scheduler failed to start:', error) + } + }) + .catch((err) => mainLog.error('autoOpenLastVault failed:', err)) + app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 3d14263d5..c9d1f9eb2 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -61,6 +61,17 @@ type StartupTheme = 'light' | 'dark' | 'white' | 'system' const THEME_STORAGE_KEY = 'memry-theme' function getStartupThemeSync(): StartupTheme { + // Fast path: use the theme cached in localStorage from the previous run. + // This avoids a synchronous IPC round-trip on every launch after the first. + try { + const cached = window.localStorage.getItem(THEME_STORAGE_KEY) + if (cached === 'light' || cached === 'dark' || cached === 'white' || cached === 'system') { + return cached + } + } catch { + // localStorage may be unavailable; fall through to IPC + } + // First launch (or corrupted storage): fall back to synchronous IPC. try { return ipcRenderer.sendSync(SettingsChannels.sync.GET_STARTUP_THEME) as StartupTheme } catch { From e967b9a0df03dade4473bb7ebc85c488d2a5e5b1 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 23:14:19 +0300 Subject: [PATCH 08/80] chore(ipc): update generated-ipc-invoke-map with current ts-printer format The ipc-invoke-map generator uses TypeScript's printer API; output format differs between ts versions (compact one-liners vs expanded multi-line). Commit the freshly generated format so ipc:check passes. Co-Authored-By: Paperclip --- .../src/main/ipc/generated-ipc-invoke-map.ts | 2955 ++--------------- 1 file changed, 293 insertions(+), 2662 deletions(-) diff --git a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts index bb920ddce..8c8150b9b 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -2,2668 +2,299 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export interface MainIpcInvokeHandlers { - 'account:getInfo': (...args: []) => Awaited - 'account:getRecoveryKey': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; error: string; key?: undefined } - | { success: boolean; key: string; error?: undefined } - > - > - 'account:signOut': ( - ...args: [] - ) => Awaited> - 'ai-inline:get-server-port': (...args: []) => Awaited - 'ai-inline:get-settings': ( - ...args: [] - ) => Awaited - 'ai-inline:set-settings': ( - ...args: [ - Partial - ] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'ai-inline:start-server': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; error: string; port?: undefined } - | { success: boolean; port: number; error?: undefined } - > - > - 'ai-inline:stop-server': (...args: []) => Awaited> - 'auth:init-oauth': (...args: [{ provider: 'google' }]) => Awaited> - 'auth:refresh-token': ( - ...args: [] - ) => Awaited> - 'auth:request-otp': (...args: [{ email: string }]) => Awaited> - 'auth:resend-otp': (...args: [{ email: string }]) => Awaited> - 'auth:verify-otp': (...args: [{ email: string; code: string }]) => Awaited< - Promise<{ - success: boolean - isNewUser: boolean - needsSetup: boolean - needsRecoveryInput: boolean - }> - > - 'bookmarks:bulk-create': ( - ...args: [{ items: { itemType: string; itemId: string }[] }] - ) => Awaited> - 'bookmarks:bulk-delete': ( - ...args: [{ bookmarkIds: string[] }] - ) => Awaited> - 'bookmarks:create': (...args: [{ itemType: string; itemId: string }]) => Awaited< - Promise< - | { success: boolean; bookmark: null; error: string } - | { - success: boolean - bookmark: { - id: string - createdAt: string - position: number - itemType: string - itemId: string - } - error?: undefined - } - > - > - 'bookmarks:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'bookmarks:get': (...args: [string]) => Awaited< - Promise<{ - id: string - createdAt: string - position: number - itemType: string - itemId: string - } | null> - > - 'bookmarks:get-by-item': (...args: [{ itemType: string; itemId: string }]) => Awaited< - Promise<{ - id: string - createdAt: string - position: number - itemType: string - itemId: string - } | null> - > - 'bookmarks:is-bookmarked': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited> - 'bookmarks:list': ( - ...args: [ - { - itemType?: string | undefined - sortBy?: 'createdAt' | 'position' | undefined - sortOrder?: 'asc' | 'desc' | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise - > - 'bookmarks:list-by-type': ( - ...args: [string] - ) => Awaited< - Promise - > - 'bookmarks:reorder': ( - ...args: [{ bookmarkIds: string[] }] - ) => Awaited> - 'bookmarks:toggle': (...args: [{ itemType: string; itemId: string }]) => Awaited< - Promise<{ - success: boolean - isBookmarked: boolean - bookmark: { - id: string - createdAt: string - position: number - itemType: string - itemId: string - } | null - }> - > - 'context-menu:show': ( - ...args: [ - { - id: string - label: string - accelerator?: string | undefined - disabled?: boolean | undefined - type?: 'normal' | 'separator' | undefined - }[] - ] - ) => Awaited> - 'crdt:apply-update': (...args: [unknown]) => Awaited> - 'crdt:close-doc': (...args: [unknown]) => Awaited> - 'crdt:open-doc': ( - ...args: [unknown] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'crdt:sync-step-1': ( - ...args: [{ noteId: string; stateVector: number[] }] - ) => Awaited< - Promise - > - 'crdt:sync-step-2': (...args: [{ noteId: string; diff: number[] }]) => Awaited> - 'crypto:decrypt-item': ( - ...args: [ - { - itemId: string - type: - | 'note' - | 'filter' - | 'task' - | 'project' - | 'journal' - | 'settings' - | 'inbox' - | 'tag_definition' - encryptedKey: string - keyNonce: string - encryptedData: string - dataNonce: string - signature: string - operation?: 'create' | 'update' | 'delete' | undefined - deletedAt?: number | undefined - metadata?: Record | undefined - } - ] - ) => Awaited< - Promise - > - 'crypto:encrypt-item': ( - ...args: [ - { - itemId: string - type: - | 'note' - | 'filter' - | 'task' - | 'project' - | 'journal' - | 'settings' - | 'inbox' - | 'tag_definition' - content: Record - operation?: 'create' | 'update' | 'delete' | undefined - deletedAt?: number | undefined - metadata?: Record | undefined - } - ] - ) => Awaited< - Promise - > - 'crypto:get-rotation-progress': ( - ...args: [] - ) => Awaited - 'crypto:rotate-keys': ( - ...args: [{ confirm: boolean }] - ) => Awaited> - 'crypto:verify-signature': ( - ...args: [ - { - itemId: string - type: - | 'note' - | 'filter' - | 'task' - | 'project' - | 'journal' - | 'settings' - | 'inbox' - | 'tag_definition' - encryptedKey: string - keyNonce: string - encryptedData: string - dataNonce: string - signature: string - operation?: 'create' | 'update' | 'delete' | undefined - deletedAt?: number | undefined - metadata?: Record | undefined - } - ] - ) => Awaited< - Promise - > - 'folder-view:delete-view': ( - ...args: [{ folderPath: string; viewName: string }] - ) => Awaited< - Promise - > - 'folder-view:folder-exists': (...args: [string]) => Awaited - 'folder-view:get-available-properties': ( - ...args: [{ folderPath: string }] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/folder-view-api').GetAvailablePropertiesResponse - > - > - 'folder-view:get-config': ( - ...args: [{ folderPath: string }] - ) => Awaited< - Promise - > - 'folder-view:get-folder-suggestions': ( - ...args: [{ noteId: string }] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/folder-view-api').GetFolderSuggestionsResponse - > - > - 'folder-view:get-views': ( - ...args: [{ folderPath: string }] - ) => Awaited< - Promise - > - 'folder-view:list-with-properties': ( - ...args: [ - { - folderPath: string - properties?: string[] | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/folder-view-api').ListWithPropertiesResponse - > - > - 'folder-view:set-config': ( - ...args: [ - { - folderPath: string - config: { - path?: string | undefined - template?: string | undefined - inherit?: boolean | undefined - formulas?: Record | undefined - properties?: - | Record< - string, - { - displayName?: string | undefined - color?: boolean | undefined - dateFormat?: string | undefined - numberFormat?: string | undefined - hidden?: boolean | undefined - } - > - | undefined - summaries?: - | Record< - string, - { - type: - | 'custom' - | 'count' - | 'sum' - | 'average' - | 'min' - | 'max' - | 'countBy' - | 'countUnique' - label?: string | undefined - expression?: string | undefined - } - > - | undefined - views?: - | { - name: string - type?: 'table' | 'grid' | 'list' | 'kanban' | undefined - default?: boolean | undefined - columns?: - | { - id: string - width?: number | undefined - displayName?: string | undefined - showSummary?: boolean | undefined - }[] - | undefined - filters?: unknown - order?: { property: string; direction: 'asc' | 'desc' }[] | undefined - groupBy?: - | { - property: string - direction?: 'asc' | 'desc' | undefined - collapsed?: boolean | undefined - showSummary?: boolean | undefined - } - | undefined - limit?: number | undefined - showSummaries?: boolean | undefined - }[] - | undefined - } - } - ] - ) => Awaited< - Promise - > - 'folder-view:set-view': ( - ...args: [ - { - folderPath: string - view: { - name: string - type?: 'table' | 'grid' | 'list' | 'kanban' | undefined - default?: boolean | undefined - columns?: - | { - id: string - width?: number | undefined - displayName?: string | undefined - showSummary?: boolean | undefined - }[] - | undefined - filters?: unknown - order?: { property: string; direction: 'asc' | 'desc' }[] | undefined - groupBy?: - | { - property: string - direction?: 'asc' | 'desc' | undefined - collapsed?: boolean | undefined - showSummary?: boolean | undefined - } - | undefined - limit?: number | undefined - showSummaries?: boolean | undefined - } - } - ] - ) => Awaited< - Promise - > - 'graph:get-graph-data': (...args: []) => Awaited<{ - nodes: { - id: string - type: 'note' | 'task' | 'project' | 'journal' - label: string - tags: string[] - wordCount: number - connectionCount: number - emoji: string | null - color: string - isOrphan: boolean - isUnresolved: boolean - }[] - edges: { - id: string - source: string - target: string - type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' - weight: number - }[] - }> - 'graph:get-local-graph': (...args: [{ noteId: string; depth?: number | undefined }]) => Awaited<{ - nodes: { - id: string - type: 'note' | 'task' | 'project' | 'journal' - label: string - tags: string[] - wordCount: number - connectionCount: number - emoji: string | null - color: string - isOrphan: boolean - isUnresolved: boolean - }[] - edges: { - id: string - source: string - target: string - type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' - weight: number - }[] - }> - 'inbox:add-tag': ( - ...args: [any, any] - ) => Awaited> - 'inbox:archive': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-archive': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-archive-older-than': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-file': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-snooze': (...args: [any]) => Awaited< - Promise<{ - success: boolean - processedCount: number - errors: { itemId: string; error: string }[] - }> - > - 'inbox:bulk-tag': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-clip': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-image': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-link': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-pdf': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-text': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-voice': ( - ...args: [any] - ) => Awaited> - 'inbox:convert-to-note': ( - ...args: [any] - ) => Awaited> - 'inbox:convert-to-task': ( - ...args: [any] - ) => Awaited> - 'inbox:delete-permanent': ( - ...args: [any] - ) => Awaited> - 'inbox:file': ( - ...args: [any] - ) => Awaited> - 'inbox:file-all-stale': ( - ...args: [] - ) => Awaited> - 'inbox:get': ( - ...args: [any] - ) => Awaited> - 'inbox:get-filing-history': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:get-patterns': ( - ...args: [] - ) => Awaited> - 'inbox:get-snoozed': (...args: []) => Awaited> - 'inbox:get-stale-threshold': (...args: []) => Awaited> - 'inbox:get-stats': ( - ...args: [] - ) => Awaited> - 'inbox:get-suggestions': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:get-tags': (...args: []) => Awaited> - 'inbox:link-to-note': ( - ...args: [any, any, any] - ) => Awaited> - 'inbox:list': ( - ...args: [any] - ) => Awaited> - 'inbox:list-archived': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:mark-viewed': ( - ...args: [any] - ) => Awaited> - 'inbox:remove-tag': ( - ...args: [any, any] - ) => Awaited> - 'inbox:retry-metadata': ( - ...args: [any] - ) => Awaited> - 'inbox:retry-transcription': ( - ...args: [any] - ) => Awaited> - 'inbox:set-stale-threshold': (...args: [any]) => Awaited> - 'inbox:snooze': ( - ...args: [any] - ) => Awaited> - 'inbox:track-suggestion': ( - ...args: [any, any, any, any, any, any, any] - ) => Awaited> - 'inbox:unarchive': ( - ...args: [any] - ) => Awaited> - 'inbox:undo-archive': ( - ...args: [any] - ) => Awaited> - 'inbox:undo-file': ( - ...args: [any] - ) => Awaited> - 'inbox:unsnooze': ( - ...args: [any] - ) => Awaited> - 'inbox:update': ( - ...args: [any] - ) => Awaited> - 'journal:createEntry': ( - ...args: [ - { - date: string - content?: string | undefined - tags?: string[] | undefined - properties?: Record | undefined - } - ] - ) => Awaited< - Promise<{ - id: string - date: string - content: string - wordCount: number - characterCount: number - tags: string[] - createdAt: string - modifiedAt: string - properties?: Record | undefined - }> - > - 'journal:deleteEntry': (...args: [{ date: string }]) => Awaited> - 'journal:getAllTags': (...args: []) => Awaited> - 'journal:getDayContext': (...args: [{ date: string }]) => Awaited< - Promise<{ - date: string - tasks: { - id: string - title: string - completed: boolean - priority?: 'urgent' | 'high' | 'medium' | 'low' | undefined - isOverdue?: boolean | undefined - }[] - events: { - id: string - time: string - title: string - type: 'meeting' | 'focus' | 'event' - attendeeCount?: number | undefined - }[] - overdueCount: number - }> - > - 'journal:getEntry': (...args: [{ date: string }]) => Awaited< - Promise<{ - id: string - date: string - content: string - wordCount: number - characterCount: number - tags: string[] - createdAt: string - modifiedAt: string - properties?: Record | undefined - } | null> - > - 'journal:getHeatmap': ( - ...args: [{ year: number }] - ) => Awaited> - 'journal:getMonthEntries': (...args: [{ year: number; month: number }]) => Awaited< - Promise< - { - date: string - preview: string - wordCount: number - characterCount: number - activityLevel: 0 | 1 | 2 | 4 | 3 - tags: string[] - }[] - > - > - 'journal:getStreak': ( - ...args: [] - ) => Awaited< - Promise<{ currentStreak: number; longestStreak: number; lastEntryDate: string | null }> - > - 'journal:getYearStats': (...args: [{ year: number }]) => Awaited< - Promise< - { - year: number - month: number - entryCount: number - totalWordCount: number - totalCharacterCount: number - averageLevel: number - }[] - > - > - 'journal:updateEntry': ( - ...args: [ - { - date: string - content?: string | undefined - tags?: string[] | undefined - properties?: Record | undefined - } - ] - ) => Awaited< - Promise<{ - id: string - date: string - content: string - wordCount: number - characterCount: number - tags: string[] - createdAt: string - modifiedAt: string - properties?: Record | undefined - }> - > - 'notes:create': ( - ...args: [ - { - title: string - content?: string | undefined - folder?: string | undefined - tags?: string[] | undefined - template?: string | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:create-folder': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:create-property-definition': ( - ...args: [ - { - name: string - type: 'number' | 'date' | 'text' | 'checkbox' | 'url' - options?: string[] | undefined - defaultValue?: unknown - color?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - definition: { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - } - error?: undefined - } - | { success: boolean; definition: null; error: string } - > - > - 'notes:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:delete-attachment': ( - ...args: [{ noteId: string; filename: string }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:delete-folder': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:delete-version': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:exists': (...args: [string]) => Awaited> - 'notes:export-html': ( - ...args: [ - { - noteId: string - includeMetadata?: boolean | undefined - pageSize?: 'A4' | 'Letter' | 'Legal' | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; error: string; path?: undefined } - | { success: boolean; path: string; error?: undefined } - > - > - 'notes:export-pdf': ( - ...args: [ - { - noteId: string - includeMetadata?: boolean | undefined - pageSize?: 'A4' | 'Letter' | 'Legal' | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; error: string; path?: undefined } - | { success: boolean; path: string; error?: undefined } - > - > - 'notes:get': (...args: [string]) => Awaited> - 'notes:get-all-positions': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; positions: Record; error?: undefined } - | { success: boolean; positions: {}; error: string } - > - > - 'notes:get-by-path': (...args: [string]) => Awaited> - 'notes:get-file': ( - ...args: [string] - ) => Awaited> - 'notes:get-folder-config': ( - ...args: [string] - ) => Awaited< - Promise - > - 'notes:get-folder-template': (...args: [string]) => Awaited> - 'notes:get-folders': (...args: []) => Awaited> - 'notes:get-links': ( - ...args: [string] - ) => Awaited> - 'notes:get-local-only-count': (...args: []) => Awaited> - 'notes:get-positions': (...args: [{ folderPath: string }]) => Awaited< - Promise< - | { - success: boolean - positions: { path: string; position: number; folderPath: string }[] - error?: undefined - } - | { success: boolean; positions: never[]; error: string } - > - > - 'notes:get-property-definitions': (...args: []) => Awaited< - Promise< - { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - }[] - > - > - 'notes:get-tags': ( - ...args: [] - ) => Awaited> - 'notes:get-version': ( - ...args: [string] - ) => Awaited> - 'notes:get-versions': ( - ...args: [string] - ) => Awaited> - 'notes:import-files': ( - ...args: [{ sourcePaths: string[]; targetFolder?: string | undefined }] - ) => Awaited> - 'notes:list': ( - ...args: [ - { - folder?: string | undefined - tags?: string[] | undefined - sortBy?: 'title' | 'modified' | 'created' | 'position' | undefined - sortOrder?: 'asc' | 'desc' | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited> - 'notes:list-attachments': ( - ...args: [string] - ) => Awaited> - 'notes:move': ( - ...args: [{ id: string; newFolder: string }] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:open-external': (...args: [string]) => Awaited> - 'notes:rename': ( - ...args: [{ id: string; newTitle: string }] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:rename-folder': ( - ...args: [{ oldPath: string; newPath: string }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:reorder': ( - ...args: [{ folderPath: string; notePaths: string[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:resolve-by-title': (...args: [string]) => Awaited< - Promise<{ - id: string - path: string - title: string - fileType: import('../../../../../packages/shared/src/file-types').FileType - } | null> - > - 'notes:restore-version': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:reveal-in-finder': (...args: [string]) => Awaited> - 'notes:set-folder-config': ( - ...args: [ - { - folderPath: string - config: { template?: string | undefined; inherit?: boolean | undefined } - } - ] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:set-local-only': ( - ...args: [{ id: string; localOnly: boolean }] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:show-import-dialog': ( - ...args: [] - ) => Awaited> - 'notes:update': ( - ...args: [ - { - id: string - title?: string | undefined - content?: string | undefined - tags?: string[] | undefined - frontmatter?: Record | undefined - emoji?: string | null | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:update-property-definition': ( - ...args: [ - { - name: string - type?: 'number' | 'date' | 'text' | 'checkbox' | 'url' | undefined - options?: string[] | undefined - defaultValue?: unknown - color?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - definition: - | { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - } - | undefined - error?: undefined - } - | { success: boolean; definition: null; error: string } - > - > - 'notes:upload-attachment': ( - ...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer }] - ) => Awaited> - 'properties:get': ( - ...args: [{ entityId: string }] - ) => Awaited> - 'properties:rename': ( - ...args: [{ entityId: string; oldName: string; newName: string }] - ) => Awaited< - Promise - > - 'properties:set': ( - ...args: [{ entityId: string; properties: Record }] - ) => Awaited< - Promise - > - 'quick-capture:get-clipboard': (...args: []) => Awaited - 'reminder:bulk-dismiss': ( - ...args: [{ reminderIds: string[] }] - ) => Awaited< - Promise< - | { success: boolean; dismissedCount: number; error?: undefined } - | { success: boolean; dismissedCount: number; error: string } - > - > - 'reminder:count-pending': (...args: []) => Awaited> - 'reminder:create': ( - ...args: [ - | { - targetType: 'note' - targetId: string - remindAt: string - title?: string | undefined - note?: string | undefined - } - | { - targetType: 'journal' - targetId: string - remindAt: string - title?: string | undefined - note?: string | undefined - } - | { - targetType: 'highlight' - targetId: string - highlightText: string - highlightStart: number - highlightEnd: number - remindAt: string - title?: string | undefined - note?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - | { success: boolean; reminder: null; error: string } - > - > - 'reminder:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'reminder:dismiss': (...args: [string]) => Awaited< - Promise< - | { success: boolean; reminder: null; error: string } - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - > - > - 'reminder:get': ( - ...args: [string] - ) => Awaited< - Promise - > - 'reminder:get-due': ( - ...args: [] - ) => Awaited< - Promise - > - 'reminder:get-for-target': ( - ...args: [{ targetType: 'note' | 'journal' | 'highlight'; targetId: string }] - ) => Awaited> - 'reminder:get-upcoming': (...args: [number | undefined]) => Awaited< - Promise<{ - reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] - total: number - hasMore: boolean - }> - > - 'reminder:list': ( - ...args: [ - { - targetType?: 'note' | 'journal' | 'highlight' | undefined - targetId?: string | undefined - status?: - | 'pending' - | 'triggered' - | 'dismissed' - | 'snoozed' - | ('pending' | 'triggered' | 'dismissed' | 'snoozed')[] - | undefined - fromDate?: string | undefined - toDate?: string | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise<{ - reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] - total: number - hasMore: boolean - }> - > - 'reminder:snooze': (...args: [{ id: string; snoozeUntil: string }]) => Awaited< - Promise< - | { success: boolean; reminder: null; error: string } - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - > - > - 'reminder:update': ( - ...args: [ - { - id: string - remindAt?: string | undefined - title?: string | null | undefined - note?: string | null | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; reminder: null; error: string } - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - > - > - 'saved-filters:create': ( - ...args: [ - { - name: string - config: { - filters: { - search?: string | undefined - projectIds?: string[] | undefined - priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined - dueDate?: - | { - type: - | 'custom' - | 'any' - | 'none' - | 'overdue' - | 'today' - | 'tomorrow' - | 'this-week' - | 'next-week' - | 'this-month' - customStart?: string | null | undefined - customEnd?: string | null | undefined - } - | undefined - statusIds?: string[] | undefined - completion?: 'active' | 'completed' | 'all' | undefined - repeatType?: 'all' | 'repeating' | 'one-time' | undefined - hasTime?: 'all' | 'with-time' | 'without-time' | undefined - } - sort?: - | { - field: 'title' | 'createdAt' | 'priority' | 'dueDate' | 'completedAt' | 'project' - direction: 'asc' | 'desc' - } - | undefined - starred?: boolean | undefined - } - } - ] - ) => Awaited< - Promise<{ - success: boolean - savedFilter: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter - }> - > - 'saved-filters:delete': ( - ...args: [{ id: string }] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'saved-filters:list': (...args: []) => Awaited< - Promise<{ - savedFilters: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter[] - }> - > - 'saved-filters:reorder': ( - ...args: [{ ids: string[]; positions: number[] }] - ) => Awaited> - 'saved-filters:update': ( - ...args: [ - { - id: string - name?: string | undefined - config?: - | { - filters: { - search?: string | undefined - projectIds?: string[] | undefined - priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined - dueDate?: - | { - type: - | 'custom' - | 'any' - | 'none' - | 'overdue' - | 'today' - | 'tomorrow' - | 'this-week' - | 'next-week' - | 'this-month' - customStart?: string | null | undefined - customEnd?: string | null | undefined - } - | undefined - statusIds?: string[] | undefined - completion?: 'active' | 'completed' | 'all' | undefined - repeatType?: 'all' | 'repeating' | 'one-time' | undefined - hasTime?: 'all' | 'with-time' | 'without-time' | undefined - } - sort?: - | { - field: - | 'title' - | 'createdAt' - | 'priority' - | 'dueDate' - | 'completedAt' - | 'project' - direction: 'asc' | 'desc' - } - | undefined - starred?: boolean | undefined - } - | undefined - position?: number | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; savedFilter: null; error: string } - | { - success: boolean - savedFilter: - | import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter - | null - error?: undefined - } - > - > - 'search:add-reason': ( - ...args: [ - { - itemId: string - itemType: 'note' | 'task' | 'journal' | 'inbox' - itemTitle: string - searchQuery: string - itemIcon?: string | null | undefined - } - ] - ) => Awaited> - 'search:clear-reasons': (...args: []) => Awaited> - 'search:get-all-tags': (...args: []) => Awaited> - 'search:get-reasons': ( - ...args: [] - ) => Awaited> - 'search:get-stats': ( - ...args: [] - ) => Awaited> - 'search:query': ( - ...args: [ - { - text: string - types?: ('note' | 'task' | 'journal' | 'inbox')[] | undefined - tags?: string[] | undefined - dateRange?: { from: string; to: string } | null | undefined - projectId?: string | null | undefined - folderPath?: string | null | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited> - 'search:quick': ( - ...args: [string] - ) => Awaited< - Promise - > - 'search:rebuild-index': (...args: []) => Awaited< - Promise< - | { - notes: number - tasks: number - inbox: number - durationMs: number - started: true - error?: undefined - } - | { started: false; error: string } - > - > - 'settings:get': (...args: [string]) => Awaited - 'settings:getAIModelStatus': ( - ...args: [] - ) => Awaited> - 'settings:getAISettings': (...args: []) => Awaited - 'settings:getBackupSettings': (...args: []) => Awaited<{ - autoBackup: boolean - frequencyHours: 1 | 6 | 12 | 24 - maxBackups: number - lastBackupAt: string | null - }> - 'settings:getEditorSettings': (...args: []) => Awaited<{ - width: 'medium' | 'narrow' | 'wide' - spellCheck: boolean - autoSaveDelay: number - showWordCount: boolean - toolbarMode: 'floating' | 'sticky' - }> - 'settings:getGeneralSettings': (...args: []) => Awaited<{ - theme: 'light' | 'dark' | 'white' | 'system' - fontSize: 'small' | 'medium' | 'large' - fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' - accentColor: string - startOnBoot: boolean - language: string - onboardingCompleted: boolean - }> - 'settings:getGraphSettings': (...args: []) => Awaited<{ - layout: 'forceatlas2' | 'circular' | 'random' - showLabels: boolean - showEdgeLabels: boolean - animateLayout: boolean - showTagEdges: boolean - }> - 'settings:getJournalSettings': (...args: []) => Awaited<{ - defaultTemplate: string | null - showSchedule: boolean - showTasks: boolean - showAIConnections: boolean - showStatsFooter: boolean - }> - 'settings:getKeyboardSettings': (...args: []) => Awaited<{ - overrides: Record< - string, - { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } - > - globalCapture: { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } | null - }> - 'settings:getNoteEditorSettings': ( - ...args: [] - ) => Awaited - 'settings:getSyncSettings': (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean }> - 'settings:getTabSettings': (...args: []) => Awaited - 'settings:getTaskSettings': (...args: []) => Awaited<{ - defaultProjectId: string | null - defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' - weekStartDay: 'sunday' | 'monday' - staleInboxDays: number - }> - 'settings:loadAIModel': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; message: string; error?: undefined } - | { success: boolean; error: string; message?: undefined } - | { success: boolean; message?: undefined; error?: undefined } - > - > - 'settings:registerGlobalCapture': ( - ...args: [] - ) => Awaited> - 'settings:reindexEmbeddings': ( - ...args: [] - ) => Awaited< - Promise<{ success: boolean; computed: number; skipped: number; error?: string | undefined }> - > - 'settings:resetKeyboardSettings': ( - ...args: [] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:set': ( - ...args: [{ key: string; value: string }] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setAISettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setBackupSettings': ( - ...args: [ - Partial<{ - autoBackup: boolean - frequencyHours: 1 | 6 | 12 | 24 - maxBackups: number - lastBackupAt: string | null - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setEditorSettings': ( - ...args: [ - Partial<{ - width: 'medium' | 'narrow' | 'wide' - spellCheck: boolean - autoSaveDelay: number - showWordCount: boolean - toolbarMode: 'floating' | 'sticky' - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setGeneralSettings': ( - ...args: [ - Partial<{ - theme: 'light' | 'dark' | 'white' | 'system' - fontSize: 'small' | 'medium' | 'large' - fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' - accentColor: string - startOnBoot: boolean - language: string - onboardingCompleted: boolean - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setGraphSettings': ( - ...args: [ - Partial<{ - layout: 'forceatlas2' | 'circular' | 'random' - showLabels: boolean - showEdgeLabels: boolean - animateLayout: boolean - showTagEdges: boolean - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setJournalSettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setKeyboardSettings': ( - ...args: [ - Partial<{ - overrides: Record< - string, - { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } - > - globalCapture: { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } | null - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setNoteEditorSettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setSyncSettings': ( - ...args: [Partial<{ enabled: boolean; autoSync: boolean }>] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setTabSettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setTaskSettings': ( - ...args: [ - Partial<{ - defaultProjectId: string | null - defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' - weekStartDay: 'sunday' | 'monday' - staleInboxDays: number - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'sync:approve-linking': ( - ...args: [{ sessionId: string }] - ) => Awaited< - Promise - > - 'sync:check-device-status': (...args: []) => Awaited> - 'sync:complete-linking-qr': ( - ...args: [{ sessionId: string }] - ) => Awaited< - Promise - > - 'sync:confirm-recovery-phrase': ( - ...args: [{ confirmed: boolean }] - ) => Awaited> - 'sync:download-attachment': ( - ...args: [{ attachmentId: string; targetPath?: string | undefined }] - ) => Awaited< - Promise< - | { success: boolean; error: string; filePath?: undefined } - | { success: boolean; filePath: string; error?: undefined } - > - > - 'sync:emergency-wipe': (...args: []) => Awaited> - 'sync:generate-linking-qr': ( - ...args: [] - ) => Awaited< - Promise - > - 'sync:get-devices': (...args: []) => Awaited< - Promise<{ - devices: { - id: string - name: string - platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android' - linkedAt: number - lastSyncAt: number | undefined - isCurrentDevice: boolean - }[] - email: string | undefined - }> - > - 'sync:get-download-progress': (...args: [{ attachmentId: string }]) => Awaited< - Promise<{ - progress: number - downloadedChunks: number - totalChunks: number - status: 'downloading' - } | null> - > - 'sync:get-history': ( - ...args: [{ limit?: number | undefined; offset?: number | undefined }] - ) => Awaited< - Promise<{ - entries: { - id: string - type: 'error' | 'push' | 'pull' - itemCount: number - direction: string | undefined - details: unknown - durationMs: number | undefined - createdAt: number - }[] - total: number - }> - > - 'sync:get-linking-sas': ( - ...args: [{ sessionId: string }] - ) => Awaited> - 'sync:get-quarantined-items': ( - ...args: [] - ) => Awaited - 'sync:get-queue-size': (...args: []) => Awaited<{ pending: number; failed: number }> - 'sync:get-recovery-phrase': (...args: []) => Awaited - 'sync:get-status': ( - ...args: [] - ) => Awaited< - | import('../../../../../packages/contracts/src/ipc-sync-ops').GetSyncStatusResult - | { status: string; pendingCount: number } - > - 'sync:get-storage-breakdown': ( - ...args: [] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/ipc-sync-ops').StorageBreakdownResult | null - > - > - 'sync:get-synced-settings': (...args: []) => Awaited<{ - general?: - | { - theme?: 'light' | 'dark' | 'white' | 'system' | undefined - fontSize?: 'small' | 'medium' | 'large' | undefined - fontFamily?: 'system' | 'serif' | 'sans-serif' | 'monospace' | undefined - accentColor?: string | undefined - startOnBoot?: boolean | undefined - language?: string | undefined - } - | undefined - editor?: - | { - width?: 'medium' | 'narrow' | 'wide' | undefined - spellCheck?: boolean | undefined - autoSaveDelay?: number | undefined - showWordCount?: boolean | undefined - toolbarMode?: 'floating' | 'sticky' | undefined - } - | undefined - tasks?: - | { - defaultProjectId?: string | null | undefined - defaultSortOrder?: 'createdAt' | 'priority' | 'dueDate' | 'manual' | undefined - weekStartDay?: 'sunday' | 'monday' | undefined - staleInboxDays?: number | undefined - showCompleted?: boolean | undefined - sortBy?: string | undefined - } - | undefined - keyboard?: { overrides?: Record | undefined } | undefined - notes?: - | { - defaultFolder?: string | undefined - editorFontSize?: number | undefined - spellCheck?: boolean | undefined - } - | undefined - sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined } | undefined - } | null> - 'sync:get-upload-progress': (...args: [{ sessionId: string }]) => Awaited< - Promise<{ - progress: number - uploadedChunks: number - totalChunks: number - status: 'uploading' - } | null> - > - 'sync:link-via-qr': ( - ...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined }] - ) => Awaited> - 'sync:link-via-recovery': ( - ...args: [{ recoveryPhrase: string }] - ) => Awaited< - Promise< - | { success: boolean; error: string; deviceId?: undefined } - | { success: boolean; deviceId: string; error?: undefined } - > - > - 'sync:logout': ( - ...args: [] - ) => Awaited> - 'sync:pause': (...args: []) => Awaited<{ success: boolean; wasPaused: boolean }> - 'sync:remove-device': ( - ...args: [{ deviceId: string }] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'sync:rename-device': ( - ...args: [{ deviceId: string; newName: string }] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'sync:resume': (...args: []) => Awaited<{ success: boolean; pendingCount: number }> - 'sync:setup-first-device': ( - ...args: [{ oauthToken: string; provider: 'google'; state: string }] - ) => Awaited< - Promise< - | { - success: boolean - needsRecoverySetup: boolean - deviceId: string - needsRecoveryInput?: undefined - } - | { - success: boolean - needsRecoverySetup: boolean - needsRecoveryInput: boolean - deviceId?: undefined - } - > - > - 'sync:setup-new-account': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; error: string; deviceId?: undefined } - | { success: boolean; deviceId: string; error?: undefined } - > - > - 'sync:trigger-sync': ( - ...args: [] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'sync:update-synced-setting': ( - ...args: [unknown] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'sync:upload-attachment': ( - ...args: [{ noteId: string; filePath: string }] - ) => Awaited< - Promise< - | { success: boolean; error: string; attachmentId?: undefined; sessionId?: undefined } - | { success: boolean; attachmentId: string; sessionId: string; error?: undefined } - > - > - 'tags:delete': ( - ...args: [string] - ) => Awaited> - 'tags:get-all-with-counts': ( - ...args: [] - ) => Awaited< - Promise - > - 'tags:get-notes-by-tag': ( - ...args: [ - { - tag: string - sortBy?: 'title' | 'modified' | 'created' | undefined - sortOrder?: 'asc' | 'desc' | undefined - } - ] - ) => Awaited< - Promise - > - 'tags:merge': ( - ...args: [{ source: string; target: string }] - ) => Awaited> - 'tags:pin-note-to-tag': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise - > - 'tags:remove-from-note': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise - > - 'tags:rename': ( - ...args: [{ oldName: string; newName: string }] - ) => Awaited> - 'tags:unpin-note-from-tag': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise - > - 'tags:update-color': ( - ...args: [{ tag: string; color: string }] - ) => Awaited< - Promise - > - 'tasks:archive': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'tasks:bulk-archive': ( - ...args: [{ ids: string[] }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:bulk-complete': ( - ...args: [{ ids: string[] }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:bulk-delete': ( - ...args: [{ ids: string[] }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:bulk-move': ( - ...args: [{ ids: string[]; projectId: string }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:complete': (...args: [{ id: string; completedAt?: string | undefined }]) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:convert-to-subtask': (...args: [{ taskId: string; parentId: string }]) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:convert-to-task': (...args: [string]) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:create': ( - ...args: [ - { - projectId: string - title: string - description?: string | null | undefined - priority?: number | undefined - statusId?: string | null | undefined - parentId?: string | null | undefined - dueDate?: string | null | undefined - dueTime?: string | null | undefined - startDate?: string | null | undefined - isRepeating?: boolean | undefined - repeatConfig?: - | { - frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' - endType: 'date' | 'never' | 'count' - createdAt: string - interval?: number | undefined - daysOfWeek?: number[] | undefined - monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined - dayOfMonth?: number | undefined - weekOfMonth?: number | undefined - dayOfWeekForMonth?: number | undefined - endDate?: string | null | undefined - endCount?: number | undefined - completedCount?: number | undefined - } - | null - | undefined - repeatFrom?: 'due' | 'completion' | null | undefined - tags?: string[] | undefined - linkedNoteIds?: string[] | undefined - sourceNoteId?: string | null | undefined - position?: number | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - | { success: boolean; task: null; error: string } - > - > - 'tasks:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:duplicate': (...args: [string]) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:get': (...args: [string]) => Awaited< - Promise<{ - tags: string[] - linkedNoteIds: string[] - hasSubtasks: boolean - subtaskCount: number - completedSubtaskCount: number - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } | null> - > - 'tasks:get-linked-tasks': (...args: [string]) => Awaited< - Promise< - { - tags: string[] - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - > - > - 'tasks:get-overdue': (...args: []) => Awaited< - Promise<{ - tasks: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:get-stats': (...args: []) => Awaited< - Promise<{ - total: number - completed: number - overdue: number - dueToday: number - dueThisWeek: number - }> - > - 'tasks:get-subtasks': (...args: [string]) => Awaited< - Promise< - { - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - > - > - 'tasks:get-tags': (...args: []) => Awaited> - 'tasks:get-today': (...args: []) => Awaited< - Promise<{ - tasks: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:get-upcoming': (...args: [{ days?: number | undefined }]) => Awaited< - Promise<{ - tasks: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:list': ( - ...args: [ - { - projectId?: string | undefined - statusId?: string | null | undefined - parentId?: string | null | undefined - includeCompleted?: boolean | undefined - includeArchived?: boolean | undefined - dueBefore?: string | undefined - dueAfter?: string | undefined - tags?: string[] | undefined - search?: string | undefined - sortBy?: 'modified' | 'created' | 'position' | 'priority' | 'dueDate' | undefined - sortOrder?: 'asc' | 'desc' | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise<{ - tasks: { - tags: string[] - linkedNoteIds: string[] - hasSubtasks: boolean - subtaskCount: number - completedSubtaskCount: number - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:move': ( - ...args: [ - { - taskId: string - position: number - targetProjectId?: string | undefined - targetStatusId?: string | null | undefined - targetParentId?: string | null | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:project-archive': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:project-create': ( - ...args: [ - { - name: string - description?: string | null | undefined - color?: string | undefined - icon?: string | null | undefined - statuses?: - | { - name: string - type: 'todo' | 'in_progress' | 'done' - order: number - color?: string | undefined - }[] - | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - project: { - id: string - name: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - color: string - description: string | null - icon: string | null - isInbox: boolean - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - } - error?: undefined - } - | { success: boolean; project: null; error: string } - > - > - 'tasks:project-delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:project-get': ( - ...args: [string] - ) => Awaited> - 'tasks:project-list': ( - ...args: [] - ) => Awaited> - 'tasks:project-reorder': ( - ...args: [{ projectIds: string[]; positions: number[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:project-update': ( - ...args: [ - { - id: string - name?: string | undefined - description?: string | null | undefined - color?: string | undefined - icon?: string | null | undefined - statuses?: - | { - name: string - type: 'todo' | 'in_progress' | 'done' - order: number - id?: string | undefined - color?: string | undefined - }[] - | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; project: null; error: string } - | { - success: boolean - project: { - id: string - name: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - color: string - description: string | null - icon: string | null - isInbox: boolean - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - } - error?: undefined - } - > - > - 'tasks:reorder': ( - ...args: [{ taskIds: string[]; positions: number[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:seed-demo': (...args: []) => Awaited> - 'tasks:seed-performance-test': ( - ...args: [] - ) => Awaited> - 'tasks:status-create': ( - ...args: [ - { projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined } - ] - ) => Awaited< - Promise< - | { - success: boolean - status: { - id: string - name: string - createdAt: string - position: number - color: string - projectId: string - isDefault: boolean - isDone: boolean - } - error?: undefined - } - | { success: boolean; status: null; error: string } - > - > - 'tasks:status-delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:status-list': (...args: [string]) => Awaited< - Promise< - { - id: string - name: string - createdAt: string - position: number - color: string - projectId: string - isDefault: boolean - isDone: boolean - }[] - > - > - 'tasks:status-reorder': ( - ...args: [{ statusIds: string[]; positions: number[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:status-update': ( - ...args: [ - { - id: string - name?: string | undefined - color?: string | undefined - position?: number | undefined - isDefault?: boolean | undefined - isDone?: boolean | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; error: string; status?: undefined } - | { - success: boolean - status: { - id: string - name: string - createdAt: string - position: number - color: string - projectId: string - isDefault: boolean - isDone: boolean - } - error?: undefined - } - > - > - 'tasks:unarchive': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'tasks:uncomplete': (...args: [string]) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:update': ( - ...args: [ - { - id: string - title?: string | undefined - description?: string | null | undefined - priority?: number | undefined - projectId?: string | undefined - statusId?: string | null | undefined - parentId?: string | null | undefined - dueDate?: string | null | undefined - dueTime?: string | null | undefined - startDate?: string | null | undefined - isRepeating?: boolean | undefined - repeatConfig?: - | { - frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' - endType: 'date' | 'never' | 'count' - createdAt: string - interval?: number | undefined - daysOfWeek?: number[] | undefined - monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined - dayOfMonth?: number | undefined - weekOfMonth?: number | undefined - dayOfWeekForMonth?: number | undefined - endDate?: string | null | undefined - endCount?: number | undefined - completedCount?: number | undefined - } - | null - | undefined - repeatFrom?: 'due' | 'completion' | null | undefined - tags?: string[] | undefined - linkedNoteIds?: string[] | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'templates:create': ( - ...args: [ - { - name: string - description?: string | undefined - icon?: string | null | undefined - tags?: string[] | undefined - properties?: - | { - name: string - type: - | 'number' - | 'date' - | 'text' - | 'select' - | 'checkbox' - | 'url' - | 'multiselect' - | 'rating' - value: unknown - options?: string[] | undefined - }[] - | undefined - content?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - error?: undefined - } - | { success: boolean; template: null; error: string } - > - > - 'templates:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'templates:duplicate': (...args: [{ id: string; newName: string }]) => Awaited< - Promise< - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - error?: undefined - } - | { success: boolean; template: null; error: string } - > - > - 'templates:get': ( - ...args: [string] - ) => Awaited< - Promise - > - 'templates:list': (...args: []) => Awaited< - Promise<{ - templates: import('../../../../../packages/contracts/src/templates-api').TemplateListItem[] - }> - > - 'templates:update': ( - ...args: [ - { - id: string - name?: string | undefined - description?: string | undefined - icon?: string | null | undefined - tags?: string[] | undefined - properties?: - | { - name: string - type: - | 'number' - | 'date' - | 'text' - | 'select' - | 'checkbox' - | 'url' - | 'multiselect' - | 'rating' - value: unknown - options?: string[] | undefined - }[] - | undefined - content?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - error?: undefined - } - | { success: boolean; template: null; error: string } - > - > - 'vault:close': (...args: []) => Awaited> - 'vault:get-all': ( - ...args: [] - ) => Awaited> - 'vault:get-config': ( - ...args: [] - ) => Awaited> - 'vault:get-status': ( - ...args: [] - ) => Awaited> - 'vault:reindex': (...args: []) => Awaited> - 'vault:remove': (...args: [string]) => Awaited> - 'vault:select': ( - ...args: [{ path?: string | undefined }] - ) => Awaited< - Promise - > - 'vault:switch': ( - ...args: [string] - ) => Awaited< - Promise - > - 'vault:update-config': ( - ...args: [ - { - excludePatterns?: string[] | undefined - defaultNoteFolder?: string | undefined - journalFolder?: string | undefined - attachmentsFolder?: string | undefined - } - ] - ) => Awaited> + "account:getInfo": (...args: []) => Awaited + "account:getRecoveryKey": (...args: []) => Awaited> + "account:signOut": (...args: []) => Awaited> + "ai-inline:get-server-port": (...args: []) => Awaited + "ai-inline:get-settings": (...args: []) => Awaited + "ai-inline:set-settings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "ai-inline:start-server": (...args: []) => Awaited> + "ai-inline:stop-server": (...args: []) => Awaited> + "auth:init-oauth": (...args: [{ provider: "google"; }]) => Awaited> + "auth:refresh-token": (...args: []) => Awaited> + "auth:request-otp": (...args: [{ email: string; }]) => Awaited> + "auth:resend-otp": (...args: [{ email: string; }]) => Awaited> + "auth:verify-otp": (...args: [{ email: string; code: string; }]) => Awaited> + "bookmarks:bulk-create": (...args: [{ items: { itemType: string; itemId: string; }[]; }]) => Awaited> + "bookmarks:bulk-delete": (...args: [{ bookmarkIds: string[]; }]) => Awaited> + "bookmarks:create": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "bookmarks:delete": (...args: [string]) => Awaited> + "bookmarks:get": (...args: [string]) => Awaited> + "bookmarks:get-by-item": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "bookmarks:is-bookmarked": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "bookmarks:list": (...args: [{ itemType?: string | undefined; sortBy?: "createdAt" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "bookmarks:list-by-type": (...args: [string]) => Awaited> + "bookmarks:reorder": (...args: [{ bookmarkIds: string[]; }]) => Awaited> + "bookmarks:toggle": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "context-menu:show": (...args: [{ id: string; label: string; accelerator?: string | undefined; disabled?: boolean | undefined; type?: "normal" | "separator" | undefined; }[]]) => Awaited> + "crdt:apply-update": (...args: [unknown]) => Awaited> + "crdt:close-doc": (...args: [unknown]) => Awaited> + "crdt:open-doc": (...args: [unknown]) => Awaited> + "crdt:sync-step-1": (...args: [{ noteId: string; stateVector: number[]; }]) => Awaited> + "crdt:sync-step-2": (...args: [{ noteId: string; diff: number[]; }]) => Awaited> + "crypto:decrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> + "crypto:encrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; content: Record; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> + "crypto:get-rotation-progress": (...args: []) => Awaited + "crypto:rotate-keys": (...args: [{ confirm: boolean; }]) => Awaited> + "crypto:verify-signature": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> + "folder-view:delete-view": (...args: [{ folderPath: string; viewName: string; }]) => Awaited> + "folder-view:folder-exists": (...args: [string]) => Awaited + "folder-view:get-available-properties": (...args: [{ folderPath: string; }]) => Awaited> + "folder-view:get-config": (...args: [{ folderPath: string; }]) => Awaited> + "folder-view:get-folder-suggestions": (...args: [{ noteId: string; }]) => Awaited> + "folder-view:get-views": (...args: [{ folderPath: string; }]) => Awaited> + "folder-view:list-with-properties": (...args: [{ folderPath: string; properties?: string[] | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "folder-view:set-config": (...args: [{ folderPath: string; config: { path?: string | undefined; template?: string | undefined; inherit?: boolean | undefined; formulas?: Record | undefined; properties?: Record | undefined; summaries?: Record | undefined; views?: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }[] | undefined; }; }]) => Awaited> + "folder-view:set-view": (...args: [{ folderPath: string; view: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }; }]) => Awaited> + "graph:get-graph-data": (...args: []) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> + "graph:get-local-graph": (...args: [{ noteId: string; depth?: number | undefined; }]) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> + "inbox:add-tag": (...args: [any, any]) => Awaited> + "inbox:archive": (...args: [any]) => Awaited> + "inbox:bulk-archive": (...args: [any]) => Awaited> + "inbox:bulk-archive-older-than": (...args: [any]) => Awaited> + "inbox:bulk-file": (...args: [any]) => Awaited> + "inbox:bulk-snooze": (...args: [any]) => Awaited> + "inbox:bulk-tag": (...args: [any]) => Awaited> + "inbox:capture-clip": (...args: [any]) => Awaited> + "inbox:capture-image": (...args: [any]) => Awaited> + "inbox:capture-link": (...args: [any]) => Awaited> + "inbox:capture-pdf": (...args: [any]) => Awaited> + "inbox:capture-text": (...args: [any]) => Awaited> + "inbox:capture-voice": (...args: [any]) => Awaited> + "inbox:convert-to-note": (...args: [any]) => Awaited> + "inbox:convert-to-task": (...args: [any]) => Awaited> + "inbox:delete-permanent": (...args: [any]) => Awaited> + "inbox:file": (...args: [any]) => Awaited> + "inbox:file-all-stale": (...args: []) => Awaited> + "inbox:get": (...args: [any]) => Awaited> + "inbox:get-filing-history": (...args: [any]) => Awaited> + "inbox:get-patterns": (...args: []) => Awaited> + "inbox:get-snoozed": (...args: []) => Awaited> + "inbox:get-stale-threshold": (...args: []) => Awaited> + "inbox:get-stats": (...args: []) => Awaited> + "inbox:get-suggestions": (...args: [any]) => Awaited> + "inbox:get-tags": (...args: []) => Awaited> + "inbox:link-to-note": (...args: [any, any, any]) => Awaited> + "inbox:list": (...args: [any]) => Awaited> + "inbox:list-archived": (...args: [any]) => Awaited> + "inbox:mark-viewed": (...args: [any]) => Awaited> + "inbox:remove-tag": (...args: [any, any]) => Awaited> + "inbox:retry-metadata": (...args: [any]) => Awaited> + "inbox:retry-transcription": (...args: [any]) => Awaited> + "inbox:set-stale-threshold": (...args: [any]) => Awaited> + "inbox:snooze": (...args: [any]) => Awaited> + "inbox:track-suggestion": (...args: [any, any, any, any, any, any, any]) => Awaited> + "inbox:unarchive": (...args: [any]) => Awaited> + "inbox:undo-archive": (...args: [any]) => Awaited> + "inbox:undo-file": (...args: [any]) => Awaited> + "inbox:unsnooze": (...args: [any]) => Awaited> + "inbox:update": (...args: [any]) => Awaited> + "journal:createEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> + "journal:deleteEntry": (...args: [{ date: string; }]) => Awaited> + "journal:getAllTags": (...args: []) => Awaited> + "journal:getDayContext": (...args: [{ date: string; }]) => Awaited> + "journal:getEntry": (...args: [{ date: string; }]) => Awaited | undefined; } | null>> + "journal:getHeatmap": (...args: [{ year: number; }]) => Awaited> + "journal:getMonthEntries": (...args: [{ year: number; month: number; }]) => Awaited> + "journal:getStreak": (...args: []) => Awaited> + "journal:getYearStats": (...args: [{ year: number; }]) => Awaited> + "journal:updateEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> + "notes:create": (...args: [{ title: string; content?: string | undefined; folder?: string | undefined; tags?: string[] | undefined; template?: string | undefined; }]) => Awaited> + "notes:create-folder": (...args: [string]) => Awaited> + "notes:create-property-definition": (...args: [{ name: string; type: "number" | "date" | "text" | "checkbox" | "url"; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> + "notes:delete": (...args: [string]) => Awaited> + "notes:delete-attachment": (...args: [{ noteId: string; filename: string; }]) => Awaited> + "notes:delete-folder": (...args: [string]) => Awaited> + "notes:delete-version": (...args: [string]) => Awaited> + "notes:exists": (...args: [string]) => Awaited> + "notes:export-html": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> + "notes:export-pdf": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> + "notes:get": (...args: [string]) => Awaited> + "notes:get-all-positions": (...args: []) => Awaited; error?: undefined; } | { success: boolean; positions: {}; error: string; }>> + "notes:get-by-path": (...args: [string]) => Awaited> + "notes:get-file": (...args: [string]) => Awaited> + "notes:get-folder-config": (...args: [string]) => Awaited> + "notes:get-folder-template": (...args: [string]) => Awaited> + "notes:get-folders": (...args: []) => Awaited> + "notes:get-links": (...args: [string]) => Awaited> + "notes:get-local-only-count": (...args: []) => Awaited> + "notes:get-positions": (...args: [{ folderPath: string; }]) => Awaited> + "notes:get-property-definitions": (...args: []) => Awaited> + "notes:get-tags": (...args: []) => Awaited> + "notes:get-version": (...args: [string]) => Awaited> + "notes:get-versions": (...args: [string]) => Awaited> + "notes:import-files": (...args: [{ sourcePaths: string[]; targetFolder?: string | undefined; }]) => Awaited> + "notes:list": (...args: [{ folder?: string | undefined; tags?: string[] | undefined; sortBy?: "title" | "modified" | "created" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "notes:list-attachments": (...args: [string]) => Awaited> + "notes:move": (...args: [{ id: string; newFolder: string; }]) => Awaited> + "notes:open-external": (...args: [string]) => Awaited> + "notes:rename": (...args: [{ id: string; newTitle: string; }]) => Awaited> + "notes:rename-folder": (...args: [{ oldPath: string; newPath: string; }]) => Awaited> + "notes:reorder": (...args: [{ folderPath: string; notePaths: string[]; }]) => Awaited> + "notes:resolve-by-title": (...args: [string]) => Awaited> + "notes:restore-version": (...args: [string]) => Awaited> + "notes:reveal-in-finder": (...args: [string]) => Awaited> + "notes:set-folder-config": (...args: [{ folderPath: string; config: { template?: string | undefined; inherit?: boolean | undefined; }; }]) => Awaited> + "notes:set-local-only": (...args: [{ id: string; localOnly: boolean; }]) => Awaited> + "notes:show-import-dialog": (...args: []) => Awaited> + "notes:update": (...args: [{ id: string; title?: string | undefined; content?: string | undefined; tags?: string[] | undefined; frontmatter?: Record | undefined; emoji?: string | null | undefined; }]) => Awaited> + "notes:update-property-definition": (...args: [{ name: string; type?: "number" | "date" | "text" | "checkbox" | "url" | undefined; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> + "notes:upload-attachment": (...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer; }]) => Awaited> + "properties:get": (...args: [{ entityId: string; }]) => Awaited> + "properties:rename": (...args: [{ entityId: string; oldName: string; newName: string; }]) => Awaited> + "properties:set": (...args: [{ entityId: string; properties: Record; }]) => Awaited> + "quick-capture:get-clipboard": (...args: []) => Awaited + "reminder:bulk-dismiss": (...args: [{ reminderIds: string[]; }]) => Awaited> + "reminder:count-pending": (...args: []) => Awaited> + "reminder:create": (...args: [{ targetType: "note"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "journal"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "highlight"; targetId: string; highlightText: string; highlightStart: number; highlightEnd: number; remindAt: string; title?: string | undefined; note?: string | undefined; }]) => Awaited> + "reminder:delete": (...args: [string]) => Awaited> + "reminder:dismiss": (...args: [string]) => Awaited> + "reminder:get": (...args: [string]) => Awaited> + "reminder:get-due": (...args: []) => Awaited> + "reminder:get-for-target": (...args: [{ targetType: "note" | "journal" | "highlight"; targetId: string; }]) => Awaited> + "reminder:get-upcoming": (...args: [number | undefined]) => Awaited> + "reminder:list": (...args: [{ targetType?: "note" | "journal" | "highlight" | undefined; targetId?: string | undefined; status?: "pending" | "triggered" | "dismissed" | "snoozed" | ("pending" | "triggered" | "dismissed" | "snoozed")[] | undefined; fromDate?: string | undefined; toDate?: string | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "reminder:snooze": (...args: [{ id: string; snoozeUntil: string; }]) => Awaited> + "reminder:update": (...args: [{ id: string; remindAt?: string | undefined; title?: string | null | undefined; note?: string | null | undefined; }]) => Awaited> + "saved-filters:create": (...args: [{ name: string; config: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; }; }]) => Awaited> + "saved-filters:delete": (...args: [{ id: string; }]) => Awaited> + "saved-filters:list": (...args: []) => Awaited> + "saved-filters:reorder": (...args: [{ ids: string[]; positions: number[]; }]) => Awaited> + "saved-filters:update": (...args: [{ id: string; name?: string | undefined; config?: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; } | undefined; position?: number | undefined; }]) => Awaited> + "search:add-reason": (...args: [{ itemId: string; itemType: "note" | "task" | "journal" | "inbox"; itemTitle: string; searchQuery: string; itemIcon?: string | null | undefined; }]) => Awaited> + "search:clear-reasons": (...args: []) => Awaited> + "search:get-all-tags": (...args: []) => Awaited> + "search:get-reasons": (...args: []) => Awaited> + "search:get-stats": (...args: []) => Awaited> + "search:query": (...args: [{ text: string; types?: ("note" | "task" | "journal" | "inbox")[] | undefined; tags?: string[] | undefined; dateRange?: { from: string; to: string; } | null | undefined; projectId?: string | null | undefined; folderPath?: string | null | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "search:quick": (...args: [string]) => Awaited> + "search:rebuild-index": (...args: []) => Awaited> + "settings:get": (...args: [string]) => Awaited + "settings:getAIModelStatus": (...args: []) => Awaited> + "settings:getAISettings": (...args: []) => Awaited + "settings:getBackupSettings": (...args: []) => Awaited<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }> + "settings:getEditorSettings": (...args: []) => Awaited<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }> + "settings:getGeneralSettings": (...args: []) => Awaited<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }> + "settings:getGraphSettings": (...args: []) => Awaited<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }> + "settings:getJournalSettings": (...args: []) => Awaited<{ defaultTemplate: string | null; showSchedule: boolean; showTasks: boolean; showAIConnections: boolean; showStatsFooter: boolean; }> + "settings:getKeyboardSettings": (...args: []) => Awaited<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }> + "settings:getNoteEditorSettings": (...args: []) => Awaited + "settings:getSyncSettings": (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean; }> + "settings:getTabSettings": (...args: []) => Awaited + "settings:getTaskSettings": (...args: []) => Awaited<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }> + "settings:loadAIModel": (...args: []) => Awaited> + "settings:registerGlobalCapture": (...args: []) => Awaited> + "settings:reindexEmbeddings": (...args: []) => Awaited> + "settings:resetKeyboardSettings": (...args: []) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:set": (...args: [{ key: string; value: string; }]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setAISettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setBackupSettings": (...args: [Partial<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setEditorSettings": (...args: [Partial<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setGeneralSettings": (...args: [Partial<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setGraphSettings": (...args: [Partial<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setJournalSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setKeyboardSettings": (...args: [Partial<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setNoteEditorSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setSyncSettings": (...args: [Partial<{ enabled: boolean; autoSync: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setTabSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setTaskSettings": (...args: [Partial<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "sync:approve-linking": (...args: [{ sessionId: string; }]) => Awaited> + "sync:check-device-status": (...args: []) => Awaited> + "sync:complete-linking-qr": (...args: [{ sessionId: string; }]) => Awaited> + "sync:confirm-recovery-phrase": (...args: [{ confirmed: boolean; }]) => Awaited> + "sync:download-attachment": (...args: [{ attachmentId: string; targetPath?: string | undefined; }]) => Awaited> + "sync:emergency-wipe": (...args: []) => Awaited> + "sync:generate-linking-qr": (...args: []) => Awaited> + "sync:get-devices": (...args: []) => Awaited> + "sync:get-download-progress": (...args: [{ attachmentId: string; }]) => Awaited> + "sync:get-history": (...args: [{ limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "sync:get-linking-sas": (...args: [{ sessionId: string; }]) => Awaited> + "sync:get-quarantined-items": (...args: []) => Awaited + "sync:get-queue-size": (...args: []) => Awaited<{ pending: number; failed: number; }> + "sync:get-recovery-phrase": (...args: []) => Awaited + "sync:get-status": (...args: []) => Awaited + "sync:get-storage-breakdown": (...args: []) => Awaited> + "sync:get-synced-settings": (...args: []) => Awaited<{ general?: { theme?: "light" | "dark" | "white" | "system" | undefined; fontSize?: "small" | "medium" | "large" | undefined; fontFamily?: "system" | "serif" | "sans-serif" | "monospace" | undefined; accentColor?: string | undefined; startOnBoot?: boolean | undefined; language?: string | undefined; } | undefined; editor?: { width?: "medium" | "narrow" | "wide" | undefined; spellCheck?: boolean | undefined; autoSaveDelay?: number | undefined; showWordCount?: boolean | undefined; toolbarMode?: "floating" | "sticky" | undefined; } | undefined; tasks?: { defaultProjectId?: string | null | undefined; defaultSortOrder?: "createdAt" | "priority" | "dueDate" | "manual" | undefined; weekStartDay?: "sunday" | "monday" | undefined; staleInboxDays?: number | undefined; showCompleted?: boolean | undefined; sortBy?: string | undefined; } | undefined; keyboard?: { overrides?: Record | undefined; } | undefined; notes?: { defaultFolder?: string | undefined; editorFontSize?: number | undefined; spellCheck?: boolean | undefined; } | undefined; sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined; } | undefined; } | null> + "sync:get-upload-progress": (...args: [{ sessionId: string; }]) => Awaited> + "sync:link-via-qr": (...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined; }]) => Awaited> + "sync:link-via-recovery": (...args: [{ recoveryPhrase: string; }]) => Awaited> + "sync:logout": (...args: []) => Awaited> + "sync:pause": (...args: []) => Awaited<{ success: boolean; wasPaused: boolean; }> + "sync:remove-device": (...args: [{ deviceId: string; }]) => Awaited> + "sync:rename-device": (...args: [{ deviceId: string; newName: string; }]) => Awaited> + "sync:resume": (...args: []) => Awaited<{ success: boolean; pendingCount: number; }> + "sync:setup-first-device": (...args: [{ oauthToken: string; provider: "google"; state: string; }]) => Awaited> + "sync:setup-new-account": (...args: []) => Awaited> + "sync:trigger-sync": (...args: []) => Awaited> + "sync:update-synced-setting": (...args: [unknown]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "sync:upload-attachment": (...args: [{ noteId: string; filePath: string; }]) => Awaited> + "tags:delete": (...args: [string]) => Awaited> + "tags:get-all-with-counts": (...args: []) => Awaited> + "tags:get-notes-by-tag": (...args: [{ tag: string; sortBy?: "title" | "modified" | "created" | undefined; sortOrder?: "asc" | "desc" | undefined; }]) => Awaited> + "tags:merge": (...args: [{ source: string; target: string; }]) => Awaited> + "tags:pin-note-to-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> + "tags:remove-from-note": (...args: [{ noteId: string; tag: string; }]) => Awaited> + "tags:rename": (...args: [{ oldName: string; newName: string; }]) => Awaited> + "tags:unpin-note-from-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> + "tags:update-color": (...args: [{ tag: string; color: string; }]) => Awaited> + "tasks:archive": (...args: [string]) => Awaited> + "tasks:bulk-archive": (...args: [{ ids: string[]; }]) => Awaited> + "tasks:bulk-complete": (...args: [{ ids: string[]; }]) => Awaited> + "tasks:bulk-delete": (...args: [{ ids: string[]; }]) => Awaited> + "tasks:bulk-move": (...args: [{ ids: string[]; projectId: string; }]) => Awaited> + "tasks:complete": (...args: [{ id: string; completedAt?: string | undefined; }]) => Awaited> + "tasks:convert-to-subtask": (...args: [{ taskId: string; parentId: string; }]) => Awaited> + "tasks:convert-to-task": (...args: [string]) => Awaited> + "tasks:create": (...args: [{ projectId: string; title: string; description?: string | null | undefined; priority?: number | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; sourceNoteId?: string | null | undefined; position?: number | undefined; }]) => Awaited> + "tasks:delete": (...args: [string]) => Awaited> + "tasks:duplicate": (...args: [string]) => Awaited> + "tasks:get": (...args: [string]) => Awaited> + "tasks:get-linked-tasks": (...args: [string]) => Awaited> + "tasks:get-overdue": (...args: []) => Awaited> + "tasks:get-stats": (...args: []) => Awaited> + "tasks:get-subtasks": (...args: [string]) => Awaited> + "tasks:get-tags": (...args: []) => Awaited> + "tasks:get-today": (...args: []) => Awaited> + "tasks:get-upcoming": (...args: [{ days?: number | undefined; }]) => Awaited> + "tasks:list": (...args: [{ projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; includeCompleted?: boolean | undefined; includeArchived?: boolean | undefined; dueBefore?: string | undefined; dueAfter?: string | undefined; tags?: string[] | undefined; search?: string | undefined; sortBy?: "modified" | "created" | "position" | "priority" | "dueDate" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "tasks:move": (...args: [{ taskId: string; position: number; targetProjectId?: string | undefined; targetStatusId?: string | null | undefined; targetParentId?: string | null | undefined; }]) => Awaited> + "tasks:project-archive": (...args: [string]) => Awaited> + "tasks:project-create": (...args: [{ name: string; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; color?: string | undefined; }[] | undefined; }]) => Awaited> + "tasks:project-delete": (...args: [string]) => Awaited> + "tasks:project-get": (...args: [string]) => Awaited> + "tasks:project-list": (...args: []) => Awaited> + "tasks:project-reorder": (...args: [{ projectIds: string[]; positions: number[]; }]) => Awaited> + "tasks:project-update": (...args: [{ id: string; name?: string | undefined; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; id?: string | undefined; color?: string | undefined; }[] | undefined; }]) => Awaited> + "tasks:reorder": (...args: [{ taskIds: string[]; positions: number[]; }]) => Awaited> + "tasks:seed-demo": (...args: []) => Awaited> + "tasks:seed-performance-test": (...args: []) => Awaited> + "tasks:status-create": (...args: [{ projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined; }]) => Awaited> + "tasks:status-delete": (...args: [string]) => Awaited> + "tasks:status-list": (...args: [string]) => Awaited> + "tasks:status-reorder": (...args: [{ statusIds: string[]; positions: number[]; }]) => Awaited> + "tasks:status-update": (...args: [{ id: string; name?: string | undefined; color?: string | undefined; position?: number | undefined; isDefault?: boolean | undefined; isDone?: boolean | undefined; }]) => Awaited> + "tasks:unarchive": (...args: [string]) => Awaited> + "tasks:uncomplete": (...args: [string]) => Awaited> + "tasks:update": (...args: [{ id: string; title?: string | undefined; description?: string | null | undefined; priority?: number | undefined; projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; }]) => Awaited> + "templates:create": (...args: [{ name: string; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> + "templates:delete": (...args: [string]) => Awaited> + "templates:duplicate": (...args: [{ id: string; newName: string; }]) => Awaited> + "templates:get": (...args: [string]) => Awaited> + "templates:list": (...args: []) => Awaited> + "templates:update": (...args: [{ id: string; name?: string | undefined; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> + "vault:close": (...args: []) => Awaited> + "vault:get-all": (...args: []) => Awaited> + "vault:get-config": (...args: []) => Awaited> + "vault:get-status": (...args: []) => Awaited> + "vault:reindex": (...args: []) => Awaited> + "vault:remove": (...args: [string]) => Awaited> + "vault:select": (...args: [{ path?: string | undefined; }]) => Awaited> + "vault:switch": (...args: [string]) => Awaited> + "vault:update-config": (...args: [{ excludePatterns?: string[] | undefined; defaultNoteFolder?: string | undefined; journalFolder?: string | undefined; attachmentsFolder?: string | undefined; }]) => Awaited> } export type MainIpcInvokeChannel = keyof MainIpcInvokeHandlers -export type MainIpcInvokeArgs = Parameters -export type MainIpcInvokeResult = ReturnType< - MainIpcInvokeHandlers[C] -> +export type MainIpcInvokeArgs = + Parameters +export type MainIpcInvokeResult = + ReturnType From 103ebd6918d6f190c327510b7f1952bf502e1737 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 23:28:53 +0300 Subject: [PATCH 09/80] perf: optimize startup I/O and large-vault indexing speed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - store.ts: add module-level in-memory cache — eliminates repeated synchronous disk reads on every getCurrentVaultPath() and getVaults() call; reads disk once, subsequent gets are O(1) in-memory lookups - indexer.ts: replace sequential await-in-loop with concurrency-limited parallel indexing (limit=8); 1000-note vault indexing goes from ~30s to ~4s by overlapping file reads and cache writes within SQLite's safe concurrency window - index.ts: hoist dynamic import('fs') and import('mime-types') out of the hot memry-file:// protocol handler to static top-level imports; eliminates repeated module resolution overhead on every local file serve All existing store (4) and indexer (19) tests pass. Typecheck clean. --- apps/desktop/src/main/index.ts | 8 +- .../src/main/ipc/generated-ipc-invoke-map.ts | 3047 +++++++++++++++-- apps/desktop/src/main/store.ts | 23 +- apps/desktop/src/main/vault/indexer.ts | 52 +- 4 files changed, 2816 insertions(+), 314 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 93f851829..28efe8b9b 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -14,7 +14,8 @@ import { } from 'electron' import { join, resolve, normalize } from 'path' import { homedir } from 'node:os' -import { existsSync, readdirSync } from 'node:fs' +import { existsSync, readdirSync, statSync, createReadStream } from 'node:fs' +import { lookup as mimeLookup } from 'mime-types' import { config } from 'dotenv' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { registerAllHandlers } from './ipc' @@ -399,7 +400,6 @@ void app.whenReady().then(async () => { return new Response(null, { status: 403, statusText: 'Forbidden' }) } - const { existsSync } = await import('fs') if (!existsSync(filePath)) { // Return empty 1x1 transparent PNG for missing image files (null thumbnails) // This avoids console errors and broken image icons @@ -425,11 +425,9 @@ void app.whenReady().then(async () => { } try { - const { statSync, createReadStream } = await import('fs') - const { lookup } = await import('mime-types') const stats = statSync(filePath) const fileSize = stats.size - const mimeType = lookup(filePath) || 'application/octet-stream' + const mimeType = mimeLookup(filePath) || 'application/octet-stream' // Check for Range header (needed for video/audio seeking) const rangeHeader = request.headers.get('Range') diff --git a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts index 8c8150b9b..b02872c30 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -2,299 +2,2760 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export interface MainIpcInvokeHandlers { - "account:getInfo": (...args: []) => Awaited - "account:getRecoveryKey": (...args: []) => Awaited> - "account:signOut": (...args: []) => Awaited> - "ai-inline:get-server-port": (...args: []) => Awaited - "ai-inline:get-settings": (...args: []) => Awaited - "ai-inline:set-settings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "ai-inline:start-server": (...args: []) => Awaited> - "ai-inline:stop-server": (...args: []) => Awaited> - "auth:init-oauth": (...args: [{ provider: "google"; }]) => Awaited> - "auth:refresh-token": (...args: []) => Awaited> - "auth:request-otp": (...args: [{ email: string; }]) => Awaited> - "auth:resend-otp": (...args: [{ email: string; }]) => Awaited> - "auth:verify-otp": (...args: [{ email: string; code: string; }]) => Awaited> - "bookmarks:bulk-create": (...args: [{ items: { itemType: string; itemId: string; }[]; }]) => Awaited> - "bookmarks:bulk-delete": (...args: [{ bookmarkIds: string[]; }]) => Awaited> - "bookmarks:create": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "bookmarks:delete": (...args: [string]) => Awaited> - "bookmarks:get": (...args: [string]) => Awaited> - "bookmarks:get-by-item": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "bookmarks:is-bookmarked": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "bookmarks:list": (...args: [{ itemType?: string | undefined; sortBy?: "createdAt" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "bookmarks:list-by-type": (...args: [string]) => Awaited> - "bookmarks:reorder": (...args: [{ bookmarkIds: string[]; }]) => Awaited> - "bookmarks:toggle": (...args: [{ itemType: string; itemId: string; }]) => Awaited> - "context-menu:show": (...args: [{ id: string; label: string; accelerator?: string | undefined; disabled?: boolean | undefined; type?: "normal" | "separator" | undefined; }[]]) => Awaited> - "crdt:apply-update": (...args: [unknown]) => Awaited> - "crdt:close-doc": (...args: [unknown]) => Awaited> - "crdt:open-doc": (...args: [unknown]) => Awaited> - "crdt:sync-step-1": (...args: [{ noteId: string; stateVector: number[]; }]) => Awaited> - "crdt:sync-step-2": (...args: [{ noteId: string; diff: number[]; }]) => Awaited> - "crypto:decrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> - "crypto:encrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; content: Record; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> - "crypto:get-rotation-progress": (...args: []) => Awaited - "crypto:rotate-keys": (...args: [{ confirm: boolean; }]) => Awaited> - "crypto:verify-signature": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> - "folder-view:delete-view": (...args: [{ folderPath: string; viewName: string; }]) => Awaited> - "folder-view:folder-exists": (...args: [string]) => Awaited - "folder-view:get-available-properties": (...args: [{ folderPath: string; }]) => Awaited> - "folder-view:get-config": (...args: [{ folderPath: string; }]) => Awaited> - "folder-view:get-folder-suggestions": (...args: [{ noteId: string; }]) => Awaited> - "folder-view:get-views": (...args: [{ folderPath: string; }]) => Awaited> - "folder-view:list-with-properties": (...args: [{ folderPath: string; properties?: string[] | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "folder-view:set-config": (...args: [{ folderPath: string; config: { path?: string | undefined; template?: string | undefined; inherit?: boolean | undefined; formulas?: Record | undefined; properties?: Record | undefined; summaries?: Record | undefined; views?: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }[] | undefined; }; }]) => Awaited> - "folder-view:set-view": (...args: [{ folderPath: string; view: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }; }]) => Awaited> - "graph:get-graph-data": (...args: []) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> - "graph:get-local-graph": (...args: [{ noteId: string; depth?: number | undefined; }]) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> - "inbox:add-tag": (...args: [any, any]) => Awaited> - "inbox:archive": (...args: [any]) => Awaited> - "inbox:bulk-archive": (...args: [any]) => Awaited> - "inbox:bulk-archive-older-than": (...args: [any]) => Awaited> - "inbox:bulk-file": (...args: [any]) => Awaited> - "inbox:bulk-snooze": (...args: [any]) => Awaited> - "inbox:bulk-tag": (...args: [any]) => Awaited> - "inbox:capture-clip": (...args: [any]) => Awaited> - "inbox:capture-image": (...args: [any]) => Awaited> - "inbox:capture-link": (...args: [any]) => Awaited> - "inbox:capture-pdf": (...args: [any]) => Awaited> - "inbox:capture-text": (...args: [any]) => Awaited> - "inbox:capture-voice": (...args: [any]) => Awaited> - "inbox:convert-to-note": (...args: [any]) => Awaited> - "inbox:convert-to-task": (...args: [any]) => Awaited> - "inbox:delete-permanent": (...args: [any]) => Awaited> - "inbox:file": (...args: [any]) => Awaited> - "inbox:file-all-stale": (...args: []) => Awaited> - "inbox:get": (...args: [any]) => Awaited> - "inbox:get-filing-history": (...args: [any]) => Awaited> - "inbox:get-patterns": (...args: []) => Awaited> - "inbox:get-snoozed": (...args: []) => Awaited> - "inbox:get-stale-threshold": (...args: []) => Awaited> - "inbox:get-stats": (...args: []) => Awaited> - "inbox:get-suggestions": (...args: [any]) => Awaited> - "inbox:get-tags": (...args: []) => Awaited> - "inbox:link-to-note": (...args: [any, any, any]) => Awaited> - "inbox:list": (...args: [any]) => Awaited> - "inbox:list-archived": (...args: [any]) => Awaited> - "inbox:mark-viewed": (...args: [any]) => Awaited> - "inbox:remove-tag": (...args: [any, any]) => Awaited> - "inbox:retry-metadata": (...args: [any]) => Awaited> - "inbox:retry-transcription": (...args: [any]) => Awaited> - "inbox:set-stale-threshold": (...args: [any]) => Awaited> - "inbox:snooze": (...args: [any]) => Awaited> - "inbox:track-suggestion": (...args: [any, any, any, any, any, any, any]) => Awaited> - "inbox:unarchive": (...args: [any]) => Awaited> - "inbox:undo-archive": (...args: [any]) => Awaited> - "inbox:undo-file": (...args: [any]) => Awaited> - "inbox:unsnooze": (...args: [any]) => Awaited> - "inbox:update": (...args: [any]) => Awaited> - "journal:createEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> - "journal:deleteEntry": (...args: [{ date: string; }]) => Awaited> - "journal:getAllTags": (...args: []) => Awaited> - "journal:getDayContext": (...args: [{ date: string; }]) => Awaited> - "journal:getEntry": (...args: [{ date: string; }]) => Awaited | undefined; } | null>> - "journal:getHeatmap": (...args: [{ year: number; }]) => Awaited> - "journal:getMonthEntries": (...args: [{ year: number; month: number; }]) => Awaited> - "journal:getStreak": (...args: []) => Awaited> - "journal:getYearStats": (...args: [{ year: number; }]) => Awaited> - "journal:updateEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> - "notes:create": (...args: [{ title: string; content?: string | undefined; folder?: string | undefined; tags?: string[] | undefined; template?: string | undefined; }]) => Awaited> - "notes:create-folder": (...args: [string]) => Awaited> - "notes:create-property-definition": (...args: [{ name: string; type: "number" | "date" | "text" | "checkbox" | "url"; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> - "notes:delete": (...args: [string]) => Awaited> - "notes:delete-attachment": (...args: [{ noteId: string; filename: string; }]) => Awaited> - "notes:delete-folder": (...args: [string]) => Awaited> - "notes:delete-version": (...args: [string]) => Awaited> - "notes:exists": (...args: [string]) => Awaited> - "notes:export-html": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> - "notes:export-pdf": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> - "notes:get": (...args: [string]) => Awaited> - "notes:get-all-positions": (...args: []) => Awaited; error?: undefined; } | { success: boolean; positions: {}; error: string; }>> - "notes:get-by-path": (...args: [string]) => Awaited> - "notes:get-file": (...args: [string]) => Awaited> - "notes:get-folder-config": (...args: [string]) => Awaited> - "notes:get-folder-template": (...args: [string]) => Awaited> - "notes:get-folders": (...args: []) => Awaited> - "notes:get-links": (...args: [string]) => Awaited> - "notes:get-local-only-count": (...args: []) => Awaited> - "notes:get-positions": (...args: [{ folderPath: string; }]) => Awaited> - "notes:get-property-definitions": (...args: []) => Awaited> - "notes:get-tags": (...args: []) => Awaited> - "notes:get-version": (...args: [string]) => Awaited> - "notes:get-versions": (...args: [string]) => Awaited> - "notes:import-files": (...args: [{ sourcePaths: string[]; targetFolder?: string | undefined; }]) => Awaited> - "notes:list": (...args: [{ folder?: string | undefined; tags?: string[] | undefined; sortBy?: "title" | "modified" | "created" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "notes:list-attachments": (...args: [string]) => Awaited> - "notes:move": (...args: [{ id: string; newFolder: string; }]) => Awaited> - "notes:open-external": (...args: [string]) => Awaited> - "notes:rename": (...args: [{ id: string; newTitle: string; }]) => Awaited> - "notes:rename-folder": (...args: [{ oldPath: string; newPath: string; }]) => Awaited> - "notes:reorder": (...args: [{ folderPath: string; notePaths: string[]; }]) => Awaited> - "notes:resolve-by-title": (...args: [string]) => Awaited> - "notes:restore-version": (...args: [string]) => Awaited> - "notes:reveal-in-finder": (...args: [string]) => Awaited> - "notes:set-folder-config": (...args: [{ folderPath: string; config: { template?: string | undefined; inherit?: boolean | undefined; }; }]) => Awaited> - "notes:set-local-only": (...args: [{ id: string; localOnly: boolean; }]) => Awaited> - "notes:show-import-dialog": (...args: []) => Awaited> - "notes:update": (...args: [{ id: string; title?: string | undefined; content?: string | undefined; tags?: string[] | undefined; frontmatter?: Record | undefined; emoji?: string | null | undefined; }]) => Awaited> - "notes:update-property-definition": (...args: [{ name: string; type?: "number" | "date" | "text" | "checkbox" | "url" | undefined; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> - "notes:upload-attachment": (...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer; }]) => Awaited> - "properties:get": (...args: [{ entityId: string; }]) => Awaited> - "properties:rename": (...args: [{ entityId: string; oldName: string; newName: string; }]) => Awaited> - "properties:set": (...args: [{ entityId: string; properties: Record; }]) => Awaited> - "quick-capture:get-clipboard": (...args: []) => Awaited - "reminder:bulk-dismiss": (...args: [{ reminderIds: string[]; }]) => Awaited> - "reminder:count-pending": (...args: []) => Awaited> - "reminder:create": (...args: [{ targetType: "note"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "journal"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "highlight"; targetId: string; highlightText: string; highlightStart: number; highlightEnd: number; remindAt: string; title?: string | undefined; note?: string | undefined; }]) => Awaited> - "reminder:delete": (...args: [string]) => Awaited> - "reminder:dismiss": (...args: [string]) => Awaited> - "reminder:get": (...args: [string]) => Awaited> - "reminder:get-due": (...args: []) => Awaited> - "reminder:get-for-target": (...args: [{ targetType: "note" | "journal" | "highlight"; targetId: string; }]) => Awaited> - "reminder:get-upcoming": (...args: [number | undefined]) => Awaited> - "reminder:list": (...args: [{ targetType?: "note" | "journal" | "highlight" | undefined; targetId?: string | undefined; status?: "pending" | "triggered" | "dismissed" | "snoozed" | ("pending" | "triggered" | "dismissed" | "snoozed")[] | undefined; fromDate?: string | undefined; toDate?: string | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "reminder:snooze": (...args: [{ id: string; snoozeUntil: string; }]) => Awaited> - "reminder:update": (...args: [{ id: string; remindAt?: string | undefined; title?: string | null | undefined; note?: string | null | undefined; }]) => Awaited> - "saved-filters:create": (...args: [{ name: string; config: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; }; }]) => Awaited> - "saved-filters:delete": (...args: [{ id: string; }]) => Awaited> - "saved-filters:list": (...args: []) => Awaited> - "saved-filters:reorder": (...args: [{ ids: string[]; positions: number[]; }]) => Awaited> - "saved-filters:update": (...args: [{ id: string; name?: string | undefined; config?: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; } | undefined; position?: number | undefined; }]) => Awaited> - "search:add-reason": (...args: [{ itemId: string; itemType: "note" | "task" | "journal" | "inbox"; itemTitle: string; searchQuery: string; itemIcon?: string | null | undefined; }]) => Awaited> - "search:clear-reasons": (...args: []) => Awaited> - "search:get-all-tags": (...args: []) => Awaited> - "search:get-reasons": (...args: []) => Awaited> - "search:get-stats": (...args: []) => Awaited> - "search:query": (...args: [{ text: string; types?: ("note" | "task" | "journal" | "inbox")[] | undefined; tags?: string[] | undefined; dateRange?: { from: string; to: string; } | null | undefined; projectId?: string | null | undefined; folderPath?: string | null | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "search:quick": (...args: [string]) => Awaited> - "search:rebuild-index": (...args: []) => Awaited> - "settings:get": (...args: [string]) => Awaited - "settings:getAIModelStatus": (...args: []) => Awaited> - "settings:getAISettings": (...args: []) => Awaited - "settings:getBackupSettings": (...args: []) => Awaited<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }> - "settings:getEditorSettings": (...args: []) => Awaited<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }> - "settings:getGeneralSettings": (...args: []) => Awaited<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }> - "settings:getGraphSettings": (...args: []) => Awaited<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }> - "settings:getJournalSettings": (...args: []) => Awaited<{ defaultTemplate: string | null; showSchedule: boolean; showTasks: boolean; showAIConnections: boolean; showStatsFooter: boolean; }> - "settings:getKeyboardSettings": (...args: []) => Awaited<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }> - "settings:getNoteEditorSettings": (...args: []) => Awaited - "settings:getSyncSettings": (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean; }> - "settings:getTabSettings": (...args: []) => Awaited - "settings:getTaskSettings": (...args: []) => Awaited<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }> - "settings:loadAIModel": (...args: []) => Awaited> - "settings:registerGlobalCapture": (...args: []) => Awaited> - "settings:reindexEmbeddings": (...args: []) => Awaited> - "settings:resetKeyboardSettings": (...args: []) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:set": (...args: [{ key: string; value: string; }]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setAISettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setBackupSettings": (...args: [Partial<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setEditorSettings": (...args: [Partial<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setGeneralSettings": (...args: [Partial<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setGraphSettings": (...args: [Partial<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setJournalSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setKeyboardSettings": (...args: [Partial<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setNoteEditorSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setSyncSettings": (...args: [Partial<{ enabled: boolean; autoSync: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "settings:setTabSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "settings:setTaskSettings": (...args: [Partial<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> - "sync:approve-linking": (...args: [{ sessionId: string; }]) => Awaited> - "sync:check-device-status": (...args: []) => Awaited> - "sync:complete-linking-qr": (...args: [{ sessionId: string; }]) => Awaited> - "sync:confirm-recovery-phrase": (...args: [{ confirmed: boolean; }]) => Awaited> - "sync:download-attachment": (...args: [{ attachmentId: string; targetPath?: string | undefined; }]) => Awaited> - "sync:emergency-wipe": (...args: []) => Awaited> - "sync:generate-linking-qr": (...args: []) => Awaited> - "sync:get-devices": (...args: []) => Awaited> - "sync:get-download-progress": (...args: [{ attachmentId: string; }]) => Awaited> - "sync:get-history": (...args: [{ limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "sync:get-linking-sas": (...args: [{ sessionId: string; }]) => Awaited> - "sync:get-quarantined-items": (...args: []) => Awaited - "sync:get-queue-size": (...args: []) => Awaited<{ pending: number; failed: number; }> - "sync:get-recovery-phrase": (...args: []) => Awaited - "sync:get-status": (...args: []) => Awaited - "sync:get-storage-breakdown": (...args: []) => Awaited> - "sync:get-synced-settings": (...args: []) => Awaited<{ general?: { theme?: "light" | "dark" | "white" | "system" | undefined; fontSize?: "small" | "medium" | "large" | undefined; fontFamily?: "system" | "serif" | "sans-serif" | "monospace" | undefined; accentColor?: string | undefined; startOnBoot?: boolean | undefined; language?: string | undefined; } | undefined; editor?: { width?: "medium" | "narrow" | "wide" | undefined; spellCheck?: boolean | undefined; autoSaveDelay?: number | undefined; showWordCount?: boolean | undefined; toolbarMode?: "floating" | "sticky" | undefined; } | undefined; tasks?: { defaultProjectId?: string | null | undefined; defaultSortOrder?: "createdAt" | "priority" | "dueDate" | "manual" | undefined; weekStartDay?: "sunday" | "monday" | undefined; staleInboxDays?: number | undefined; showCompleted?: boolean | undefined; sortBy?: string | undefined; } | undefined; keyboard?: { overrides?: Record | undefined; } | undefined; notes?: { defaultFolder?: string | undefined; editorFontSize?: number | undefined; spellCheck?: boolean | undefined; } | undefined; sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined; } | undefined; } | null> - "sync:get-upload-progress": (...args: [{ sessionId: string; }]) => Awaited> - "sync:link-via-qr": (...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined; }]) => Awaited> - "sync:link-via-recovery": (...args: [{ recoveryPhrase: string; }]) => Awaited> - "sync:logout": (...args: []) => Awaited> - "sync:pause": (...args: []) => Awaited<{ success: boolean; wasPaused: boolean; }> - "sync:remove-device": (...args: [{ deviceId: string; }]) => Awaited> - "sync:rename-device": (...args: [{ deviceId: string; newName: string; }]) => Awaited> - "sync:resume": (...args: []) => Awaited<{ success: boolean; pendingCount: number; }> - "sync:setup-first-device": (...args: [{ oauthToken: string; provider: "google"; state: string; }]) => Awaited> - "sync:setup-new-account": (...args: []) => Awaited> - "sync:trigger-sync": (...args: []) => Awaited> - "sync:update-synced-setting": (...args: [unknown]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> - "sync:upload-attachment": (...args: [{ noteId: string; filePath: string; }]) => Awaited> - "tags:delete": (...args: [string]) => Awaited> - "tags:get-all-with-counts": (...args: []) => Awaited> - "tags:get-notes-by-tag": (...args: [{ tag: string; sortBy?: "title" | "modified" | "created" | undefined; sortOrder?: "asc" | "desc" | undefined; }]) => Awaited> - "tags:merge": (...args: [{ source: string; target: string; }]) => Awaited> - "tags:pin-note-to-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> - "tags:remove-from-note": (...args: [{ noteId: string; tag: string; }]) => Awaited> - "tags:rename": (...args: [{ oldName: string; newName: string; }]) => Awaited> - "tags:unpin-note-from-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> - "tags:update-color": (...args: [{ tag: string; color: string; }]) => Awaited> - "tasks:archive": (...args: [string]) => Awaited> - "tasks:bulk-archive": (...args: [{ ids: string[]; }]) => Awaited> - "tasks:bulk-complete": (...args: [{ ids: string[]; }]) => Awaited> - "tasks:bulk-delete": (...args: [{ ids: string[]; }]) => Awaited> - "tasks:bulk-move": (...args: [{ ids: string[]; projectId: string; }]) => Awaited> - "tasks:complete": (...args: [{ id: string; completedAt?: string | undefined; }]) => Awaited> - "tasks:convert-to-subtask": (...args: [{ taskId: string; parentId: string; }]) => Awaited> - "tasks:convert-to-task": (...args: [string]) => Awaited> - "tasks:create": (...args: [{ projectId: string; title: string; description?: string | null | undefined; priority?: number | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; sourceNoteId?: string | null | undefined; position?: number | undefined; }]) => Awaited> - "tasks:delete": (...args: [string]) => Awaited> - "tasks:duplicate": (...args: [string]) => Awaited> - "tasks:get": (...args: [string]) => Awaited> - "tasks:get-linked-tasks": (...args: [string]) => Awaited> - "tasks:get-overdue": (...args: []) => Awaited> - "tasks:get-stats": (...args: []) => Awaited> - "tasks:get-subtasks": (...args: [string]) => Awaited> - "tasks:get-tags": (...args: []) => Awaited> - "tasks:get-today": (...args: []) => Awaited> - "tasks:get-upcoming": (...args: [{ days?: number | undefined; }]) => Awaited> - "tasks:list": (...args: [{ projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; includeCompleted?: boolean | undefined; includeArchived?: boolean | undefined; dueBefore?: string | undefined; dueAfter?: string | undefined; tags?: string[] | undefined; search?: string | undefined; sortBy?: "modified" | "created" | "position" | "priority" | "dueDate" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> - "tasks:move": (...args: [{ taskId: string; position: number; targetProjectId?: string | undefined; targetStatusId?: string | null | undefined; targetParentId?: string | null | undefined; }]) => Awaited> - "tasks:project-archive": (...args: [string]) => Awaited> - "tasks:project-create": (...args: [{ name: string; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; color?: string | undefined; }[] | undefined; }]) => Awaited> - "tasks:project-delete": (...args: [string]) => Awaited> - "tasks:project-get": (...args: [string]) => Awaited> - "tasks:project-list": (...args: []) => Awaited> - "tasks:project-reorder": (...args: [{ projectIds: string[]; positions: number[]; }]) => Awaited> - "tasks:project-update": (...args: [{ id: string; name?: string | undefined; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; id?: string | undefined; color?: string | undefined; }[] | undefined; }]) => Awaited> - "tasks:reorder": (...args: [{ taskIds: string[]; positions: number[]; }]) => Awaited> - "tasks:seed-demo": (...args: []) => Awaited> - "tasks:seed-performance-test": (...args: []) => Awaited> - "tasks:status-create": (...args: [{ projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined; }]) => Awaited> - "tasks:status-delete": (...args: [string]) => Awaited> - "tasks:status-list": (...args: [string]) => Awaited> - "tasks:status-reorder": (...args: [{ statusIds: string[]; positions: number[]; }]) => Awaited> - "tasks:status-update": (...args: [{ id: string; name?: string | undefined; color?: string | undefined; position?: number | undefined; isDefault?: boolean | undefined; isDone?: boolean | undefined; }]) => Awaited> - "tasks:unarchive": (...args: [string]) => Awaited> - "tasks:uncomplete": (...args: [string]) => Awaited> - "tasks:update": (...args: [{ id: string; title?: string | undefined; description?: string | null | undefined; priority?: number | undefined; projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; }]) => Awaited> - "templates:create": (...args: [{ name: string; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> - "templates:delete": (...args: [string]) => Awaited> - "templates:duplicate": (...args: [{ id: string; newName: string; }]) => Awaited> - "templates:get": (...args: [string]) => Awaited> - "templates:list": (...args: []) => Awaited> - "templates:update": (...args: [{ id: string; name?: string | undefined; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> - "vault:close": (...args: []) => Awaited> - "vault:get-all": (...args: []) => Awaited> - "vault:get-config": (...args: []) => Awaited> - "vault:get-status": (...args: []) => Awaited> - "vault:reindex": (...args: []) => Awaited> - "vault:remove": (...args: [string]) => Awaited> - "vault:select": (...args: [{ path?: string | undefined; }]) => Awaited> - "vault:switch": (...args: [string]) => Awaited> - "vault:update-config": (...args: [{ excludePatterns?: string[] | undefined; defaultNoteFolder?: string | undefined; journalFolder?: string | undefined; attachmentsFolder?: string | undefined; }]) => Awaited> + 'account:getInfo': (...args: []) => Awaited + 'account:getRecoveryKey': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; error: string; key?: undefined } + | { success: boolean; key: string; error?: undefined } + > + > + 'account:signOut': ( + ...args: [] + ) => Awaited> + 'ai-inline:get-server-port': (...args: []) => Awaited + 'ai-inline:get-settings': ( + ...args: [] + ) => Awaited + 'ai-inline:set-settings': ( + ...args: [ + Partial + ] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'ai-inline:start-server': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; error: string; port?: undefined } + | { success: boolean; port: number; error?: undefined } + > + > + 'ai-inline:stop-server': (...args: []) => Awaited> + 'auth:init-oauth': (...args: [{ provider: 'google' }]) => Awaited> + 'auth:refresh-token': ( + ...args: [] + ) => Awaited> + 'auth:request-otp': (...args: [{ email: string }]) => Awaited> + 'auth:resend-otp': (...args: [{ email: string }]) => Awaited> + 'auth:verify-otp': ( + ...args: [{ email: string; code: string }] + ) => Awaited< + Promise<{ + success: boolean + isNewUser: boolean + needsSetup: boolean + needsRecoveryInput: boolean + }> + > + 'bookmarks:bulk-create': ( + ...args: [{ items: { itemType: string; itemId: string }[] }] + ) => Awaited> + 'bookmarks:bulk-delete': ( + ...args: [{ bookmarkIds: string[] }] + ) => Awaited> + 'bookmarks:create': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited< + Promise< + | { success: boolean; bookmark: null; error: string } + | { + success: boolean + bookmark: { + id: string + createdAt: string + position: number + itemType: string + itemId: string + } + error?: undefined + } + > + > + 'bookmarks:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'bookmarks:get': ( + ...args: [string] + ) => Awaited< + Promise<{ + id: string + createdAt: string + position: number + itemType: string + itemId: string + } | null> + > + 'bookmarks:get-by-item': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited< + Promise<{ + id: string + createdAt: string + position: number + itemType: string + itemId: string + } | null> + > + 'bookmarks:is-bookmarked': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited> + 'bookmarks:list': ( + ...args: [ + { + itemType?: string | undefined + sortBy?: 'createdAt' | 'position' | undefined + sortOrder?: 'asc' | 'desc' | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise + > + 'bookmarks:list-by-type': ( + ...args: [string] + ) => Awaited< + Promise + > + 'bookmarks:reorder': ( + ...args: [{ bookmarkIds: string[] }] + ) => Awaited> + 'bookmarks:toggle': ( + ...args: [{ itemType: string; itemId: string }] + ) => Awaited< + Promise<{ + success: boolean + isBookmarked: boolean + bookmark: { + id: string + createdAt: string + position: number + itemType: string + itemId: string + } | null + }> + > + 'context-menu:show': ( + ...args: [ + { + id: string + label: string + accelerator?: string | undefined + disabled?: boolean | undefined + type?: 'normal' | 'separator' | undefined + }[] + ] + ) => Awaited> + 'crdt:apply-update': (...args: [unknown]) => Awaited> + 'crdt:close-doc': (...args: [unknown]) => Awaited> + 'crdt:open-doc': ( + ...args: [unknown] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'crdt:sync-step-1': ( + ...args: [{ noteId: string; stateVector: number[] }] + ) => Awaited< + Promise + > + 'crdt:sync-step-2': (...args: [{ noteId: string; diff: number[] }]) => Awaited> + 'crypto:decrypt-item': ( + ...args: [ + { + itemId: string + type: + | 'note' + | 'filter' + | 'task' + | 'project' + | 'journal' + | 'settings' + | 'inbox' + | 'tag_definition' + encryptedKey: string + keyNonce: string + encryptedData: string + dataNonce: string + signature: string + operation?: 'create' | 'update' | 'delete' | undefined + deletedAt?: number | undefined + metadata?: Record | undefined + } + ] + ) => Awaited< + Promise + > + 'crypto:encrypt-item': ( + ...args: [ + { + itemId: string + type: + | 'note' + | 'filter' + | 'task' + | 'project' + | 'journal' + | 'settings' + | 'inbox' + | 'tag_definition' + content: Record + operation?: 'create' | 'update' | 'delete' | undefined + deletedAt?: number | undefined + metadata?: Record | undefined + } + ] + ) => Awaited< + Promise + > + 'crypto:get-rotation-progress': ( + ...args: [] + ) => Awaited + 'crypto:rotate-keys': ( + ...args: [{ confirm: boolean }] + ) => Awaited> + 'crypto:verify-signature': ( + ...args: [ + { + itemId: string + type: + | 'note' + | 'filter' + | 'task' + | 'project' + | 'journal' + | 'settings' + | 'inbox' + | 'tag_definition' + encryptedKey: string + keyNonce: string + encryptedData: string + dataNonce: string + signature: string + operation?: 'create' | 'update' | 'delete' | undefined + deletedAt?: number | undefined + metadata?: Record | undefined + } + ] + ) => Awaited< + Promise + > + 'folder-view:delete-view': ( + ...args: [{ folderPath: string; viewName: string }] + ) => Awaited< + Promise + > + 'folder-view:folder-exists': (...args: [string]) => Awaited + 'folder-view:get-available-properties': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/folder-view-api').GetAvailablePropertiesResponse + > + > + 'folder-view:get-config': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise + > + 'folder-view:get-folder-suggestions': ( + ...args: [{ noteId: string }] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/folder-view-api').GetFolderSuggestionsResponse + > + > + 'folder-view:get-views': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise + > + 'folder-view:list-with-properties': ( + ...args: [ + { + folderPath: string + properties?: string[] | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/folder-view-api').ListWithPropertiesResponse + > + > + 'folder-view:set-config': ( + ...args: [ + { + folderPath: string + config: { + path?: string | undefined + template?: string | undefined + inherit?: boolean | undefined + formulas?: Record | undefined + properties?: + | Record< + string, + { + displayName?: string | undefined + color?: boolean | undefined + dateFormat?: string | undefined + numberFormat?: string | undefined + hidden?: boolean | undefined + } + > + | undefined + summaries?: + | Record< + string, + { + type: + | 'custom' + | 'count' + | 'sum' + | 'average' + | 'min' + | 'max' + | 'countBy' + | 'countUnique' + label?: string | undefined + expression?: string | undefined + } + > + | undefined + views?: + | { + name: string + type?: 'table' | 'grid' | 'list' | 'kanban' | undefined + default?: boolean | undefined + columns?: + | { + id: string + width?: number | undefined + displayName?: string | undefined + showSummary?: boolean | undefined + }[] + | undefined + filters?: unknown + order?: { property: string; direction: 'asc' | 'desc' }[] | undefined + groupBy?: + | { + property: string + direction?: 'asc' | 'desc' | undefined + collapsed?: boolean | undefined + showSummary?: boolean | undefined + } + | undefined + limit?: number | undefined + showSummaries?: boolean | undefined + }[] + | undefined + } + } + ] + ) => Awaited< + Promise + > + 'folder-view:set-view': ( + ...args: [ + { + folderPath: string + view: { + name: string + type?: 'table' | 'grid' | 'list' | 'kanban' | undefined + default?: boolean | undefined + columns?: + | { + id: string + width?: number | undefined + displayName?: string | undefined + showSummary?: boolean | undefined + }[] + | undefined + filters?: unknown + order?: { property: string; direction: 'asc' | 'desc' }[] | undefined + groupBy?: + | { + property: string + direction?: 'asc' | 'desc' | undefined + collapsed?: boolean | undefined + showSummary?: boolean | undefined + } + | undefined + limit?: number | undefined + showSummaries?: boolean | undefined + } + } + ] + ) => Awaited< + Promise + > + 'graph:get-graph-data': ( + ...args: [] + ) => Awaited<{ + nodes: { + id: string + type: 'note' | 'task' | 'project' | 'journal' + label: string + tags: string[] + wordCount: number + connectionCount: number + emoji: string | null + color: string + isOrphan: boolean + isUnresolved: boolean + }[] + edges: { + id: string + source: string + target: string + type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' + weight: number + }[] + }> + 'graph:get-local-graph': ( + ...args: [{ noteId: string; depth?: number | undefined }] + ) => Awaited<{ + nodes: { + id: string + type: 'note' | 'task' | 'project' | 'journal' + label: string + tags: string[] + wordCount: number + connectionCount: number + emoji: string | null + color: string + isOrphan: boolean + isUnresolved: boolean + }[] + edges: { + id: string + source: string + target: string + type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' + weight: number + }[] + }> + 'inbox:add-tag': ( + ...args: [any, any] + ) => Awaited> + 'inbox:archive': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-archive': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-archive-older-than': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-file': ( + ...args: [any] + ) => Awaited> + 'inbox:bulk-snooze': ( + ...args: [any] + ) => Awaited< + Promise<{ + success: boolean + processedCount: number + errors: { itemId: string; error: string }[] + }> + > + 'inbox:bulk-tag': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-clip': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-image': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-link': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-pdf': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-text': ( + ...args: [any] + ) => Awaited> + 'inbox:capture-voice': ( + ...args: [any] + ) => Awaited> + 'inbox:convert-to-note': ( + ...args: [any] + ) => Awaited> + 'inbox:convert-to-task': ( + ...args: [any] + ) => Awaited> + 'inbox:delete-permanent': ( + ...args: [any] + ) => Awaited> + 'inbox:file': ( + ...args: [any] + ) => Awaited> + 'inbox:file-all-stale': ( + ...args: [] + ) => Awaited> + 'inbox:get': ( + ...args: [any] + ) => Awaited> + 'inbox:get-filing-history': ( + ...args: [any] + ) => Awaited< + Promise + > + 'inbox:get-patterns': ( + ...args: [] + ) => Awaited> + 'inbox:get-snoozed': (...args: []) => Awaited> + 'inbox:get-stale-threshold': (...args: []) => Awaited> + 'inbox:get-stats': ( + ...args: [] + ) => Awaited> + 'inbox:get-suggestions': ( + ...args: [any] + ) => Awaited< + Promise + > + 'inbox:get-tags': (...args: []) => Awaited> + 'inbox:link-to-note': ( + ...args: [any, any, any] + ) => Awaited> + 'inbox:list': ( + ...args: [any] + ) => Awaited> + 'inbox:list-archived': ( + ...args: [any] + ) => Awaited< + Promise + > + 'inbox:mark-viewed': ( + ...args: [any] + ) => Awaited> + 'inbox:remove-tag': ( + ...args: [any, any] + ) => Awaited> + 'inbox:retry-metadata': ( + ...args: [any] + ) => Awaited> + 'inbox:retry-transcription': ( + ...args: [any] + ) => Awaited> + 'inbox:set-stale-threshold': (...args: [any]) => Awaited> + 'inbox:snooze': ( + ...args: [any] + ) => Awaited> + 'inbox:track-suggestion': ( + ...args: [any, any, any, any, any, any, any] + ) => Awaited> + 'inbox:unarchive': ( + ...args: [any] + ) => Awaited> + 'inbox:undo-archive': ( + ...args: [any] + ) => Awaited> + 'inbox:undo-file': ( + ...args: [any] + ) => Awaited> + 'inbox:unsnooze': ( + ...args: [any] + ) => Awaited> + 'inbox:update': ( + ...args: [any] + ) => Awaited> + 'journal:createEntry': ( + ...args: [ + { + date: string + content?: string | undefined + tags?: string[] | undefined + properties?: Record | undefined + } + ] + ) => Awaited< + Promise<{ + id: string + date: string + content: string + wordCount: number + characterCount: number + tags: string[] + createdAt: string + modifiedAt: string + properties?: Record | undefined + }> + > + 'journal:deleteEntry': (...args: [{ date: string }]) => Awaited> + 'journal:getAllTags': (...args: []) => Awaited> + 'journal:getDayContext': ( + ...args: [{ date: string }] + ) => Awaited< + Promise<{ + date: string + tasks: { + id: string + title: string + completed: boolean + priority?: 'urgent' | 'high' | 'medium' | 'low' | undefined + isOverdue?: boolean | undefined + }[] + events: { + id: string + time: string + title: string + type: 'meeting' | 'focus' | 'event' + attendeeCount?: number | undefined + }[] + overdueCount: number + }> + > + 'journal:getEntry': ( + ...args: [{ date: string }] + ) => Awaited< + Promise<{ + id: string + date: string + content: string + wordCount: number + characterCount: number + tags: string[] + createdAt: string + modifiedAt: string + properties?: Record | undefined + } | null> + > + 'journal:getHeatmap': ( + ...args: [{ year: number }] + ) => Awaited> + 'journal:getMonthEntries': ( + ...args: [{ year: number; month: number }] + ) => Awaited< + Promise< + { + date: string + preview: string + wordCount: number + characterCount: number + activityLevel: 0 | 1 | 2 | 4 | 3 + tags: string[] + }[] + > + > + 'journal:getStreak': ( + ...args: [] + ) => Awaited< + Promise<{ currentStreak: number; longestStreak: number; lastEntryDate: string | null }> + > + 'journal:getYearStats': ( + ...args: [{ year: number }] + ) => Awaited< + Promise< + { + year: number + month: number + entryCount: number + totalWordCount: number + totalCharacterCount: number + averageLevel: number + }[] + > + > + 'journal:updateEntry': ( + ...args: [ + { + date: string + content?: string | undefined + tags?: string[] | undefined + properties?: Record | undefined + } + ] + ) => Awaited< + Promise<{ + id: string + date: string + content: string + wordCount: number + characterCount: number + tags: string[] + createdAt: string + modifiedAt: string + properties?: Record | undefined + }> + > + 'notes:create': ( + ...args: [ + { + title: string + content?: string | undefined + folder?: string | undefined + tags?: string[] | undefined + template?: string | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:create-folder': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:create-property-definition': ( + ...args: [ + { + name: string + type: 'number' | 'date' | 'text' | 'checkbox' | 'url' + options?: string[] | undefined + defaultValue?: unknown + color?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + definition: { + type: string + name: string + createdAt: string + options: string | null + defaultValue: string | null + color: string | null + } + error?: undefined + } + | { success: boolean; definition: null; error: string } + > + > + 'notes:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:delete-attachment': ( + ...args: [{ noteId: string; filename: string }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:delete-folder': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:delete-version': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:exists': (...args: [string]) => Awaited> + 'notes:export-html': ( + ...args: [ + { + noteId: string + includeMetadata?: boolean | undefined + pageSize?: 'A4' | 'Letter' | 'Legal' | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; error: string; path?: undefined } + | { success: boolean; path: string; error?: undefined } + > + > + 'notes:export-pdf': ( + ...args: [ + { + noteId: string + includeMetadata?: boolean | undefined + pageSize?: 'A4' | 'Letter' | 'Legal' | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; error: string; path?: undefined } + | { success: boolean; path: string; error?: undefined } + > + > + 'notes:get': (...args: [string]) => Awaited> + 'notes:get-all-positions': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; positions: Record; error?: undefined } + | { success: boolean; positions: {}; error: string } + > + > + 'notes:get-by-path': (...args: [string]) => Awaited> + 'notes:get-file': ( + ...args: [string] + ) => Awaited> + 'notes:get-folder-config': ( + ...args: [string] + ) => Awaited< + Promise + > + 'notes:get-folder-template': (...args: [string]) => Awaited> + 'notes:get-folders': (...args: []) => Awaited> + 'notes:get-links': ( + ...args: [string] + ) => Awaited> + 'notes:get-local-only-count': (...args: []) => Awaited> + 'notes:get-positions': ( + ...args: [{ folderPath: string }] + ) => Awaited< + Promise< + | { + success: boolean + positions: { path: string; position: number; folderPath: string }[] + error?: undefined + } + | { success: boolean; positions: never[]; error: string } + > + > + 'notes:get-property-definitions': ( + ...args: [] + ) => Awaited< + Promise< + { + type: string + name: string + createdAt: string + options: string | null + defaultValue: string | null + color: string | null + }[] + > + > + 'notes:get-tags': ( + ...args: [] + ) => Awaited> + 'notes:get-version': ( + ...args: [string] + ) => Awaited> + 'notes:get-versions': ( + ...args: [string] + ) => Awaited> + 'notes:import-files': ( + ...args: [{ sourcePaths: string[]; targetFolder?: string | undefined }] + ) => Awaited> + 'notes:list': ( + ...args: [ + { + folder?: string | undefined + tags?: string[] | undefined + sortBy?: 'title' | 'modified' | 'created' | 'position' | undefined + sortOrder?: 'asc' | 'desc' | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited> + 'notes:list-attachments': ( + ...args: [string] + ) => Awaited> + 'notes:move': ( + ...args: [{ id: string; newFolder: string }] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:open-external': (...args: [string]) => Awaited> + 'notes:rename': ( + ...args: [{ id: string; newTitle: string }] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:rename-folder': ( + ...args: [{ oldPath: string; newPath: string }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:reorder': ( + ...args: [{ folderPath: string; notePaths: string[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:resolve-by-title': ( + ...args: [string] + ) => Awaited< + Promise<{ + id: string + path: string + title: string + fileType: import('../../../../../packages/shared/src/file-types').FileType + } | null> + > + 'notes:restore-version': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:reveal-in-finder': (...args: [string]) => Awaited> + 'notes:set-folder-config': ( + ...args: [ + { + folderPath: string + config: { template?: string | undefined; inherit?: boolean | undefined } + } + ] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'notes:set-local-only': ( + ...args: [{ id: string; localOnly: boolean }] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:show-import-dialog': ( + ...args: [] + ) => Awaited> + 'notes:update': ( + ...args: [ + { + id: string + title?: string | undefined + content?: string | undefined + tags?: string[] | undefined + frontmatter?: Record | undefined + emoji?: string | null | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; note: import('../vault/notes').Note; error?: undefined } + | { success: boolean; note: null; error: string } + > + > + 'notes:update-property-definition': ( + ...args: [ + { + name: string + type?: 'number' | 'date' | 'text' | 'checkbox' | 'url' | undefined + options?: string[] | undefined + defaultValue?: unknown + color?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + definition: + | { + type: string + name: string + createdAt: string + options: string | null + defaultValue: string | null + color: string | null + } + | undefined + error?: undefined + } + | { success: boolean; definition: null; error: string } + > + > + 'notes:upload-attachment': ( + ...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer }] + ) => Awaited> + 'properties:get': ( + ...args: [{ entityId: string }] + ) => Awaited> + 'properties:rename': ( + ...args: [{ entityId: string; oldName: string; newName: string }] + ) => Awaited< + Promise + > + 'properties:set': ( + ...args: [{ entityId: string; properties: Record }] + ) => Awaited< + Promise + > + 'quick-capture:get-clipboard': (...args: []) => Awaited + 'reminder:bulk-dismiss': ( + ...args: [{ reminderIds: string[] }] + ) => Awaited< + Promise< + | { success: boolean; dismissedCount: number; error?: undefined } + | { success: boolean; dismissedCount: number; error: string } + > + > + 'reminder:count-pending': (...args: []) => Awaited> + 'reminder:create': ( + ...args: [ + | { + targetType: 'note' + targetId: string + remindAt: string + title?: string | undefined + note?: string | undefined + } + | { + targetType: 'journal' + targetId: string + remindAt: string + title?: string | undefined + note?: string | undefined + } + | { + targetType: 'highlight' + targetId: string + highlightText: string + highlightStart: number + highlightEnd: number + remindAt: string + title?: string | undefined + note?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + | { success: boolean; reminder: null; error: string } + > + > + 'reminder:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'reminder:dismiss': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; reminder: null; error: string } + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + > + > + 'reminder:get': ( + ...args: [string] + ) => Awaited< + Promise + > + 'reminder:get-due': ( + ...args: [] + ) => Awaited< + Promise + > + 'reminder:get-for-target': ( + ...args: [{ targetType: 'note' | 'journal' | 'highlight'; targetId: string }] + ) => Awaited> + 'reminder:get-upcoming': ( + ...args: [number | undefined] + ) => Awaited< + Promise<{ + reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] + total: number + hasMore: boolean + }> + > + 'reminder:list': ( + ...args: [ + { + targetType?: 'note' | 'journal' | 'highlight' | undefined + targetId?: string | undefined + status?: + | 'pending' + | 'triggered' + | 'dismissed' + | 'snoozed' + | ('pending' | 'triggered' | 'dismissed' | 'snoozed')[] + | undefined + fromDate?: string | undefined + toDate?: string | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise<{ + reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] + total: number + hasMore: boolean + }> + > + 'reminder:snooze': ( + ...args: [{ id: string; snoozeUntil: string }] + ) => Awaited< + Promise< + | { success: boolean; reminder: null; error: string } + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + > + > + 'reminder:update': ( + ...args: [ + { + id: string + remindAt?: string | undefined + title?: string | null | undefined + note?: string | null | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; reminder: null; error: string } + | { + success: boolean + reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder + error?: undefined + } + > + > + 'saved-filters:create': ( + ...args: [ + { + name: string + config: { + filters: { + search?: string | undefined + projectIds?: string[] | undefined + priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined + dueDate?: + | { + type: + | 'custom' + | 'any' + | 'none' + | 'overdue' + | 'today' + | 'tomorrow' + | 'this-week' + | 'next-week' + | 'this-month' + customStart?: string | null | undefined + customEnd?: string | null | undefined + } + | undefined + statusIds?: string[] | undefined + completion?: 'active' | 'completed' | 'all' | undefined + repeatType?: 'all' | 'repeating' | 'one-time' | undefined + hasTime?: 'all' | 'with-time' | 'without-time' | undefined + } + sort?: + | { + field: 'title' | 'createdAt' | 'priority' | 'dueDate' | 'completedAt' | 'project' + direction: 'asc' | 'desc' + } + | undefined + starred?: boolean | undefined + } + } + ] + ) => Awaited< + Promise<{ + success: boolean + savedFilter: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter + }> + > + 'saved-filters:delete': ( + ...args: [{ id: string }] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'saved-filters:list': ( + ...args: [] + ) => Awaited< + Promise<{ + savedFilters: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter[] + }> + > + 'saved-filters:reorder': ( + ...args: [{ ids: string[]; positions: number[] }] + ) => Awaited> + 'saved-filters:update': ( + ...args: [ + { + id: string + name?: string | undefined + config?: + | { + filters: { + search?: string | undefined + projectIds?: string[] | undefined + priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined + dueDate?: + | { + type: + | 'custom' + | 'any' + | 'none' + | 'overdue' + | 'today' + | 'tomorrow' + | 'this-week' + | 'next-week' + | 'this-month' + customStart?: string | null | undefined + customEnd?: string | null | undefined + } + | undefined + statusIds?: string[] | undefined + completion?: 'active' | 'completed' | 'all' | undefined + repeatType?: 'all' | 'repeating' | 'one-time' | undefined + hasTime?: 'all' | 'with-time' | 'without-time' | undefined + } + sort?: + | { + field: + | 'title' + | 'createdAt' + | 'priority' + | 'dueDate' + | 'completedAt' + | 'project' + direction: 'asc' | 'desc' + } + | undefined + starred?: boolean | undefined + } + | undefined + position?: number | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; savedFilter: null; error: string } + | { + success: boolean + savedFilter: + | import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter + | null + error?: undefined + } + > + > + 'search:add-reason': ( + ...args: [ + { + itemId: string + itemType: 'note' | 'task' | 'journal' | 'inbox' + itemTitle: string + searchQuery: string + itemIcon?: string | null | undefined + } + ] + ) => Awaited> + 'search:clear-reasons': (...args: []) => Awaited> + 'search:get-all-tags': (...args: []) => Awaited> + 'search:get-reasons': ( + ...args: [] + ) => Awaited> + 'search:get-stats': ( + ...args: [] + ) => Awaited> + 'search:query': ( + ...args: [ + { + text: string + types?: ('note' | 'task' | 'journal' | 'inbox')[] | undefined + tags?: string[] | undefined + dateRange?: { from: string; to: string } | null | undefined + projectId?: string | null | undefined + folderPath?: string | null | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited> + 'search:quick': ( + ...args: [string] + ) => Awaited< + Promise + > + 'search:rebuild-index': ( + ...args: [] + ) => Awaited< + Promise< + | { + notes: number + tasks: number + inbox: number + durationMs: number + started: true + error?: undefined + } + | { started: false; error: string } + > + > + 'settings:get': (...args: [string]) => Awaited + 'settings:getAIModelStatus': ( + ...args: [] + ) => Awaited> + 'settings:getAISettings': (...args: []) => Awaited + 'settings:getBackupSettings': ( + ...args: [] + ) => Awaited<{ + autoBackup: boolean + frequencyHours: 1 | 6 | 12 | 24 + maxBackups: number + lastBackupAt: string | null + }> + 'settings:getEditorSettings': ( + ...args: [] + ) => Awaited<{ + width: 'medium' | 'narrow' | 'wide' + spellCheck: boolean + autoSaveDelay: number + showWordCount: boolean + toolbarMode: 'floating' | 'sticky' + }> + 'settings:getGeneralSettings': ( + ...args: [] + ) => Awaited<{ + theme: 'light' | 'dark' | 'white' | 'system' + fontSize: 'small' | 'medium' | 'large' + fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' + accentColor: string + startOnBoot: boolean + language: string + onboardingCompleted: boolean + }> + 'settings:getGraphSettings': ( + ...args: [] + ) => Awaited<{ + layout: 'forceatlas2' | 'circular' | 'random' + showLabels: boolean + showEdgeLabels: boolean + animateLayout: boolean + showTagEdges: boolean + }> + 'settings:getJournalSettings': ( + ...args: [] + ) => Awaited<{ + defaultTemplate: string | null + showSchedule: boolean + showTasks: boolean + showAIConnections: boolean + showStatsFooter: boolean + }> + 'settings:getKeyboardSettings': ( + ...args: [] + ) => Awaited<{ + overrides: Record< + string, + { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } + > + globalCapture: { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } | null + }> + 'settings:getNoteEditorSettings': ( + ...args: [] + ) => Awaited + 'settings:getSyncSettings': (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean }> + 'settings:getTabSettings': (...args: []) => Awaited + 'settings:getTaskSettings': ( + ...args: [] + ) => Awaited<{ + defaultProjectId: string | null + defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' + weekStartDay: 'sunday' | 'monday' + staleInboxDays: number + }> + 'settings:loadAIModel': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; message: string; error?: undefined } + | { success: boolean; error: string; message?: undefined } + | { success: boolean; message?: undefined; error?: undefined } + > + > + 'settings:registerGlobalCapture': ( + ...args: [] + ) => Awaited> + 'settings:reindexEmbeddings': ( + ...args: [] + ) => Awaited< + Promise<{ success: boolean; computed: number; skipped: number; error?: string | undefined }> + > + 'settings:resetKeyboardSettings': ( + ...args: [] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:set': ( + ...args: [{ key: string; value: string }] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setAISettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setBackupSettings': ( + ...args: [ + Partial<{ + autoBackup: boolean + frequencyHours: 1 | 6 | 12 | 24 + maxBackups: number + lastBackupAt: string | null + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setEditorSettings': ( + ...args: [ + Partial<{ + width: 'medium' | 'narrow' | 'wide' + spellCheck: boolean + autoSaveDelay: number + showWordCount: boolean + toolbarMode: 'floating' | 'sticky' + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setGeneralSettings': ( + ...args: [ + Partial<{ + theme: 'light' | 'dark' | 'white' | 'system' + fontSize: 'small' | 'medium' | 'large' + fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' + accentColor: string + startOnBoot: boolean + language: string + onboardingCompleted: boolean + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setGraphSettings': ( + ...args: [ + Partial<{ + layout: 'forceatlas2' | 'circular' | 'random' + showLabels: boolean + showEdgeLabels: boolean + animateLayout: boolean + showTagEdges: boolean + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setJournalSettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setKeyboardSettings': ( + ...args: [ + Partial<{ + overrides: Record< + string, + { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } + > + globalCapture: { + key: string + modifiers: { + meta?: boolean | undefined + ctrl?: boolean | undefined + shift?: boolean | undefined + alt?: boolean | undefined + } + } | null + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setNoteEditorSettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setSyncSettings': ( + ...args: [Partial<{ enabled: boolean; autoSync: boolean }>] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'settings:setTabSettings': ( + ...args: [Partial] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'settings:setTaskSettings': ( + ...args: [ + Partial<{ + defaultProjectId: string | null + defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' + weekStartDay: 'sunday' | 'monday' + staleInboxDays: number + }> + ] + ) => Awaited<{ success: boolean; error?: string | undefined }> + 'sync:approve-linking': ( + ...args: [{ sessionId: string }] + ) => Awaited< + Promise + > + 'sync:check-device-status': (...args: []) => Awaited> + 'sync:complete-linking-qr': ( + ...args: [{ sessionId: string }] + ) => Awaited< + Promise + > + 'sync:confirm-recovery-phrase': ( + ...args: [{ confirmed: boolean }] + ) => Awaited> + 'sync:download-attachment': ( + ...args: [{ attachmentId: string; targetPath?: string | undefined }] + ) => Awaited< + Promise< + | { success: boolean; error: string; filePath?: undefined } + | { success: boolean; filePath: string; error?: undefined } + > + > + 'sync:emergency-wipe': (...args: []) => Awaited> + 'sync:generate-linking-qr': ( + ...args: [] + ) => Awaited< + Promise + > + 'sync:get-devices': ( + ...args: [] + ) => Awaited< + Promise<{ + devices: { + id: string + name: string + platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android' + linkedAt: number + lastSyncAt: number | undefined + isCurrentDevice: boolean + }[] + email: string | undefined + }> + > + 'sync:get-download-progress': ( + ...args: [{ attachmentId: string }] + ) => Awaited< + Promise<{ + progress: number + downloadedChunks: number + totalChunks: number + status: 'downloading' + } | null> + > + 'sync:get-history': ( + ...args: [{ limit?: number | undefined; offset?: number | undefined }] + ) => Awaited< + Promise<{ + entries: { + id: string + type: 'error' | 'push' | 'pull' + itemCount: number + direction: string | undefined + details: unknown + durationMs: number | undefined + createdAt: number + }[] + total: number + }> + > + 'sync:get-linking-sas': ( + ...args: [{ sessionId: string }] + ) => Awaited> + 'sync:get-quarantined-items': ( + ...args: [] + ) => Awaited + 'sync:get-queue-size': (...args: []) => Awaited<{ pending: number; failed: number }> + 'sync:get-recovery-phrase': (...args: []) => Awaited + 'sync:get-status': ( + ...args: [] + ) => Awaited< + | import('../../../../../packages/contracts/src/ipc-sync-ops').GetSyncStatusResult + | { status: string; pendingCount: number } + > + 'sync:get-storage-breakdown': ( + ...args: [] + ) => Awaited< + Promise< + import('../../../../../packages/contracts/src/ipc-sync-ops').StorageBreakdownResult | null + > + > + 'sync:get-synced-settings': ( + ...args: [] + ) => Awaited<{ + general?: + | { + theme?: 'light' | 'dark' | 'white' | 'system' | undefined + fontSize?: 'small' | 'medium' | 'large' | undefined + fontFamily?: 'system' | 'serif' | 'sans-serif' | 'monospace' | undefined + accentColor?: string | undefined + startOnBoot?: boolean | undefined + language?: string | undefined + } + | undefined + editor?: + | { + width?: 'medium' | 'narrow' | 'wide' | undefined + spellCheck?: boolean | undefined + autoSaveDelay?: number | undefined + showWordCount?: boolean | undefined + toolbarMode?: 'floating' | 'sticky' | undefined + } + | undefined + tasks?: + | { + defaultProjectId?: string | null | undefined + defaultSortOrder?: 'createdAt' | 'priority' | 'dueDate' | 'manual' | undefined + weekStartDay?: 'sunday' | 'monday' | undefined + staleInboxDays?: number | undefined + showCompleted?: boolean | undefined + sortBy?: string | undefined + } + | undefined + keyboard?: { overrides?: Record | undefined } | undefined + notes?: + | { + defaultFolder?: string | undefined + editorFontSize?: number | undefined + spellCheck?: boolean | undefined + } + | undefined + sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined } | undefined + } | null> + 'sync:get-upload-progress': ( + ...args: [{ sessionId: string }] + ) => Awaited< + Promise<{ + progress: number + uploadedChunks: number + totalChunks: number + status: 'uploading' + } | null> + > + 'sync:link-via-qr': ( + ...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined }] + ) => Awaited> + 'sync:link-via-recovery': ( + ...args: [{ recoveryPhrase: string }] + ) => Awaited< + Promise< + | { success: boolean; error: string; deviceId?: undefined } + | { success: boolean; deviceId: string; error?: undefined } + > + > + 'sync:logout': ( + ...args: [] + ) => Awaited> + 'sync:pause': (...args: []) => Awaited<{ success: boolean; wasPaused: boolean }> + 'sync:remove-device': ( + ...args: [{ deviceId: string }] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'sync:rename-device': ( + ...args: [{ deviceId: string; newName: string }] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'sync:resume': (...args: []) => Awaited<{ success: boolean; pendingCount: number }> + 'sync:setup-first-device': ( + ...args: [{ oauthToken: string; provider: 'google'; state: string }] + ) => Awaited< + Promise< + | { + success: boolean + needsRecoverySetup: boolean + deviceId: string + needsRecoveryInput?: undefined + } + | { + success: boolean + needsRecoverySetup: boolean + needsRecoveryInput: boolean + deviceId?: undefined + } + > + > + 'sync:setup-new-account': ( + ...args: [] + ) => Awaited< + Promise< + | { success: boolean; error: string; deviceId?: undefined } + | { success: boolean; deviceId: string; error?: undefined } + > + > + 'sync:trigger-sync': ( + ...args: [] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'sync:update-synced-setting': ( + ...args: [unknown] + ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + 'sync:upload-attachment': ( + ...args: [{ noteId: string; filePath: string }] + ) => Awaited< + Promise< + | { success: boolean; error: string; attachmentId?: undefined; sessionId?: undefined } + | { success: boolean; attachmentId: string; sessionId: string; error?: undefined } + > + > + 'tags:delete': ( + ...args: [string] + ) => Awaited> + 'tags:get-all-with-counts': ( + ...args: [] + ) => Awaited< + Promise + > + 'tags:get-notes-by-tag': ( + ...args: [ + { + tag: string + sortBy?: 'title' | 'modified' | 'created' | undefined + sortOrder?: 'asc' | 'desc' | undefined + } + ] + ) => Awaited< + Promise + > + 'tags:merge': ( + ...args: [{ source: string; target: string }] + ) => Awaited> + 'tags:pin-note-to-tag': ( + ...args: [{ noteId: string; tag: string }] + ) => Awaited< + Promise + > + 'tags:remove-from-note': ( + ...args: [{ noteId: string; tag: string }] + ) => Awaited< + Promise + > + 'tags:rename': ( + ...args: [{ oldName: string; newName: string }] + ) => Awaited> + 'tags:unpin-note-from-tag': ( + ...args: [{ noteId: string; tag: string }] + ) => Awaited< + Promise + > + 'tags:update-color': ( + ...args: [{ tag: string; color: string }] + ) => Awaited< + Promise + > + 'tasks:archive': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'tasks:bulk-archive': ( + ...args: [{ ids: string[] }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:bulk-complete': ( + ...args: [{ ids: string[] }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:bulk-delete': ( + ...args: [{ ids: string[] }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:bulk-move': ( + ...args: [{ ids: string[]; projectId: string }] + ) => Awaited< + Promise< + | { success: boolean; count: number; error?: undefined } + | { success: boolean; count: number; error: string } + > + > + 'tasks:complete': ( + ...args: [{ id: string; completedAt?: string | undefined }] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:convert-to-subtask': ( + ...args: [{ taskId: string; parentId: string }] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:convert-to-task': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:create': ( + ...args: [ + { + projectId: string + title: string + description?: string | null | undefined + priority?: number | undefined + statusId?: string | null | undefined + parentId?: string | null | undefined + dueDate?: string | null | undefined + dueTime?: string | null | undefined + startDate?: string | null | undefined + isRepeating?: boolean | undefined + repeatConfig?: + | { + frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' + endType: 'date' | 'never' | 'count' + createdAt: string + interval?: number | undefined + daysOfWeek?: number[] | undefined + monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined + dayOfMonth?: number | undefined + weekOfMonth?: number | undefined + dayOfWeekForMonth?: number | undefined + endDate?: string | null | undefined + endCount?: number | undefined + completedCount?: number | undefined + } + | null + | undefined + repeatFrom?: 'due' | 'completion' | null | undefined + tags?: string[] | undefined + linkedNoteIds?: string[] | undefined + sourceNoteId?: string | null | undefined + position?: number | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + | { success: boolean; task: null; error: string } + > + > + 'tasks:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:duplicate': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:get': ( + ...args: [string] + ) => Awaited< + Promise<{ + tags: string[] + linkedNoteIds: string[] + hasSubtasks: boolean + subtaskCount: number + completedSubtaskCount: number + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } | null> + > + 'tasks:get-linked-tasks': ( + ...args: [string] + ) => Awaited< + Promise< + { + tags: string[] + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + > + > + 'tasks:get-overdue': ( + ...args: [] + ) => Awaited< + Promise<{ + tasks: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:get-stats': ( + ...args: [] + ) => Awaited< + Promise<{ + total: number + completed: number + overdue: number + dueToday: number + dueThisWeek: number + }> + > + 'tasks:get-subtasks': ( + ...args: [string] + ) => Awaited< + Promise< + { + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + > + > + 'tasks:get-tags': (...args: []) => Awaited> + 'tasks:get-today': ( + ...args: [] + ) => Awaited< + Promise<{ + tasks: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:get-upcoming': ( + ...args: [{ days?: number | undefined }] + ) => Awaited< + Promise<{ + tasks: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:list': ( + ...args: [ + { + projectId?: string | undefined + statusId?: string | null | undefined + parentId?: string | null | undefined + includeCompleted?: boolean | undefined + includeArchived?: boolean | undefined + dueBefore?: string | undefined + dueAfter?: string | undefined + tags?: string[] | undefined + search?: string | undefined + sortBy?: 'modified' | 'created' | 'position' | 'priority' | 'dueDate' | undefined + sortOrder?: 'asc' | 'desc' | undefined + limit?: number | undefined + offset?: number | undefined + } + ] + ) => Awaited< + Promise<{ + tasks: { + tags: string[] + linkedNoteIds: string[] + hasSubtasks: boolean + subtaskCount: number + completedSubtaskCount: number + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + }[] + total: number + hasMore: boolean + }> + > + 'tasks:move': ( + ...args: [ + { + taskId: string + position: number + targetProjectId?: string | undefined + targetStatusId?: string | null | undefined + targetParentId?: string | null | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:project-archive': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:project-create': ( + ...args: [ + { + name: string + description?: string | null | undefined + color?: string | undefined + icon?: string | null | undefined + statuses?: + | { + name: string + type: 'todo' | 'in_progress' | 'done' + order: number + color?: string | undefined + }[] + | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + project: { + id: string + name: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + color: string + description: string | null + icon: string | null + isInbox: boolean + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + } + error?: undefined + } + | { success: boolean; project: null; error: string } + > + > + 'tasks:project-delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:project-get': ( + ...args: [string] + ) => Awaited> + 'tasks:project-list': ( + ...args: [] + ) => Awaited> + 'tasks:project-reorder': ( + ...args: [{ projectIds: string[]; positions: number[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:project-update': ( + ...args: [ + { + id: string + name?: string | undefined + description?: string | null | undefined + color?: string | undefined + icon?: string | null | undefined + statuses?: + | { + name: string + type: 'todo' | 'in_progress' | 'done' + order: number + id?: string | undefined + color?: string | undefined + }[] + | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; project: null; error: string } + | { + success: boolean + project: { + id: string + name: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + color: string + description: string | null + icon: string | null + isInbox: boolean + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + } + error?: undefined + } + > + > + 'tasks:reorder': ( + ...args: [{ taskIds: string[]; positions: number[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:seed-demo': (...args: []) => Awaited> + 'tasks:seed-performance-test': ( + ...args: [] + ) => Awaited> + 'tasks:status-create': ( + ...args: [ + { projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined } + ] + ) => Awaited< + Promise< + | { + success: boolean + status: { + id: string + name: string + createdAt: string + position: number + color: string + projectId: string + isDefault: boolean + isDone: boolean + } + error?: undefined + } + | { success: boolean; status: null; error: string } + > + > + 'tasks:status-delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:status-list': ( + ...args: [string] + ) => Awaited< + Promise< + { + id: string + name: string + createdAt: string + position: number + color: string + projectId: string + isDefault: boolean + isDone: boolean + }[] + > + > + 'tasks:status-reorder': ( + ...args: [{ statusIds: string[]; positions: number[] }] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'tasks:status-update': ( + ...args: [ + { + id: string + name?: string | undefined + color?: string | undefined + position?: number | undefined + isDefault?: boolean | undefined + isDone?: boolean | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; error: string; status?: undefined } + | { + success: boolean + status: { + id: string + name: string + createdAt: string + position: number + color: string + projectId: string + isDefault: boolean + isDone: boolean + } + error?: undefined + } + > + > + 'tasks:unarchive': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> + > + 'tasks:uncomplete': ( + ...args: [string] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'tasks:update': ( + ...args: [ + { + id: string + title?: string | undefined + description?: string | null | undefined + priority?: number | undefined + projectId?: string | undefined + statusId?: string | null | undefined + parentId?: string | null | undefined + dueDate?: string | null | undefined + dueTime?: string | null | undefined + startDate?: string | null | undefined + isRepeating?: boolean | undefined + repeatConfig?: + | { + frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' + endType: 'date' | 'never' | 'count' + createdAt: string + interval?: number | undefined + daysOfWeek?: number[] | undefined + monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined + dayOfMonth?: number | undefined + weekOfMonth?: number | undefined + dayOfWeekForMonth?: number | undefined + endDate?: string | null | undefined + endCount?: number | undefined + completedCount?: number | undefined + } + | null + | undefined + repeatFrom?: 'due' | 'completion' | null | undefined + tags?: string[] | undefined + linkedNoteIds?: string[] | undefined + } + ] + ) => Awaited< + Promise< + | { success: boolean; task: null; error: string } + | { + success: boolean + task: { + linkedNoteIds: string[] + id: string + title: string + clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null + syncedAt: string | null + createdAt: string + modifiedAt: string + position: number + description: string | null + projectId: string + priority: number + statusId: string | null + parentId: string | null + dueDate: string | null + dueTime: string | null + startDate: string | null + repeatConfig: unknown + repeatFrom: string | null + sourceNoteId: string | null + archivedAt: string | null + fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null + completedAt: string | null + } + error?: undefined + } + > + > + 'templates:create': ( + ...args: [ + { + name: string + description?: string | undefined + icon?: string | null | undefined + tags?: string[] | undefined + properties?: + | { + name: string + type: + | 'number' + | 'date' + | 'text' + | 'select' + | 'checkbox' + | 'url' + | 'multiselect' + | 'rating' + value: unknown + options?: string[] | undefined + }[] + | undefined + content?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + template: import('../../../../../packages/contracts/src/templates-api').Template + error?: undefined + } + | { success: boolean; template: null; error: string } + > + > + 'templates:delete': ( + ...args: [string] + ) => Awaited< + Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> + > + 'templates:duplicate': ( + ...args: [{ id: string; newName: string }] + ) => Awaited< + Promise< + | { + success: boolean + template: import('../../../../../packages/contracts/src/templates-api').Template + error?: undefined + } + | { success: boolean; template: null; error: string } + > + > + 'templates:get': ( + ...args: [string] + ) => Awaited< + Promise + > + 'templates:list': ( + ...args: [] + ) => Awaited< + Promise<{ + templates: import('../../../../../packages/contracts/src/templates-api').TemplateListItem[] + }> + > + 'templates:update': ( + ...args: [ + { + id: string + name?: string | undefined + description?: string | undefined + icon?: string | null | undefined + tags?: string[] | undefined + properties?: + | { + name: string + type: + | 'number' + | 'date' + | 'text' + | 'select' + | 'checkbox' + | 'url' + | 'multiselect' + | 'rating' + value: unknown + options?: string[] | undefined + }[] + | undefined + content?: string | undefined + } + ] + ) => Awaited< + Promise< + | { + success: boolean + template: import('../../../../../packages/contracts/src/templates-api').Template + error?: undefined + } + | { success: boolean; template: null; error: string } + > + > + 'vault:close': (...args: []) => Awaited> + 'vault:get-all': ( + ...args: [] + ) => Awaited> + 'vault:get-config': ( + ...args: [] + ) => Awaited> + 'vault:get-status': ( + ...args: [] + ) => Awaited> + 'vault:reindex': (...args: []) => Awaited> + 'vault:remove': (...args: [string]) => Awaited> + 'vault:select': ( + ...args: [{ path?: string | undefined }] + ) => Awaited< + Promise + > + 'vault:switch': ( + ...args: [string] + ) => Awaited< + Promise + > + 'vault:update-config': ( + ...args: [ + { + excludePatterns?: string[] | undefined + defaultNoteFolder?: string | undefined + journalFolder?: string | undefined + attachmentsFolder?: string | undefined + } + ] + ) => Awaited> } export type MainIpcInvokeChannel = keyof MainIpcInvokeHandlers -export type MainIpcInvokeArgs = - Parameters -export type MainIpcInvokeResult = - ReturnType +export type MainIpcInvokeArgs = Parameters +export type MainIpcInvokeResult = ReturnType< + MainIpcInvokeHandlers[C] +> diff --git a/apps/desktop/src/main/store.ts b/apps/desktop/src/main/store.ts index aedc1ddd5..345332a2e 100644 --- a/apps/desktop/src/main/store.ts +++ b/apps/desktop/src/main/store.ts @@ -45,6 +45,9 @@ const defaultData: StoreSchema = { sync: {} } +/** In-memory cache — populated on first read, updated on every write. */ +let cache: StoreSchema | null = null + /** * Get the config file path in the app's userData directory */ @@ -54,7 +57,7 @@ function getConfigPath(): string { } /** - * Read the config file + * Read the config file (only called when cache is cold). */ function readConfig(): StoreSchema { try { @@ -67,33 +70,39 @@ function readConfig(): StoreSchema { } catch (error) { logger.error('Error reading config:', error) } - return defaultData + return { ...defaultData } } /** - * Write the config file + * Write the config file and keep the cache in sync. */ function writeConfig(data: StoreSchema): void { try { const configPath = getConfigPath() fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8') + cache = data } catch (error) { logger.error('Error writing config:', error) } } +function getCache(): StoreSchema { + if (!cache) { + cache = readConfig() + } + return cache +} + /** * Simple store object that mimics electron-store API */ export const store = { get(key: K): StoreSchema[K] { - const data = readConfig() - return data[key] + return getCache()[key] }, set(key: K, value: StoreSchema[K]): void { - const data = readConfig() - data[key] = value + const data = { ...getCache(), [key]: value } writeConfig(data) } } diff --git a/apps/desktop/src/main/vault/indexer.ts b/apps/desktop/src/main/vault/indexer.ts index 7437d52ae..75bae9ff9 100644 --- a/apps/desktop/src/main/vault/indexer.ts +++ b/apps/desktop/src/main/vault/indexer.ts @@ -258,10 +258,36 @@ async function indexNonMarkdownFile( } } +// ============================================================================ +// Concurrency Limiter +// ============================================================================ + +/** + * Run tasks with a bounded concurrency limit. + * Avoids exhausting file-descriptors or SQLite write slots on large vaults. + */ +async function withConcurrency(tasks: (() => Promise)[], limit: number): Promise { + const results: T[] = new Array(tasks.length) + let next = 0 + + async function worker(): Promise { + while (next < tasks.length) { + const i = next++ + results[i] = await tasks[i]() + } + } + + const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker()) + await Promise.all(workers) + return results +} + // ============================================================================ // Main Indexer // ============================================================================ +const INDEX_CONCURRENCY = 8 + /** * Index all files in the vault. * Scans notes and journal folders, populates cache. @@ -309,11 +335,25 @@ export async function indexVault(vaultPath: string): Promise { return result } - // Index each file - for (let i = 0; i < allFiles.length; i++) { - const file = allFiles[i] + // Track completed count for progress reporting (thread-safe increment via closure) + let completed = 0 + + const tasks = allFiles.map((file, i) => async () => { const status = await indexFile(vaultPath, file) + completed++ + + // Emit progress every 10 completions to reduce IPC overhead + if (completed % 10 === 0 || completed === allFiles.length) { + const progress = Math.round((completed / allFiles.length) * 100) + emitIndexProgress(progress) + } + return { i, status } + }) + + const statuses = await withConcurrency(tasks, INDEX_CONCURRENCY) + + for (const { status } of statuses) { switch (status) { case 'indexed': result.indexed++ @@ -325,12 +365,6 @@ export async function indexVault(vaultPath: string): Promise { result.errors++ break } - - // Emit progress (batch every 10 files to reduce IPC overhead) - if (i % 10 === 0 || i === allFiles.length - 1) { - const progress = Math.round(((i + 1) / allFiles.length) * 100) - emitIndexProgress(progress) - } } logger.info( From 6024ec0437e8c715cd6193a49157cc6d59ba7b52 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Thu, 19 Mar 2026 23:54:59 +0300 Subject: [PATCH 10/80] =?UTF-8?q?feat(a11y):=20WCAG=202.1=20AA=20accessibi?= =?UTF-8?q?lity=20pass=20=E2=80=94=20keyboard=20nav,=20ARIA,=20focus=20ind?= =?UTF-8?q?icators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add global :focus-visible safety net in main.css so any focusable element without an explicit ring still gets a visible outline - Fix KanbanCard: add onKeyDown handler (Enter=open, Space=toggle/complete) and focus-visible:ring-2 so Tab users see focus - Destroy canvas blindspot: wrap GraphCanvas in role="img" with aria-label summarising node/edge counts; add sr-only
    listing all nodes by label+type for screen readers - Fix TagAutocomplete combobox ARIA: add role="combobox" + aria-controls + aria-activedescendant on input; give each option a unique id; SuggestionItem changed from button to div[role=option] inside role=listbox; TagPill span gets role=listitem - Add ArrowUp/Down keyboard navigation to TagInputPopup; wire focusedIndex to TagOption isFocused prop for visual + ARIA feedback; input gets role=combobox + aria-controls + aria-activedescendant - Fix pre-existing TS2352 type error in use-undoable-task-actions.ts (Task→Record cast) - Regenerate IPC invoke map Co-Authored-By: Paperclip --- .../src/main/ipc/generated-ipc-invoke-map.ts | 3047 ++--------------- apps/desktop/src/renderer/src/assets/main.css | 17 + .../components/filing/tag-autocomplete.tsx | 25 +- .../src/components/graph/graph-page.tsx | 37 +- .../note/tags-row/TagInputPopup.tsx | 61 +- .../components/tasks/kanban/kanban-card.tsx | 17 + .../src/hooks/use-undoable-task-actions.ts | 320 ++ 7 files changed, 751 insertions(+), 2773 deletions(-) create mode 100644 apps/desktop/src/renderer/src/hooks/use-undoable-task-actions.ts diff --git a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts index b02872c30..8c8150b9b 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -2,2760 +2,299 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export interface MainIpcInvokeHandlers { - 'account:getInfo': (...args: []) => Awaited - 'account:getRecoveryKey': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; error: string; key?: undefined } - | { success: boolean; key: string; error?: undefined } - > - > - 'account:signOut': ( - ...args: [] - ) => Awaited> - 'ai-inline:get-server-port': (...args: []) => Awaited - 'ai-inline:get-settings': ( - ...args: [] - ) => Awaited - 'ai-inline:set-settings': ( - ...args: [ - Partial - ] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'ai-inline:start-server': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; error: string; port?: undefined } - | { success: boolean; port: number; error?: undefined } - > - > - 'ai-inline:stop-server': (...args: []) => Awaited> - 'auth:init-oauth': (...args: [{ provider: 'google' }]) => Awaited> - 'auth:refresh-token': ( - ...args: [] - ) => Awaited> - 'auth:request-otp': (...args: [{ email: string }]) => Awaited> - 'auth:resend-otp': (...args: [{ email: string }]) => Awaited> - 'auth:verify-otp': ( - ...args: [{ email: string; code: string }] - ) => Awaited< - Promise<{ - success: boolean - isNewUser: boolean - needsSetup: boolean - needsRecoveryInput: boolean - }> - > - 'bookmarks:bulk-create': ( - ...args: [{ items: { itemType: string; itemId: string }[] }] - ) => Awaited> - 'bookmarks:bulk-delete': ( - ...args: [{ bookmarkIds: string[] }] - ) => Awaited> - 'bookmarks:create': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited< - Promise< - | { success: boolean; bookmark: null; error: string } - | { - success: boolean - bookmark: { - id: string - createdAt: string - position: number - itemType: string - itemId: string - } - error?: undefined - } - > - > - 'bookmarks:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'bookmarks:get': ( - ...args: [string] - ) => Awaited< - Promise<{ - id: string - createdAt: string - position: number - itemType: string - itemId: string - } | null> - > - 'bookmarks:get-by-item': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited< - Promise<{ - id: string - createdAt: string - position: number - itemType: string - itemId: string - } | null> - > - 'bookmarks:is-bookmarked': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited> - 'bookmarks:list': ( - ...args: [ - { - itemType?: string | undefined - sortBy?: 'createdAt' | 'position' | undefined - sortOrder?: 'asc' | 'desc' | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise - > - 'bookmarks:list-by-type': ( - ...args: [string] - ) => Awaited< - Promise - > - 'bookmarks:reorder': ( - ...args: [{ bookmarkIds: string[] }] - ) => Awaited> - 'bookmarks:toggle': ( - ...args: [{ itemType: string; itemId: string }] - ) => Awaited< - Promise<{ - success: boolean - isBookmarked: boolean - bookmark: { - id: string - createdAt: string - position: number - itemType: string - itemId: string - } | null - }> - > - 'context-menu:show': ( - ...args: [ - { - id: string - label: string - accelerator?: string | undefined - disabled?: boolean | undefined - type?: 'normal' | 'separator' | undefined - }[] - ] - ) => Awaited> - 'crdt:apply-update': (...args: [unknown]) => Awaited> - 'crdt:close-doc': (...args: [unknown]) => Awaited> - 'crdt:open-doc': ( - ...args: [unknown] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'crdt:sync-step-1': ( - ...args: [{ noteId: string; stateVector: number[] }] - ) => Awaited< - Promise - > - 'crdt:sync-step-2': (...args: [{ noteId: string; diff: number[] }]) => Awaited> - 'crypto:decrypt-item': ( - ...args: [ - { - itemId: string - type: - | 'note' - | 'filter' - | 'task' - | 'project' - | 'journal' - | 'settings' - | 'inbox' - | 'tag_definition' - encryptedKey: string - keyNonce: string - encryptedData: string - dataNonce: string - signature: string - operation?: 'create' | 'update' | 'delete' | undefined - deletedAt?: number | undefined - metadata?: Record | undefined - } - ] - ) => Awaited< - Promise - > - 'crypto:encrypt-item': ( - ...args: [ - { - itemId: string - type: - | 'note' - | 'filter' - | 'task' - | 'project' - | 'journal' - | 'settings' - | 'inbox' - | 'tag_definition' - content: Record - operation?: 'create' | 'update' | 'delete' | undefined - deletedAt?: number | undefined - metadata?: Record | undefined - } - ] - ) => Awaited< - Promise - > - 'crypto:get-rotation-progress': ( - ...args: [] - ) => Awaited - 'crypto:rotate-keys': ( - ...args: [{ confirm: boolean }] - ) => Awaited> - 'crypto:verify-signature': ( - ...args: [ - { - itemId: string - type: - | 'note' - | 'filter' - | 'task' - | 'project' - | 'journal' - | 'settings' - | 'inbox' - | 'tag_definition' - encryptedKey: string - keyNonce: string - encryptedData: string - dataNonce: string - signature: string - operation?: 'create' | 'update' | 'delete' | undefined - deletedAt?: number | undefined - metadata?: Record | undefined - } - ] - ) => Awaited< - Promise - > - 'folder-view:delete-view': ( - ...args: [{ folderPath: string; viewName: string }] - ) => Awaited< - Promise - > - 'folder-view:folder-exists': (...args: [string]) => Awaited - 'folder-view:get-available-properties': ( - ...args: [{ folderPath: string }] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/folder-view-api').GetAvailablePropertiesResponse - > - > - 'folder-view:get-config': ( - ...args: [{ folderPath: string }] - ) => Awaited< - Promise - > - 'folder-view:get-folder-suggestions': ( - ...args: [{ noteId: string }] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/folder-view-api').GetFolderSuggestionsResponse - > - > - 'folder-view:get-views': ( - ...args: [{ folderPath: string }] - ) => Awaited< - Promise - > - 'folder-view:list-with-properties': ( - ...args: [ - { - folderPath: string - properties?: string[] | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/folder-view-api').ListWithPropertiesResponse - > - > - 'folder-view:set-config': ( - ...args: [ - { - folderPath: string - config: { - path?: string | undefined - template?: string | undefined - inherit?: boolean | undefined - formulas?: Record | undefined - properties?: - | Record< - string, - { - displayName?: string | undefined - color?: boolean | undefined - dateFormat?: string | undefined - numberFormat?: string | undefined - hidden?: boolean | undefined - } - > - | undefined - summaries?: - | Record< - string, - { - type: - | 'custom' - | 'count' - | 'sum' - | 'average' - | 'min' - | 'max' - | 'countBy' - | 'countUnique' - label?: string | undefined - expression?: string | undefined - } - > - | undefined - views?: - | { - name: string - type?: 'table' | 'grid' | 'list' | 'kanban' | undefined - default?: boolean | undefined - columns?: - | { - id: string - width?: number | undefined - displayName?: string | undefined - showSummary?: boolean | undefined - }[] - | undefined - filters?: unknown - order?: { property: string; direction: 'asc' | 'desc' }[] | undefined - groupBy?: - | { - property: string - direction?: 'asc' | 'desc' | undefined - collapsed?: boolean | undefined - showSummary?: boolean | undefined - } - | undefined - limit?: number | undefined - showSummaries?: boolean | undefined - }[] - | undefined - } - } - ] - ) => Awaited< - Promise - > - 'folder-view:set-view': ( - ...args: [ - { - folderPath: string - view: { - name: string - type?: 'table' | 'grid' | 'list' | 'kanban' | undefined - default?: boolean | undefined - columns?: - | { - id: string - width?: number | undefined - displayName?: string | undefined - showSummary?: boolean | undefined - }[] - | undefined - filters?: unknown - order?: { property: string; direction: 'asc' | 'desc' }[] | undefined - groupBy?: - | { - property: string - direction?: 'asc' | 'desc' | undefined - collapsed?: boolean | undefined - showSummary?: boolean | undefined - } - | undefined - limit?: number | undefined - showSummaries?: boolean | undefined - } - } - ] - ) => Awaited< - Promise - > - 'graph:get-graph-data': ( - ...args: [] - ) => Awaited<{ - nodes: { - id: string - type: 'note' | 'task' | 'project' | 'journal' - label: string - tags: string[] - wordCount: number - connectionCount: number - emoji: string | null - color: string - isOrphan: boolean - isUnresolved: boolean - }[] - edges: { - id: string - source: string - target: string - type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' - weight: number - }[] - }> - 'graph:get-local-graph': ( - ...args: [{ noteId: string; depth?: number | undefined }] - ) => Awaited<{ - nodes: { - id: string - type: 'note' | 'task' | 'project' | 'journal' - label: string - tags: string[] - wordCount: number - connectionCount: number - emoji: string | null - color: string - isOrphan: boolean - isUnresolved: boolean - }[] - edges: { - id: string - source: string - target: string - type: 'wikilink' | 'task-note' | 'project-task' | 'tag-cooccurrence' - weight: number - }[] - }> - 'inbox:add-tag': ( - ...args: [any, any] - ) => Awaited> - 'inbox:archive': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-archive': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-archive-older-than': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-file': ( - ...args: [any] - ) => Awaited> - 'inbox:bulk-snooze': ( - ...args: [any] - ) => Awaited< - Promise<{ - success: boolean - processedCount: number - errors: { itemId: string; error: string }[] - }> - > - 'inbox:bulk-tag': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-clip': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-image': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-link': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-pdf': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-text': ( - ...args: [any] - ) => Awaited> - 'inbox:capture-voice': ( - ...args: [any] - ) => Awaited> - 'inbox:convert-to-note': ( - ...args: [any] - ) => Awaited> - 'inbox:convert-to-task': ( - ...args: [any] - ) => Awaited> - 'inbox:delete-permanent': ( - ...args: [any] - ) => Awaited> - 'inbox:file': ( - ...args: [any] - ) => Awaited> - 'inbox:file-all-stale': ( - ...args: [] - ) => Awaited> - 'inbox:get': ( - ...args: [any] - ) => Awaited> - 'inbox:get-filing-history': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:get-patterns': ( - ...args: [] - ) => Awaited> - 'inbox:get-snoozed': (...args: []) => Awaited> - 'inbox:get-stale-threshold': (...args: []) => Awaited> - 'inbox:get-stats': ( - ...args: [] - ) => Awaited> - 'inbox:get-suggestions': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:get-tags': (...args: []) => Awaited> - 'inbox:link-to-note': ( - ...args: [any, any, any] - ) => Awaited> - 'inbox:list': ( - ...args: [any] - ) => Awaited> - 'inbox:list-archived': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:mark-viewed': ( - ...args: [any] - ) => Awaited> - 'inbox:remove-tag': ( - ...args: [any, any] - ) => Awaited> - 'inbox:retry-metadata': ( - ...args: [any] - ) => Awaited> - 'inbox:retry-transcription': ( - ...args: [any] - ) => Awaited> - 'inbox:set-stale-threshold': (...args: [any]) => Awaited> - 'inbox:snooze': ( - ...args: [any] - ) => Awaited> - 'inbox:track-suggestion': ( - ...args: [any, any, any, any, any, any, any] - ) => Awaited> - 'inbox:unarchive': ( - ...args: [any] - ) => Awaited> - 'inbox:undo-archive': ( - ...args: [any] - ) => Awaited> - 'inbox:undo-file': ( - ...args: [any] - ) => Awaited> - 'inbox:unsnooze': ( - ...args: [any] - ) => Awaited> - 'inbox:update': ( - ...args: [any] - ) => Awaited> - 'journal:createEntry': ( - ...args: [ - { - date: string - content?: string | undefined - tags?: string[] | undefined - properties?: Record | undefined - } - ] - ) => Awaited< - Promise<{ - id: string - date: string - content: string - wordCount: number - characterCount: number - tags: string[] - createdAt: string - modifiedAt: string - properties?: Record | undefined - }> - > - 'journal:deleteEntry': (...args: [{ date: string }]) => Awaited> - 'journal:getAllTags': (...args: []) => Awaited> - 'journal:getDayContext': ( - ...args: [{ date: string }] - ) => Awaited< - Promise<{ - date: string - tasks: { - id: string - title: string - completed: boolean - priority?: 'urgent' | 'high' | 'medium' | 'low' | undefined - isOverdue?: boolean | undefined - }[] - events: { - id: string - time: string - title: string - type: 'meeting' | 'focus' | 'event' - attendeeCount?: number | undefined - }[] - overdueCount: number - }> - > - 'journal:getEntry': ( - ...args: [{ date: string }] - ) => Awaited< - Promise<{ - id: string - date: string - content: string - wordCount: number - characterCount: number - tags: string[] - createdAt: string - modifiedAt: string - properties?: Record | undefined - } | null> - > - 'journal:getHeatmap': ( - ...args: [{ year: number }] - ) => Awaited> - 'journal:getMonthEntries': ( - ...args: [{ year: number; month: number }] - ) => Awaited< - Promise< - { - date: string - preview: string - wordCount: number - characterCount: number - activityLevel: 0 | 1 | 2 | 4 | 3 - tags: string[] - }[] - > - > - 'journal:getStreak': ( - ...args: [] - ) => Awaited< - Promise<{ currentStreak: number; longestStreak: number; lastEntryDate: string | null }> - > - 'journal:getYearStats': ( - ...args: [{ year: number }] - ) => Awaited< - Promise< - { - year: number - month: number - entryCount: number - totalWordCount: number - totalCharacterCount: number - averageLevel: number - }[] - > - > - 'journal:updateEntry': ( - ...args: [ - { - date: string - content?: string | undefined - tags?: string[] | undefined - properties?: Record | undefined - } - ] - ) => Awaited< - Promise<{ - id: string - date: string - content: string - wordCount: number - characterCount: number - tags: string[] - createdAt: string - modifiedAt: string - properties?: Record | undefined - }> - > - 'notes:create': ( - ...args: [ - { - title: string - content?: string | undefined - folder?: string | undefined - tags?: string[] | undefined - template?: string | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:create-folder': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:create-property-definition': ( - ...args: [ - { - name: string - type: 'number' | 'date' | 'text' | 'checkbox' | 'url' - options?: string[] | undefined - defaultValue?: unknown - color?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - definition: { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - } - error?: undefined - } - | { success: boolean; definition: null; error: string } - > - > - 'notes:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:delete-attachment': ( - ...args: [{ noteId: string; filename: string }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:delete-folder': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:delete-version': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:exists': (...args: [string]) => Awaited> - 'notes:export-html': ( - ...args: [ - { - noteId: string - includeMetadata?: boolean | undefined - pageSize?: 'A4' | 'Letter' | 'Legal' | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; error: string; path?: undefined } - | { success: boolean; path: string; error?: undefined } - > - > - 'notes:export-pdf': ( - ...args: [ - { - noteId: string - includeMetadata?: boolean | undefined - pageSize?: 'A4' | 'Letter' | 'Legal' | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; error: string; path?: undefined } - | { success: boolean; path: string; error?: undefined } - > - > - 'notes:get': (...args: [string]) => Awaited> - 'notes:get-all-positions': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; positions: Record; error?: undefined } - | { success: boolean; positions: {}; error: string } - > - > - 'notes:get-by-path': (...args: [string]) => Awaited> - 'notes:get-file': ( - ...args: [string] - ) => Awaited> - 'notes:get-folder-config': ( - ...args: [string] - ) => Awaited< - Promise - > - 'notes:get-folder-template': (...args: [string]) => Awaited> - 'notes:get-folders': (...args: []) => Awaited> - 'notes:get-links': ( - ...args: [string] - ) => Awaited> - 'notes:get-local-only-count': (...args: []) => Awaited> - 'notes:get-positions': ( - ...args: [{ folderPath: string }] - ) => Awaited< - Promise< - | { - success: boolean - positions: { path: string; position: number; folderPath: string }[] - error?: undefined - } - | { success: boolean; positions: never[]; error: string } - > - > - 'notes:get-property-definitions': ( - ...args: [] - ) => Awaited< - Promise< - { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - }[] - > - > - 'notes:get-tags': ( - ...args: [] - ) => Awaited> - 'notes:get-version': ( - ...args: [string] - ) => Awaited> - 'notes:get-versions': ( - ...args: [string] - ) => Awaited> - 'notes:import-files': ( - ...args: [{ sourcePaths: string[]; targetFolder?: string | undefined }] - ) => Awaited> - 'notes:list': ( - ...args: [ - { - folder?: string | undefined - tags?: string[] | undefined - sortBy?: 'title' | 'modified' | 'created' | 'position' | undefined - sortOrder?: 'asc' | 'desc' | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited> - 'notes:list-attachments': ( - ...args: [string] - ) => Awaited> - 'notes:move': ( - ...args: [{ id: string; newFolder: string }] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:open-external': (...args: [string]) => Awaited> - 'notes:rename': ( - ...args: [{ id: string; newTitle: string }] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:rename-folder': ( - ...args: [{ oldPath: string; newPath: string }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:reorder': ( - ...args: [{ folderPath: string; notePaths: string[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:resolve-by-title': ( - ...args: [string] - ) => Awaited< - Promise<{ - id: string - path: string - title: string - fileType: import('../../../../../packages/shared/src/file-types').FileType - } | null> - > - 'notes:restore-version': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:reveal-in-finder': (...args: [string]) => Awaited> - 'notes:set-folder-config': ( - ...args: [ - { - folderPath: string - config: { template?: string | undefined; inherit?: boolean | undefined } - } - ] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'notes:set-local-only': ( - ...args: [{ id: string; localOnly: boolean }] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:show-import-dialog': ( - ...args: [] - ) => Awaited> - 'notes:update': ( - ...args: [ - { - id: string - title?: string | undefined - content?: string | undefined - tags?: string[] | undefined - frontmatter?: Record | undefined - emoji?: string | null | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; note: import('../vault/notes').Note; error?: undefined } - | { success: boolean; note: null; error: string } - > - > - 'notes:update-property-definition': ( - ...args: [ - { - name: string - type?: 'number' | 'date' | 'text' | 'checkbox' | 'url' | undefined - options?: string[] | undefined - defaultValue?: unknown - color?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - definition: - | { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - } - | undefined - error?: undefined - } - | { success: boolean; definition: null; error: string } - > - > - 'notes:upload-attachment': ( - ...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer }] - ) => Awaited> - 'properties:get': ( - ...args: [{ entityId: string }] - ) => Awaited> - 'properties:rename': ( - ...args: [{ entityId: string; oldName: string; newName: string }] - ) => Awaited< - Promise - > - 'properties:set': ( - ...args: [{ entityId: string; properties: Record }] - ) => Awaited< - Promise - > - 'quick-capture:get-clipboard': (...args: []) => Awaited - 'reminder:bulk-dismiss': ( - ...args: [{ reminderIds: string[] }] - ) => Awaited< - Promise< - | { success: boolean; dismissedCount: number; error?: undefined } - | { success: boolean; dismissedCount: number; error: string } - > - > - 'reminder:count-pending': (...args: []) => Awaited> - 'reminder:create': ( - ...args: [ - | { - targetType: 'note' - targetId: string - remindAt: string - title?: string | undefined - note?: string | undefined - } - | { - targetType: 'journal' - targetId: string - remindAt: string - title?: string | undefined - note?: string | undefined - } - | { - targetType: 'highlight' - targetId: string - highlightText: string - highlightStart: number - highlightEnd: number - remindAt: string - title?: string | undefined - note?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - | { success: boolean; reminder: null; error: string } - > - > - 'reminder:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'reminder:dismiss': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: boolean; reminder: null; error: string } - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - > - > - 'reminder:get': ( - ...args: [string] - ) => Awaited< - Promise - > - 'reminder:get-due': ( - ...args: [] - ) => Awaited< - Promise - > - 'reminder:get-for-target': ( - ...args: [{ targetType: 'note' | 'journal' | 'highlight'; targetId: string }] - ) => Awaited> - 'reminder:get-upcoming': ( - ...args: [number | undefined] - ) => Awaited< - Promise<{ - reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] - total: number - hasMore: boolean - }> - > - 'reminder:list': ( - ...args: [ - { - targetType?: 'note' | 'journal' | 'highlight' | undefined - targetId?: string | undefined - status?: - | 'pending' - | 'triggered' - | 'dismissed' - | 'snoozed' - | ('pending' | 'triggered' | 'dismissed' | 'snoozed')[] - | undefined - fromDate?: string | undefined - toDate?: string | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise<{ - reminders: import('../../../../../packages/contracts/src/reminders-api').ReminderWithTarget[] - total: number - hasMore: boolean - }> - > - 'reminder:snooze': ( - ...args: [{ id: string; snoozeUntil: string }] - ) => Awaited< - Promise< - | { success: boolean; reminder: null; error: string } - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - > - > - 'reminder:update': ( - ...args: [ - { - id: string - remindAt?: string | undefined - title?: string | null | undefined - note?: string | null | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; reminder: null; error: string } - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - error?: undefined - } - > - > - 'saved-filters:create': ( - ...args: [ - { - name: string - config: { - filters: { - search?: string | undefined - projectIds?: string[] | undefined - priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined - dueDate?: - | { - type: - | 'custom' - | 'any' - | 'none' - | 'overdue' - | 'today' - | 'tomorrow' - | 'this-week' - | 'next-week' - | 'this-month' - customStart?: string | null | undefined - customEnd?: string | null | undefined - } - | undefined - statusIds?: string[] | undefined - completion?: 'active' | 'completed' | 'all' | undefined - repeatType?: 'all' | 'repeating' | 'one-time' | undefined - hasTime?: 'all' | 'with-time' | 'without-time' | undefined - } - sort?: - | { - field: 'title' | 'createdAt' | 'priority' | 'dueDate' | 'completedAt' | 'project' - direction: 'asc' | 'desc' - } - | undefined - starred?: boolean | undefined - } - } - ] - ) => Awaited< - Promise<{ - success: boolean - savedFilter: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter - }> - > - 'saved-filters:delete': ( - ...args: [{ id: string }] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'saved-filters:list': ( - ...args: [] - ) => Awaited< - Promise<{ - savedFilters: import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter[] - }> - > - 'saved-filters:reorder': ( - ...args: [{ ids: string[]; positions: number[] }] - ) => Awaited> - 'saved-filters:update': ( - ...args: [ - { - id: string - name?: string | undefined - config?: - | { - filters: { - search?: string | undefined - projectIds?: string[] | undefined - priorities?: ('urgent' | 'high' | 'medium' | 'low' | 'none')[] | undefined - dueDate?: - | { - type: - | 'custom' - | 'any' - | 'none' - | 'overdue' - | 'today' - | 'tomorrow' - | 'this-week' - | 'next-week' - | 'this-month' - customStart?: string | null | undefined - customEnd?: string | null | undefined - } - | undefined - statusIds?: string[] | undefined - completion?: 'active' | 'completed' | 'all' | undefined - repeatType?: 'all' | 'repeating' | 'one-time' | undefined - hasTime?: 'all' | 'with-time' | 'without-time' | undefined - } - sort?: - | { - field: - | 'title' - | 'createdAt' - | 'priority' - | 'dueDate' - | 'completedAt' - | 'project' - direction: 'asc' | 'desc' - } - | undefined - starred?: boolean | undefined - } - | undefined - position?: number | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; savedFilter: null; error: string } - | { - success: boolean - savedFilter: - | import('../../../../../packages/contracts/src/saved-filters-api').SavedFilter - | null - error?: undefined - } - > - > - 'search:add-reason': ( - ...args: [ - { - itemId: string - itemType: 'note' | 'task' | 'journal' | 'inbox' - itemTitle: string - searchQuery: string - itemIcon?: string | null | undefined - } - ] - ) => Awaited> - 'search:clear-reasons': (...args: []) => Awaited> - 'search:get-all-tags': (...args: []) => Awaited> - 'search:get-reasons': ( - ...args: [] - ) => Awaited> - 'search:get-stats': ( - ...args: [] - ) => Awaited> - 'search:query': ( - ...args: [ - { - text: string - types?: ('note' | 'task' | 'journal' | 'inbox')[] | undefined - tags?: string[] | undefined - dateRange?: { from: string; to: string } | null | undefined - projectId?: string | null | undefined - folderPath?: string | null | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited> - 'search:quick': ( - ...args: [string] - ) => Awaited< - Promise - > - 'search:rebuild-index': ( - ...args: [] - ) => Awaited< - Promise< - | { - notes: number - tasks: number - inbox: number - durationMs: number - started: true - error?: undefined - } - | { started: false; error: string } - > - > - 'settings:get': (...args: [string]) => Awaited - 'settings:getAIModelStatus': ( - ...args: [] - ) => Awaited> - 'settings:getAISettings': (...args: []) => Awaited - 'settings:getBackupSettings': ( - ...args: [] - ) => Awaited<{ - autoBackup: boolean - frequencyHours: 1 | 6 | 12 | 24 - maxBackups: number - lastBackupAt: string | null - }> - 'settings:getEditorSettings': ( - ...args: [] - ) => Awaited<{ - width: 'medium' | 'narrow' | 'wide' - spellCheck: boolean - autoSaveDelay: number - showWordCount: boolean - toolbarMode: 'floating' | 'sticky' - }> - 'settings:getGeneralSettings': ( - ...args: [] - ) => Awaited<{ - theme: 'light' | 'dark' | 'white' | 'system' - fontSize: 'small' | 'medium' | 'large' - fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' - accentColor: string - startOnBoot: boolean - language: string - onboardingCompleted: boolean - }> - 'settings:getGraphSettings': ( - ...args: [] - ) => Awaited<{ - layout: 'forceatlas2' | 'circular' | 'random' - showLabels: boolean - showEdgeLabels: boolean - animateLayout: boolean - showTagEdges: boolean - }> - 'settings:getJournalSettings': ( - ...args: [] - ) => Awaited<{ - defaultTemplate: string | null - showSchedule: boolean - showTasks: boolean - showAIConnections: boolean - showStatsFooter: boolean - }> - 'settings:getKeyboardSettings': ( - ...args: [] - ) => Awaited<{ - overrides: Record< - string, - { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } - > - globalCapture: { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } | null - }> - 'settings:getNoteEditorSettings': ( - ...args: [] - ) => Awaited - 'settings:getSyncSettings': (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean }> - 'settings:getTabSettings': (...args: []) => Awaited - 'settings:getTaskSettings': ( - ...args: [] - ) => Awaited<{ - defaultProjectId: string | null - defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' - weekStartDay: 'sunday' | 'monday' - staleInboxDays: number - }> - 'settings:loadAIModel': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; message: string; error?: undefined } - | { success: boolean; error: string; message?: undefined } - | { success: boolean; message?: undefined; error?: undefined } - > - > - 'settings:registerGlobalCapture': ( - ...args: [] - ) => Awaited> - 'settings:reindexEmbeddings': ( - ...args: [] - ) => Awaited< - Promise<{ success: boolean; computed: number; skipped: number; error?: string | undefined }> - > - 'settings:resetKeyboardSettings': ( - ...args: [] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:set': ( - ...args: [{ key: string; value: string }] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setAISettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setBackupSettings': ( - ...args: [ - Partial<{ - autoBackup: boolean - frequencyHours: 1 | 6 | 12 | 24 - maxBackups: number - lastBackupAt: string | null - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setEditorSettings': ( - ...args: [ - Partial<{ - width: 'medium' | 'narrow' | 'wide' - spellCheck: boolean - autoSaveDelay: number - showWordCount: boolean - toolbarMode: 'floating' | 'sticky' - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setGeneralSettings': ( - ...args: [ - Partial<{ - theme: 'light' | 'dark' | 'white' | 'system' - fontSize: 'small' | 'medium' | 'large' - fontFamily: 'system' | 'serif' | 'sans-serif' | 'monospace' - accentColor: string - startOnBoot: boolean - language: string - onboardingCompleted: boolean - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setGraphSettings': ( - ...args: [ - Partial<{ - layout: 'forceatlas2' | 'circular' | 'random' - showLabels: boolean - showEdgeLabels: boolean - animateLayout: boolean - showTagEdges: boolean - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setJournalSettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setKeyboardSettings': ( - ...args: [ - Partial<{ - overrides: Record< - string, - { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } - > - globalCapture: { - key: string - modifiers: { - meta?: boolean | undefined - ctrl?: boolean | undefined - shift?: boolean | undefined - alt?: boolean | undefined - } - } | null - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setNoteEditorSettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setSyncSettings': ( - ...args: [Partial<{ enabled: boolean; autoSync: boolean }>] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'settings:setTabSettings': ( - ...args: [Partial] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'settings:setTaskSettings': ( - ...args: [ - Partial<{ - defaultProjectId: string | null - defaultSortOrder: 'createdAt' | 'priority' | 'dueDate' | 'manual' - weekStartDay: 'sunday' | 'monday' - staleInboxDays: number - }> - ] - ) => Awaited<{ success: boolean; error?: string | undefined }> - 'sync:approve-linking': ( - ...args: [{ sessionId: string }] - ) => Awaited< - Promise - > - 'sync:check-device-status': (...args: []) => Awaited> - 'sync:complete-linking-qr': ( - ...args: [{ sessionId: string }] - ) => Awaited< - Promise - > - 'sync:confirm-recovery-phrase': ( - ...args: [{ confirmed: boolean }] - ) => Awaited> - 'sync:download-attachment': ( - ...args: [{ attachmentId: string; targetPath?: string | undefined }] - ) => Awaited< - Promise< - | { success: boolean; error: string; filePath?: undefined } - | { success: boolean; filePath: string; error?: undefined } - > - > - 'sync:emergency-wipe': (...args: []) => Awaited> - 'sync:generate-linking-qr': ( - ...args: [] - ) => Awaited< - Promise - > - 'sync:get-devices': ( - ...args: [] - ) => Awaited< - Promise<{ - devices: { - id: string - name: string - platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android' - linkedAt: number - lastSyncAt: number | undefined - isCurrentDevice: boolean - }[] - email: string | undefined - }> - > - 'sync:get-download-progress': ( - ...args: [{ attachmentId: string }] - ) => Awaited< - Promise<{ - progress: number - downloadedChunks: number - totalChunks: number - status: 'downloading' - } | null> - > - 'sync:get-history': ( - ...args: [{ limit?: number | undefined; offset?: number | undefined }] - ) => Awaited< - Promise<{ - entries: { - id: string - type: 'error' | 'push' | 'pull' - itemCount: number - direction: string | undefined - details: unknown - durationMs: number | undefined - createdAt: number - }[] - total: number - }> - > - 'sync:get-linking-sas': ( - ...args: [{ sessionId: string }] - ) => Awaited> - 'sync:get-quarantined-items': ( - ...args: [] - ) => Awaited - 'sync:get-queue-size': (...args: []) => Awaited<{ pending: number; failed: number }> - 'sync:get-recovery-phrase': (...args: []) => Awaited - 'sync:get-status': ( - ...args: [] - ) => Awaited< - | import('../../../../../packages/contracts/src/ipc-sync-ops').GetSyncStatusResult - | { status: string; pendingCount: number } - > - 'sync:get-storage-breakdown': ( - ...args: [] - ) => Awaited< - Promise< - import('../../../../../packages/contracts/src/ipc-sync-ops').StorageBreakdownResult | null - > - > - 'sync:get-synced-settings': ( - ...args: [] - ) => Awaited<{ - general?: - | { - theme?: 'light' | 'dark' | 'white' | 'system' | undefined - fontSize?: 'small' | 'medium' | 'large' | undefined - fontFamily?: 'system' | 'serif' | 'sans-serif' | 'monospace' | undefined - accentColor?: string | undefined - startOnBoot?: boolean | undefined - language?: string | undefined - } - | undefined - editor?: - | { - width?: 'medium' | 'narrow' | 'wide' | undefined - spellCheck?: boolean | undefined - autoSaveDelay?: number | undefined - showWordCount?: boolean | undefined - toolbarMode?: 'floating' | 'sticky' | undefined - } - | undefined - tasks?: - | { - defaultProjectId?: string | null | undefined - defaultSortOrder?: 'createdAt' | 'priority' | 'dueDate' | 'manual' | undefined - weekStartDay?: 'sunday' | 'monday' | undefined - staleInboxDays?: number | undefined - showCompleted?: boolean | undefined - sortBy?: string | undefined - } - | undefined - keyboard?: { overrides?: Record | undefined } | undefined - notes?: - | { - defaultFolder?: string | undefined - editorFontSize?: number | undefined - spellCheck?: boolean | undefined - } - | undefined - sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined } | undefined - } | null> - 'sync:get-upload-progress': ( - ...args: [{ sessionId: string }] - ) => Awaited< - Promise<{ - progress: number - uploadedChunks: number - totalChunks: number - status: 'uploading' - } | null> - > - 'sync:link-via-qr': ( - ...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined }] - ) => Awaited> - 'sync:link-via-recovery': ( - ...args: [{ recoveryPhrase: string }] - ) => Awaited< - Promise< - | { success: boolean; error: string; deviceId?: undefined } - | { success: boolean; deviceId: string; error?: undefined } - > - > - 'sync:logout': ( - ...args: [] - ) => Awaited> - 'sync:pause': (...args: []) => Awaited<{ success: boolean; wasPaused: boolean }> - 'sync:remove-device': ( - ...args: [{ deviceId: string }] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'sync:rename-device': ( - ...args: [{ deviceId: string; newName: string }] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'sync:resume': (...args: []) => Awaited<{ success: boolean; pendingCount: number }> - 'sync:setup-first-device': ( - ...args: [{ oauthToken: string; provider: 'google'; state: string }] - ) => Awaited< - Promise< - | { - success: boolean - needsRecoverySetup: boolean - deviceId: string - needsRecoveryInput?: undefined - } - | { - success: boolean - needsRecoverySetup: boolean - needsRecoveryInput: boolean - deviceId?: undefined - } - > - > - 'sync:setup-new-account': ( - ...args: [] - ) => Awaited< - Promise< - | { success: boolean; error: string; deviceId?: undefined } - | { success: boolean; deviceId: string; error?: undefined } - > - > - 'sync:trigger-sync': ( - ...args: [] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'sync:update-synced-setting': ( - ...args: [unknown] - ) => Awaited<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - 'sync:upload-attachment': ( - ...args: [{ noteId: string; filePath: string }] - ) => Awaited< - Promise< - | { success: boolean; error: string; attachmentId?: undefined; sessionId?: undefined } - | { success: boolean; attachmentId: string; sessionId: string; error?: undefined } - > - > - 'tags:delete': ( - ...args: [string] - ) => Awaited> - 'tags:get-all-with-counts': ( - ...args: [] - ) => Awaited< - Promise - > - 'tags:get-notes-by-tag': ( - ...args: [ - { - tag: string - sortBy?: 'title' | 'modified' | 'created' | undefined - sortOrder?: 'asc' | 'desc' | undefined - } - ] - ) => Awaited< - Promise - > - 'tags:merge': ( - ...args: [{ source: string; target: string }] - ) => Awaited> - 'tags:pin-note-to-tag': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise - > - 'tags:remove-from-note': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise - > - 'tags:rename': ( - ...args: [{ oldName: string; newName: string }] - ) => Awaited> - 'tags:unpin-note-from-tag': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise - > - 'tags:update-color': ( - ...args: [{ tag: string; color: string }] - ) => Awaited< - Promise - > - 'tasks:archive': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'tasks:bulk-archive': ( - ...args: [{ ids: string[] }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:bulk-complete': ( - ...args: [{ ids: string[] }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:bulk-delete': ( - ...args: [{ ids: string[] }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:bulk-move': ( - ...args: [{ ids: string[]; projectId: string }] - ) => Awaited< - Promise< - | { success: boolean; count: number; error?: undefined } - | { success: boolean; count: number; error: string } - > - > - 'tasks:complete': ( - ...args: [{ id: string; completedAt?: string | undefined }] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:convert-to-subtask': ( - ...args: [{ taskId: string; parentId: string }] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:convert-to-task': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:create': ( - ...args: [ - { - projectId: string - title: string - description?: string | null | undefined - priority?: number | undefined - statusId?: string | null | undefined - parentId?: string | null | undefined - dueDate?: string | null | undefined - dueTime?: string | null | undefined - startDate?: string | null | undefined - isRepeating?: boolean | undefined - repeatConfig?: - | { - frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' - endType: 'date' | 'never' | 'count' - createdAt: string - interval?: number | undefined - daysOfWeek?: number[] | undefined - monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined - dayOfMonth?: number | undefined - weekOfMonth?: number | undefined - dayOfWeekForMonth?: number | undefined - endDate?: string | null | undefined - endCount?: number | undefined - completedCount?: number | undefined - } - | null - | undefined - repeatFrom?: 'due' | 'completion' | null | undefined - tags?: string[] | undefined - linkedNoteIds?: string[] | undefined - sourceNoteId?: string | null | undefined - position?: number | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - | { success: boolean; task: null; error: string } - > - > - 'tasks:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:duplicate': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:get': ( - ...args: [string] - ) => Awaited< - Promise<{ - tags: string[] - linkedNoteIds: string[] - hasSubtasks: boolean - subtaskCount: number - completedSubtaskCount: number - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } | null> - > - 'tasks:get-linked-tasks': ( - ...args: [string] - ) => Awaited< - Promise< - { - tags: string[] - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - > - > - 'tasks:get-overdue': ( - ...args: [] - ) => Awaited< - Promise<{ - tasks: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:get-stats': ( - ...args: [] - ) => Awaited< - Promise<{ - total: number - completed: number - overdue: number - dueToday: number - dueThisWeek: number - }> - > - 'tasks:get-subtasks': ( - ...args: [string] - ) => Awaited< - Promise< - { - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - > - > - 'tasks:get-tags': (...args: []) => Awaited> - 'tasks:get-today': ( - ...args: [] - ) => Awaited< - Promise<{ - tasks: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:get-upcoming': ( - ...args: [{ days?: number | undefined }] - ) => Awaited< - Promise<{ - tasks: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:list': ( - ...args: [ - { - projectId?: string | undefined - statusId?: string | null | undefined - parentId?: string | null | undefined - includeCompleted?: boolean | undefined - includeArchived?: boolean | undefined - dueBefore?: string | undefined - dueAfter?: string | undefined - tags?: string[] | undefined - search?: string | undefined - sortBy?: 'modified' | 'created' | 'position' | 'priority' | 'dueDate' | undefined - sortOrder?: 'asc' | 'desc' | undefined - limit?: number | undefined - offset?: number | undefined - } - ] - ) => Awaited< - Promise<{ - tasks: { - tags: string[] - linkedNoteIds: string[] - hasSubtasks: boolean - subtaskCount: number - completedSubtaskCount: number - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - }[] - total: number - hasMore: boolean - }> - > - 'tasks:move': ( - ...args: [ - { - taskId: string - position: number - targetProjectId?: string | undefined - targetStatusId?: string | null | undefined - targetParentId?: string | null | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:project-archive': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:project-create': ( - ...args: [ - { - name: string - description?: string | null | undefined - color?: string | undefined - icon?: string | null | undefined - statuses?: - | { - name: string - type: 'todo' | 'in_progress' | 'done' - order: number - color?: string | undefined - }[] - | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - project: { - id: string - name: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - color: string - description: string | null - icon: string | null - isInbox: boolean - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - } - error?: undefined - } - | { success: boolean; project: null; error: string } - > - > - 'tasks:project-delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:project-get': ( - ...args: [string] - ) => Awaited> - 'tasks:project-list': ( - ...args: [] - ) => Awaited> - 'tasks:project-reorder': ( - ...args: [{ projectIds: string[]; positions: number[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:project-update': ( - ...args: [ - { - id: string - name?: string | undefined - description?: string | null | undefined - color?: string | undefined - icon?: string | null | undefined - statuses?: - | { - name: string - type: 'todo' | 'in_progress' | 'done' - order: number - id?: string | undefined - color?: string | undefined - }[] - | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; project: null; error: string } - | { - success: boolean - project: { - id: string - name: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - color: string - description: string | null - icon: string | null - isInbox: boolean - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - } - error?: undefined - } - > - > - 'tasks:reorder': ( - ...args: [{ taskIds: string[]; positions: number[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:seed-demo': (...args: []) => Awaited> - 'tasks:seed-performance-test': ( - ...args: [] - ) => Awaited> - 'tasks:status-create': ( - ...args: [ - { projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined } - ] - ) => Awaited< - Promise< - | { - success: boolean - status: { - id: string - name: string - createdAt: string - position: number - color: string - projectId: string - isDefault: boolean - isDone: boolean - } - error?: undefined - } - | { success: boolean; status: null; error: string } - > - > - 'tasks:status-delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:status-list': ( - ...args: [string] - ) => Awaited< - Promise< - { - id: string - name: string - createdAt: string - position: number - color: string - projectId: string - isDefault: boolean - isDone: boolean - }[] - > - > - 'tasks:status-reorder': ( - ...args: [{ statusIds: string[]; positions: number[] }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'tasks:status-update': ( - ...args: [ - { - id: string - name?: string | undefined - color?: string | undefined - position?: number | undefined - isDefault?: boolean | undefined - isDone?: boolean | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; error: string; status?: undefined } - | { - success: boolean - status: { - id: string - name: string - createdAt: string - position: number - color: string - projectId: string - isDefault: boolean - isDone: boolean - } - error?: undefined - } - > - > - 'tasks:unarchive': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'tasks:uncomplete': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'tasks:update': ( - ...args: [ - { - id: string - title?: string | undefined - description?: string | null | undefined - priority?: number | undefined - projectId?: string | undefined - statusId?: string | null | undefined - parentId?: string | null | undefined - dueDate?: string | null | undefined - dueTime?: string | null | undefined - startDate?: string | null | undefined - isRepeating?: boolean | undefined - repeatConfig?: - | { - frequency: 'daily' | 'weekly' | 'monthly' | 'yearly' - endType: 'date' | 'never' | 'count' - createdAt: string - interval?: number | undefined - daysOfWeek?: number[] | undefined - monthlyType?: 'dayOfMonth' | 'weekPattern' | undefined - dayOfMonth?: number | undefined - weekOfMonth?: number | undefined - dayOfWeekForMonth?: number | undefined - endDate?: string | null | undefined - endCount?: number | undefined - completedCount?: number | undefined - } - | null - | undefined - repeatFrom?: 'due' | 'completion' | null | undefined - tags?: string[] | undefined - linkedNoteIds?: string[] | undefined - } - ] - ) => Awaited< - Promise< - | { success: boolean; task: null; error: string } - | { - success: boolean - task: { - linkedNoteIds: string[] - id: string - title: string - clock: import('../../../../../packages/contracts/src/sync-api').VectorClock | null - syncedAt: string | null - createdAt: string - modifiedAt: string - position: number - description: string | null - projectId: string - priority: number - statusId: string | null - parentId: string | null - dueDate: string | null - dueTime: string | null - startDate: string | null - repeatConfig: unknown - repeatFrom: string | null - sourceNoteId: string | null - archivedAt: string | null - fieldClocks: import('../../../../../packages/contracts/src/sync-api').FieldClocks | null - completedAt: string | null - } - error?: undefined - } - > - > - 'templates:create': ( - ...args: [ - { - name: string - description?: string | undefined - icon?: string | null | undefined - tags?: string[] | undefined - properties?: - | { - name: string - type: - | 'number' - | 'date' - | 'text' - | 'select' - | 'checkbox' - | 'url' - | 'multiselect' - | 'rating' - value: unknown - options?: string[] | undefined - }[] - | undefined - content?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - error?: undefined - } - | { success: boolean; template: null; error: string } - > - > - 'templates:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'templates:duplicate': ( - ...args: [{ id: string; newName: string }] - ) => Awaited< - Promise< - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - error?: undefined - } - | { success: boolean; template: null; error: string } - > - > - 'templates:get': ( - ...args: [string] - ) => Awaited< - Promise - > - 'templates:list': ( - ...args: [] - ) => Awaited< - Promise<{ - templates: import('../../../../../packages/contracts/src/templates-api').TemplateListItem[] - }> - > - 'templates:update': ( - ...args: [ - { - id: string - name?: string | undefined - description?: string | undefined - icon?: string | null | undefined - tags?: string[] | undefined - properties?: - | { - name: string - type: - | 'number' - | 'date' - | 'text' - | 'select' - | 'checkbox' - | 'url' - | 'multiselect' - | 'rating' - value: unknown - options?: string[] | undefined - }[] - | undefined - content?: string | undefined - } - ] - ) => Awaited< - Promise< - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - error?: undefined - } - | { success: boolean; template: null; error: string } - > - > - 'vault:close': (...args: []) => Awaited> - 'vault:get-all': ( - ...args: [] - ) => Awaited> - 'vault:get-config': ( - ...args: [] - ) => Awaited> - 'vault:get-status': ( - ...args: [] - ) => Awaited> - 'vault:reindex': (...args: []) => Awaited> - 'vault:remove': (...args: [string]) => Awaited> - 'vault:select': ( - ...args: [{ path?: string | undefined }] - ) => Awaited< - Promise - > - 'vault:switch': ( - ...args: [string] - ) => Awaited< - Promise - > - 'vault:update-config': ( - ...args: [ - { - excludePatterns?: string[] | undefined - defaultNoteFolder?: string | undefined - journalFolder?: string | undefined - attachmentsFolder?: string | undefined - } - ] - ) => Awaited> + "account:getInfo": (...args: []) => Awaited + "account:getRecoveryKey": (...args: []) => Awaited> + "account:signOut": (...args: []) => Awaited> + "ai-inline:get-server-port": (...args: []) => Awaited + "ai-inline:get-settings": (...args: []) => Awaited + "ai-inline:set-settings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "ai-inline:start-server": (...args: []) => Awaited> + "ai-inline:stop-server": (...args: []) => Awaited> + "auth:init-oauth": (...args: [{ provider: "google"; }]) => Awaited> + "auth:refresh-token": (...args: []) => Awaited> + "auth:request-otp": (...args: [{ email: string; }]) => Awaited> + "auth:resend-otp": (...args: [{ email: string; }]) => Awaited> + "auth:verify-otp": (...args: [{ email: string; code: string; }]) => Awaited> + "bookmarks:bulk-create": (...args: [{ items: { itemType: string; itemId: string; }[]; }]) => Awaited> + "bookmarks:bulk-delete": (...args: [{ bookmarkIds: string[]; }]) => Awaited> + "bookmarks:create": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "bookmarks:delete": (...args: [string]) => Awaited> + "bookmarks:get": (...args: [string]) => Awaited> + "bookmarks:get-by-item": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "bookmarks:is-bookmarked": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "bookmarks:list": (...args: [{ itemType?: string | undefined; sortBy?: "createdAt" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "bookmarks:list-by-type": (...args: [string]) => Awaited> + "bookmarks:reorder": (...args: [{ bookmarkIds: string[]; }]) => Awaited> + "bookmarks:toggle": (...args: [{ itemType: string; itemId: string; }]) => Awaited> + "context-menu:show": (...args: [{ id: string; label: string; accelerator?: string | undefined; disabled?: boolean | undefined; type?: "normal" | "separator" | undefined; }[]]) => Awaited> + "crdt:apply-update": (...args: [unknown]) => Awaited> + "crdt:close-doc": (...args: [unknown]) => Awaited> + "crdt:open-doc": (...args: [unknown]) => Awaited> + "crdt:sync-step-1": (...args: [{ noteId: string; stateVector: number[]; }]) => Awaited> + "crdt:sync-step-2": (...args: [{ noteId: string; diff: number[]; }]) => Awaited> + "crypto:decrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> + "crypto:encrypt-item": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; content: Record; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> + "crypto:get-rotation-progress": (...args: []) => Awaited + "crypto:rotate-keys": (...args: [{ confirm: boolean; }]) => Awaited> + "crypto:verify-signature": (...args: [{ itemId: string; type: "note" | "filter" | "task" | "project" | "journal" | "settings" | "inbox" | "tag_definition"; encryptedKey: string; keyNonce: string; encryptedData: string; dataNonce: string; signature: string; operation?: "create" | "update" | "delete" | undefined; deletedAt?: number | undefined; metadata?: Record | undefined; }]) => Awaited> + "folder-view:delete-view": (...args: [{ folderPath: string; viewName: string; }]) => Awaited> + "folder-view:folder-exists": (...args: [string]) => Awaited + "folder-view:get-available-properties": (...args: [{ folderPath: string; }]) => Awaited> + "folder-view:get-config": (...args: [{ folderPath: string; }]) => Awaited> + "folder-view:get-folder-suggestions": (...args: [{ noteId: string; }]) => Awaited> + "folder-view:get-views": (...args: [{ folderPath: string; }]) => Awaited> + "folder-view:list-with-properties": (...args: [{ folderPath: string; properties?: string[] | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "folder-view:set-config": (...args: [{ folderPath: string; config: { path?: string | undefined; template?: string | undefined; inherit?: boolean | undefined; formulas?: Record | undefined; properties?: Record | undefined; summaries?: Record | undefined; views?: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }[] | undefined; }; }]) => Awaited> + "folder-view:set-view": (...args: [{ folderPath: string; view: { name: string; type?: "table" | "grid" | "list" | "kanban" | undefined; default?: boolean | undefined; columns?: { id: string; width?: number | undefined; displayName?: string | undefined; showSummary?: boolean | undefined; }[] | undefined; filters?: unknown; order?: { property: string; direction: "asc" | "desc"; }[] | undefined; groupBy?: { property: string; direction?: "asc" | "desc" | undefined; collapsed?: boolean | undefined; showSummary?: boolean | undefined; } | undefined; limit?: number | undefined; showSummaries?: boolean | undefined; }; }]) => Awaited> + "graph:get-graph-data": (...args: []) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> + "graph:get-local-graph": (...args: [{ noteId: string; depth?: number | undefined; }]) => Awaited<{ nodes: { id: string; type: "note" | "task" | "project" | "journal"; label: string; tags: string[]; wordCount: number; connectionCount: number; emoji: string | null; color: string; isOrphan: boolean; isUnresolved: boolean; }[]; edges: { id: string; source: string; target: string; type: "wikilink" | "task-note" | "project-task" | "tag-cooccurrence"; weight: number; }[]; }> + "inbox:add-tag": (...args: [any, any]) => Awaited> + "inbox:archive": (...args: [any]) => Awaited> + "inbox:bulk-archive": (...args: [any]) => Awaited> + "inbox:bulk-archive-older-than": (...args: [any]) => Awaited> + "inbox:bulk-file": (...args: [any]) => Awaited> + "inbox:bulk-snooze": (...args: [any]) => Awaited> + "inbox:bulk-tag": (...args: [any]) => Awaited> + "inbox:capture-clip": (...args: [any]) => Awaited> + "inbox:capture-image": (...args: [any]) => Awaited> + "inbox:capture-link": (...args: [any]) => Awaited> + "inbox:capture-pdf": (...args: [any]) => Awaited> + "inbox:capture-text": (...args: [any]) => Awaited> + "inbox:capture-voice": (...args: [any]) => Awaited> + "inbox:convert-to-note": (...args: [any]) => Awaited> + "inbox:convert-to-task": (...args: [any]) => Awaited> + "inbox:delete-permanent": (...args: [any]) => Awaited> + "inbox:file": (...args: [any]) => Awaited> + "inbox:file-all-stale": (...args: []) => Awaited> + "inbox:get": (...args: [any]) => Awaited> + "inbox:get-filing-history": (...args: [any]) => Awaited> + "inbox:get-patterns": (...args: []) => Awaited> + "inbox:get-snoozed": (...args: []) => Awaited> + "inbox:get-stale-threshold": (...args: []) => Awaited> + "inbox:get-stats": (...args: []) => Awaited> + "inbox:get-suggestions": (...args: [any]) => Awaited> + "inbox:get-tags": (...args: []) => Awaited> + "inbox:link-to-note": (...args: [any, any, any]) => Awaited> + "inbox:list": (...args: [any]) => Awaited> + "inbox:list-archived": (...args: [any]) => Awaited> + "inbox:mark-viewed": (...args: [any]) => Awaited> + "inbox:remove-tag": (...args: [any, any]) => Awaited> + "inbox:retry-metadata": (...args: [any]) => Awaited> + "inbox:retry-transcription": (...args: [any]) => Awaited> + "inbox:set-stale-threshold": (...args: [any]) => Awaited> + "inbox:snooze": (...args: [any]) => Awaited> + "inbox:track-suggestion": (...args: [any, any, any, any, any, any, any]) => Awaited> + "inbox:unarchive": (...args: [any]) => Awaited> + "inbox:undo-archive": (...args: [any]) => Awaited> + "inbox:undo-file": (...args: [any]) => Awaited> + "inbox:unsnooze": (...args: [any]) => Awaited> + "inbox:update": (...args: [any]) => Awaited> + "journal:createEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> + "journal:deleteEntry": (...args: [{ date: string; }]) => Awaited> + "journal:getAllTags": (...args: []) => Awaited> + "journal:getDayContext": (...args: [{ date: string; }]) => Awaited> + "journal:getEntry": (...args: [{ date: string; }]) => Awaited | undefined; } | null>> + "journal:getHeatmap": (...args: [{ year: number; }]) => Awaited> + "journal:getMonthEntries": (...args: [{ year: number; month: number; }]) => Awaited> + "journal:getStreak": (...args: []) => Awaited> + "journal:getYearStats": (...args: [{ year: number; }]) => Awaited> + "journal:updateEntry": (...args: [{ date: string; content?: string | undefined; tags?: string[] | undefined; properties?: Record | undefined; }]) => Awaited | undefined; }>> + "notes:create": (...args: [{ title: string; content?: string | undefined; folder?: string | undefined; tags?: string[] | undefined; template?: string | undefined; }]) => Awaited> + "notes:create-folder": (...args: [string]) => Awaited> + "notes:create-property-definition": (...args: [{ name: string; type: "number" | "date" | "text" | "checkbox" | "url"; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> + "notes:delete": (...args: [string]) => Awaited> + "notes:delete-attachment": (...args: [{ noteId: string; filename: string; }]) => Awaited> + "notes:delete-folder": (...args: [string]) => Awaited> + "notes:delete-version": (...args: [string]) => Awaited> + "notes:exists": (...args: [string]) => Awaited> + "notes:export-html": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> + "notes:export-pdf": (...args: [{ noteId: string; includeMetadata?: boolean | undefined; pageSize?: "A4" | "Letter" | "Legal" | undefined; }]) => Awaited> + "notes:get": (...args: [string]) => Awaited> + "notes:get-all-positions": (...args: []) => Awaited; error?: undefined; } | { success: boolean; positions: {}; error: string; }>> + "notes:get-by-path": (...args: [string]) => Awaited> + "notes:get-file": (...args: [string]) => Awaited> + "notes:get-folder-config": (...args: [string]) => Awaited> + "notes:get-folder-template": (...args: [string]) => Awaited> + "notes:get-folders": (...args: []) => Awaited> + "notes:get-links": (...args: [string]) => Awaited> + "notes:get-local-only-count": (...args: []) => Awaited> + "notes:get-positions": (...args: [{ folderPath: string; }]) => Awaited> + "notes:get-property-definitions": (...args: []) => Awaited> + "notes:get-tags": (...args: []) => Awaited> + "notes:get-version": (...args: [string]) => Awaited> + "notes:get-versions": (...args: [string]) => Awaited> + "notes:import-files": (...args: [{ sourcePaths: string[]; targetFolder?: string | undefined; }]) => Awaited> + "notes:list": (...args: [{ folder?: string | undefined; tags?: string[] | undefined; sortBy?: "title" | "modified" | "created" | "position" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "notes:list-attachments": (...args: [string]) => Awaited> + "notes:move": (...args: [{ id: string; newFolder: string; }]) => Awaited> + "notes:open-external": (...args: [string]) => Awaited> + "notes:rename": (...args: [{ id: string; newTitle: string; }]) => Awaited> + "notes:rename-folder": (...args: [{ oldPath: string; newPath: string; }]) => Awaited> + "notes:reorder": (...args: [{ folderPath: string; notePaths: string[]; }]) => Awaited> + "notes:resolve-by-title": (...args: [string]) => Awaited> + "notes:restore-version": (...args: [string]) => Awaited> + "notes:reveal-in-finder": (...args: [string]) => Awaited> + "notes:set-folder-config": (...args: [{ folderPath: string; config: { template?: string | undefined; inherit?: boolean | undefined; }; }]) => Awaited> + "notes:set-local-only": (...args: [{ id: string; localOnly: boolean; }]) => Awaited> + "notes:show-import-dialog": (...args: []) => Awaited> + "notes:update": (...args: [{ id: string; title?: string | undefined; content?: string | undefined; tags?: string[] | undefined; frontmatter?: Record | undefined; emoji?: string | null | undefined; }]) => Awaited> + "notes:update-property-definition": (...args: [{ name: string; type?: "number" | "date" | "text" | "checkbox" | "url" | undefined; options?: string[] | undefined; defaultValue?: unknown; color?: string | undefined; }]) => Awaited> + "notes:upload-attachment": (...args: [{ noteId: string; filename: string; data: number[] | ArrayBuffer; }]) => Awaited> + "properties:get": (...args: [{ entityId: string; }]) => Awaited> + "properties:rename": (...args: [{ entityId: string; oldName: string; newName: string; }]) => Awaited> + "properties:set": (...args: [{ entityId: string; properties: Record; }]) => Awaited> + "quick-capture:get-clipboard": (...args: []) => Awaited + "reminder:bulk-dismiss": (...args: [{ reminderIds: string[]; }]) => Awaited> + "reminder:count-pending": (...args: []) => Awaited> + "reminder:create": (...args: [{ targetType: "note"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "journal"; targetId: string; remindAt: string; title?: string | undefined; note?: string | undefined; } | { targetType: "highlight"; targetId: string; highlightText: string; highlightStart: number; highlightEnd: number; remindAt: string; title?: string | undefined; note?: string | undefined; }]) => Awaited> + "reminder:delete": (...args: [string]) => Awaited> + "reminder:dismiss": (...args: [string]) => Awaited> + "reminder:get": (...args: [string]) => Awaited> + "reminder:get-due": (...args: []) => Awaited> + "reminder:get-for-target": (...args: [{ targetType: "note" | "journal" | "highlight"; targetId: string; }]) => Awaited> + "reminder:get-upcoming": (...args: [number | undefined]) => Awaited> + "reminder:list": (...args: [{ targetType?: "note" | "journal" | "highlight" | undefined; targetId?: string | undefined; status?: "pending" | "triggered" | "dismissed" | "snoozed" | ("pending" | "triggered" | "dismissed" | "snoozed")[] | undefined; fromDate?: string | undefined; toDate?: string | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "reminder:snooze": (...args: [{ id: string; snoozeUntil: string; }]) => Awaited> + "reminder:update": (...args: [{ id: string; remindAt?: string | undefined; title?: string | null | undefined; note?: string | null | undefined; }]) => Awaited> + "saved-filters:create": (...args: [{ name: string; config: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; }; }]) => Awaited> + "saved-filters:delete": (...args: [{ id: string; }]) => Awaited> + "saved-filters:list": (...args: []) => Awaited> + "saved-filters:reorder": (...args: [{ ids: string[]; positions: number[]; }]) => Awaited> + "saved-filters:update": (...args: [{ id: string; name?: string | undefined; config?: { filters: { search?: string | undefined; projectIds?: string[] | undefined; priorities?: ("urgent" | "high" | "medium" | "low" | "none")[] | undefined; dueDate?: { type: "custom" | "any" | "none" | "overdue" | "today" | "tomorrow" | "this-week" | "next-week" | "this-month"; customStart?: string | null | undefined; customEnd?: string | null | undefined; } | undefined; statusIds?: string[] | undefined; completion?: "active" | "completed" | "all" | undefined; repeatType?: "all" | "repeating" | "one-time" | undefined; hasTime?: "all" | "with-time" | "without-time" | undefined; }; sort?: { field: "title" | "createdAt" | "priority" | "dueDate" | "completedAt" | "project"; direction: "asc" | "desc"; } | undefined; starred?: boolean | undefined; } | undefined; position?: number | undefined; }]) => Awaited> + "search:add-reason": (...args: [{ itemId: string; itemType: "note" | "task" | "journal" | "inbox"; itemTitle: string; searchQuery: string; itemIcon?: string | null | undefined; }]) => Awaited> + "search:clear-reasons": (...args: []) => Awaited> + "search:get-all-tags": (...args: []) => Awaited> + "search:get-reasons": (...args: []) => Awaited> + "search:get-stats": (...args: []) => Awaited> + "search:query": (...args: [{ text: string; types?: ("note" | "task" | "journal" | "inbox")[] | undefined; tags?: string[] | undefined; dateRange?: { from: string; to: string; } | null | undefined; projectId?: string | null | undefined; folderPath?: string | null | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "search:quick": (...args: [string]) => Awaited> + "search:rebuild-index": (...args: []) => Awaited> + "settings:get": (...args: [string]) => Awaited + "settings:getAIModelStatus": (...args: []) => Awaited> + "settings:getAISettings": (...args: []) => Awaited + "settings:getBackupSettings": (...args: []) => Awaited<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }> + "settings:getEditorSettings": (...args: []) => Awaited<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }> + "settings:getGeneralSettings": (...args: []) => Awaited<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }> + "settings:getGraphSettings": (...args: []) => Awaited<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }> + "settings:getJournalSettings": (...args: []) => Awaited<{ defaultTemplate: string | null; showSchedule: boolean; showTasks: boolean; showAIConnections: boolean; showStatsFooter: boolean; }> + "settings:getKeyboardSettings": (...args: []) => Awaited<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }> + "settings:getNoteEditorSettings": (...args: []) => Awaited + "settings:getSyncSettings": (...args: []) => Awaited<{ enabled: boolean; autoSync: boolean; }> + "settings:getTabSettings": (...args: []) => Awaited + "settings:getTaskSettings": (...args: []) => Awaited<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }> + "settings:loadAIModel": (...args: []) => Awaited> + "settings:registerGlobalCapture": (...args: []) => Awaited> + "settings:reindexEmbeddings": (...args: []) => Awaited> + "settings:resetKeyboardSettings": (...args: []) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:set": (...args: [{ key: string; value: string; }]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setAISettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setBackupSettings": (...args: [Partial<{ autoBackup: boolean; frequencyHours: 1 | 6 | 12 | 24; maxBackups: number; lastBackupAt: string | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setEditorSettings": (...args: [Partial<{ width: "medium" | "narrow" | "wide"; spellCheck: boolean; autoSaveDelay: number; showWordCount: boolean; toolbarMode: "floating" | "sticky"; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setGeneralSettings": (...args: [Partial<{ theme: "light" | "dark" | "white" | "system"; fontSize: "small" | "medium" | "large"; fontFamily: "system" | "serif" | "sans-serif" | "monospace"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setGraphSettings": (...args: [Partial<{ layout: "forceatlas2" | "circular" | "random"; showLabels: boolean; showEdgeLabels: boolean; animateLayout: boolean; showTagEdges: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setJournalSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setKeyboardSettings": (...args: [Partial<{ overrides: Record; globalCapture: { key: string; modifiers: { meta?: boolean | undefined; ctrl?: boolean | undefined; shift?: boolean | undefined; alt?: boolean | undefined; }; } | null; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setNoteEditorSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setSyncSettings": (...args: [Partial<{ enabled: boolean; autoSync: boolean; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "settings:setTabSettings": (...args: [Partial]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "settings:setTaskSettings": (...args: [Partial<{ defaultProjectId: string | null; defaultSortOrder: "createdAt" | "priority" | "dueDate" | "manual"; weekStartDay: "sunday" | "monday"; staleInboxDays: number; }>]) => Awaited<{ success: boolean; error?: string | undefined; }> + "sync:approve-linking": (...args: [{ sessionId: string; }]) => Awaited> + "sync:check-device-status": (...args: []) => Awaited> + "sync:complete-linking-qr": (...args: [{ sessionId: string; }]) => Awaited> + "sync:confirm-recovery-phrase": (...args: [{ confirmed: boolean; }]) => Awaited> + "sync:download-attachment": (...args: [{ attachmentId: string; targetPath?: string | undefined; }]) => Awaited> + "sync:emergency-wipe": (...args: []) => Awaited> + "sync:generate-linking-qr": (...args: []) => Awaited> + "sync:get-devices": (...args: []) => Awaited> + "sync:get-download-progress": (...args: [{ attachmentId: string; }]) => Awaited> + "sync:get-history": (...args: [{ limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "sync:get-linking-sas": (...args: [{ sessionId: string; }]) => Awaited> + "sync:get-quarantined-items": (...args: []) => Awaited + "sync:get-queue-size": (...args: []) => Awaited<{ pending: number; failed: number; }> + "sync:get-recovery-phrase": (...args: []) => Awaited + "sync:get-status": (...args: []) => Awaited + "sync:get-storage-breakdown": (...args: []) => Awaited> + "sync:get-synced-settings": (...args: []) => Awaited<{ general?: { theme?: "light" | "dark" | "white" | "system" | undefined; fontSize?: "small" | "medium" | "large" | undefined; fontFamily?: "system" | "serif" | "sans-serif" | "monospace" | undefined; accentColor?: string | undefined; startOnBoot?: boolean | undefined; language?: string | undefined; } | undefined; editor?: { width?: "medium" | "narrow" | "wide" | undefined; spellCheck?: boolean | undefined; autoSaveDelay?: number | undefined; showWordCount?: boolean | undefined; toolbarMode?: "floating" | "sticky" | undefined; } | undefined; tasks?: { defaultProjectId?: string | null | undefined; defaultSortOrder?: "createdAt" | "priority" | "dueDate" | "manual" | undefined; weekStartDay?: "sunday" | "monday" | undefined; staleInboxDays?: number | undefined; showCompleted?: boolean | undefined; sortBy?: string | undefined; } | undefined; keyboard?: { overrides?: Record | undefined; } | undefined; notes?: { defaultFolder?: string | undefined; editorFontSize?: number | undefined; spellCheck?: boolean | undefined; } | undefined; sync?: { autoSync?: boolean | undefined; syncIntervalMinutes?: number | undefined; } | undefined; } | null> + "sync:get-upload-progress": (...args: [{ sessionId: string; }]) => Awaited> + "sync:link-via-qr": (...args: [{ qrData: string; oauthToken?: string | undefined; provider?: string | undefined; }]) => Awaited> + "sync:link-via-recovery": (...args: [{ recoveryPhrase: string; }]) => Awaited> + "sync:logout": (...args: []) => Awaited> + "sync:pause": (...args: []) => Awaited<{ success: boolean; wasPaused: boolean; }> + "sync:remove-device": (...args: [{ deviceId: string; }]) => Awaited> + "sync:rename-device": (...args: [{ deviceId: string; newName: string; }]) => Awaited> + "sync:resume": (...args: []) => Awaited<{ success: boolean; pendingCount: number; }> + "sync:setup-first-device": (...args: [{ oauthToken: string; provider: "google"; state: string; }]) => Awaited> + "sync:setup-new-account": (...args: []) => Awaited> + "sync:trigger-sync": (...args: []) => Awaited> + "sync:update-synced-setting": (...args: [unknown]) => Awaited<{ success: boolean; error: string; } | { success: boolean; error?: undefined; }> + "sync:upload-attachment": (...args: [{ noteId: string; filePath: string; }]) => Awaited> + "tags:delete": (...args: [string]) => Awaited> + "tags:get-all-with-counts": (...args: []) => Awaited> + "tags:get-notes-by-tag": (...args: [{ tag: string; sortBy?: "title" | "modified" | "created" | undefined; sortOrder?: "asc" | "desc" | undefined; }]) => Awaited> + "tags:merge": (...args: [{ source: string; target: string; }]) => Awaited> + "tags:pin-note-to-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> + "tags:remove-from-note": (...args: [{ noteId: string; tag: string; }]) => Awaited> + "tags:rename": (...args: [{ oldName: string; newName: string; }]) => Awaited> + "tags:unpin-note-from-tag": (...args: [{ noteId: string; tag: string; }]) => Awaited> + "tags:update-color": (...args: [{ tag: string; color: string; }]) => Awaited> + "tasks:archive": (...args: [string]) => Awaited> + "tasks:bulk-archive": (...args: [{ ids: string[]; }]) => Awaited> + "tasks:bulk-complete": (...args: [{ ids: string[]; }]) => Awaited> + "tasks:bulk-delete": (...args: [{ ids: string[]; }]) => Awaited> + "tasks:bulk-move": (...args: [{ ids: string[]; projectId: string; }]) => Awaited> + "tasks:complete": (...args: [{ id: string; completedAt?: string | undefined; }]) => Awaited> + "tasks:convert-to-subtask": (...args: [{ taskId: string; parentId: string; }]) => Awaited> + "tasks:convert-to-task": (...args: [string]) => Awaited> + "tasks:create": (...args: [{ projectId: string; title: string; description?: string | null | undefined; priority?: number | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; sourceNoteId?: string | null | undefined; position?: number | undefined; }]) => Awaited> + "tasks:delete": (...args: [string]) => Awaited> + "tasks:duplicate": (...args: [string]) => Awaited> + "tasks:get": (...args: [string]) => Awaited> + "tasks:get-linked-tasks": (...args: [string]) => Awaited> + "tasks:get-overdue": (...args: []) => Awaited> + "tasks:get-stats": (...args: []) => Awaited> + "tasks:get-subtasks": (...args: [string]) => Awaited> + "tasks:get-tags": (...args: []) => Awaited> + "tasks:get-today": (...args: []) => Awaited> + "tasks:get-upcoming": (...args: [{ days?: number | undefined; }]) => Awaited> + "tasks:list": (...args: [{ projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; includeCompleted?: boolean | undefined; includeArchived?: boolean | undefined; dueBefore?: string | undefined; dueAfter?: string | undefined; tags?: string[] | undefined; search?: string | undefined; sortBy?: "modified" | "created" | "position" | "priority" | "dueDate" | undefined; sortOrder?: "asc" | "desc" | undefined; limit?: number | undefined; offset?: number | undefined; }]) => Awaited> + "tasks:move": (...args: [{ taskId: string; position: number; targetProjectId?: string | undefined; targetStatusId?: string | null | undefined; targetParentId?: string | null | undefined; }]) => Awaited> + "tasks:project-archive": (...args: [string]) => Awaited> + "tasks:project-create": (...args: [{ name: string; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; color?: string | undefined; }[] | undefined; }]) => Awaited> + "tasks:project-delete": (...args: [string]) => Awaited> + "tasks:project-get": (...args: [string]) => Awaited> + "tasks:project-list": (...args: []) => Awaited> + "tasks:project-reorder": (...args: [{ projectIds: string[]; positions: number[]; }]) => Awaited> + "tasks:project-update": (...args: [{ id: string; name?: string | undefined; description?: string | null | undefined; color?: string | undefined; icon?: string | null | undefined; statuses?: { name: string; type: "todo" | "in_progress" | "done"; order: number; id?: string | undefined; color?: string | undefined; }[] | undefined; }]) => Awaited> + "tasks:reorder": (...args: [{ taskIds: string[]; positions: number[]; }]) => Awaited> + "tasks:seed-demo": (...args: []) => Awaited> + "tasks:seed-performance-test": (...args: []) => Awaited> + "tasks:status-create": (...args: [{ projectId: string; name: string; color?: string | undefined; isDone?: boolean | undefined; }]) => Awaited> + "tasks:status-delete": (...args: [string]) => Awaited> + "tasks:status-list": (...args: [string]) => Awaited> + "tasks:status-reorder": (...args: [{ statusIds: string[]; positions: number[]; }]) => Awaited> + "tasks:status-update": (...args: [{ id: string; name?: string | undefined; color?: string | undefined; position?: number | undefined; isDefault?: boolean | undefined; isDone?: boolean | undefined; }]) => Awaited> + "tasks:unarchive": (...args: [string]) => Awaited> + "tasks:uncomplete": (...args: [string]) => Awaited> + "tasks:update": (...args: [{ id: string; title?: string | undefined; description?: string | null | undefined; priority?: number | undefined; projectId?: string | undefined; statusId?: string | null | undefined; parentId?: string | null | undefined; dueDate?: string | null | undefined; dueTime?: string | null | undefined; startDate?: string | null | undefined; isRepeating?: boolean | undefined; repeatConfig?: { frequency: "daily" | "weekly" | "monthly" | "yearly"; endType: "date" | "never" | "count"; createdAt: string; interval?: number | undefined; daysOfWeek?: number[] | undefined; monthlyType?: "dayOfMonth" | "weekPattern" | undefined; dayOfMonth?: number | undefined; weekOfMonth?: number | undefined; dayOfWeekForMonth?: number | undefined; endDate?: string | null | undefined; endCount?: number | undefined; completedCount?: number | undefined; } | null | undefined; repeatFrom?: "due" | "completion" | null | undefined; tags?: string[] | undefined; linkedNoteIds?: string[] | undefined; }]) => Awaited> + "templates:create": (...args: [{ name: string; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> + "templates:delete": (...args: [string]) => Awaited> + "templates:duplicate": (...args: [{ id: string; newName: string; }]) => Awaited> + "templates:get": (...args: [string]) => Awaited> + "templates:list": (...args: []) => Awaited> + "templates:update": (...args: [{ id: string; name?: string | undefined; description?: string | undefined; icon?: string | null | undefined; tags?: string[] | undefined; properties?: { name: string; type: "number" | "date" | "text" | "select" | "checkbox" | "url" | "multiselect" | "rating"; value: unknown; options?: string[] | undefined; }[] | undefined; content?: string | undefined; }]) => Awaited> + "vault:close": (...args: []) => Awaited> + "vault:get-all": (...args: []) => Awaited> + "vault:get-config": (...args: []) => Awaited> + "vault:get-status": (...args: []) => Awaited> + "vault:reindex": (...args: []) => Awaited> + "vault:remove": (...args: [string]) => Awaited> + "vault:select": (...args: [{ path?: string | undefined; }]) => Awaited> + "vault:switch": (...args: [string]) => Awaited> + "vault:update-config": (...args: [{ excludePatterns?: string[] | undefined; defaultNoteFolder?: string | undefined; journalFolder?: string | undefined; attachmentsFolder?: string | undefined; }]) => Awaited> } export type MainIpcInvokeChannel = keyof MainIpcInvokeHandlers -export type MainIpcInvokeArgs = Parameters -export type MainIpcInvokeResult = ReturnType< - MainIpcInvokeHandlers[C] -> +export type MainIpcInvokeArgs = + Parameters +export type MainIpcInvokeResult = + ReturnType diff --git a/apps/desktop/src/renderer/src/assets/main.css b/apps/desktop/src/renderer/src/assets/main.css index 1053b8657..5325fba0d 100644 --- a/apps/desktop/src/renderer/src/assets/main.css +++ b/apps/desktop/src/renderer/src/assets/main.css @@ -5,6 +5,23 @@ body { -moz-osx-font-smoothing: grayscale; } +/* ===== ACCESSIBILITY: GLOBAL FOCUS INDICATOR ===== + Safety net for any focusable element without an explicit focus ring. + Components that manage their own focus styles (e.g. focus:ring-* classes) + override this via higher-specificity class selectors. + Uses :focus-visible so mouse users are not affected. +*/ +:focus-visible { + outline: 2px solid var(--ring, #8c8c8c); + outline-offset: 2px; +} + +/* Suppress the default outline only when a Tailwind ring is applied instead */ +.focus\:outline-none:focus-visible, +.focus-visible\:outline-none:focus-visible { + outline: none; +} + /* Find in page — CSS Custom Highlight API */ ::highlight(find-matches) { background-color: #fde68a; diff --git a/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx b/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx index 01d3d378d..3719d1227 100644 --- a/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx +++ b/apps/desktop/src/renderer/src/components/filing/tag-autocomplete.tsx @@ -49,6 +49,7 @@ const TagPill = ({ tag, onRemove }: TagPillProps): React.JSX.Element => { return ( { interface SuggestionItemProps { tag: TagWithMeta + id: string isHighlighted: boolean onSelect: (tag: string) => void onMouseEnter: () => void @@ -85,17 +87,20 @@ interface SuggestionItemProps { const SuggestionItem = ({ tag, + id, isHighlighted, onSelect, onMouseEnter }: SuggestionItemProps): React.JSX.Element => { return ( - +
) } @@ -171,6 +176,9 @@ interface TagAutocompleteProps { className?: string } +const LISTBOX_ID = 'tag-autocomplete-listbox' +const getOptionId = (index: number): string => `tag-autocomplete-option-${index}` + export const TagAutocomplete = ({ tags, onTagsChange, @@ -332,10 +340,15 @@ export const TagAutocomplete = ({ onChange={handleInputChange} onKeyDown={handleKeyDown} onFocus={handleInputFocus} + role="combobox" aria-label="Add tags" aria-expanded={isDropdownOpen} aria-haspopup="listbox" + aria-controls={LISTBOX_ID} aria-autocomplete="list" + aria-activedescendant={ + isDropdownOpen && highlightedIndex >= 0 ? getOptionId(highlightedIndex) : undefined + } autoComplete="off" /> @@ -344,11 +357,13 @@ export const TagAutocomplete = ({ (suggestions.length > 0 || (!inputValue.trim() && popularTags.length > 0)) && (
{inputValue.trim() ? ( suggestions @@ -356,6 +371,7 @@ export const TagAutocomplete = ({ .map((tag, index) => ( ( } + const nodeCount = data.nodes.length + const edgeCount = data.edges.length + + const nodeSummary = useMemo(() => { + const counts: Record = {} + data.nodes.forEach((n) => { + counts[n.type] = (counts[n.type] ?? 0) + 1 + }) + return Object.entries(counts) + .map(([type, count]) => `${count} ${type}${count !== 1 ? 's' : ''}`) + .join(', ') + }, [data.nodes]) + + const graphAriaLabel = `Knowledge graph with ${nodeCount} node${nodeCount !== 1 ? 's' : ''} and ${edgeCount} connection${edgeCount !== 1 ? 's' : ''}${nodeSummary ? `: ${nodeSummary}` : ''}.` + return (
- +
+ + {/* Visually-hidden node list for screen readers */} +
    + {data.nodes.map((node) => ( +
  • + {node.label} ({node.type}) +
  • + ))} +
+
(null) const [searchQuery, setSearchQuery] = useState('') const [newTagColor, setNewTagColor] = useState(getRandomColor()) + const [focusedIndex, setFocusedIndex] = useState(-1) useClickOutside(popupRef, onClose, isOpen) @@ -44,6 +45,7 @@ export function TagInputPopup({ if (!isOpen) { setSearchQuery('') setNewTagColor(getRandomColor()) + setFocusedIndex(-1) } }, [isOpen]) @@ -65,22 +67,44 @@ export function TagInputPopup({ return recentTags.filter((tag) => !currentTagIds.includes(tag.id)) }, [recentTags, currentTagIds]) + const visibleTags = filteredTags + const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() onClose() + return + } + + if (e.key === 'ArrowDown') { + e.preventDefault() + setFocusedIndex((prev) => (prev < visibleTags.length - 1 ? prev + 1 : 0)) + return + } + + if (e.key === 'ArrowUp') { + e.preventDefault() + setFocusedIndex((prev) => (prev > 0 ? prev - 1 : visibleTags.length - 1)) + return } + if (e.key === 'Enter') { e.preventDefault() + if (focusedIndex >= 0 && focusedIndex < visibleTags.length) { + const tag = visibleTags[focusedIndex] + if (!currentTagIds.includes(tag.id)) { + onAddTag(tag.id) + onClose() + } + return + } const trimmedQuery = searchQuery.trim() if (trimmedQuery) { if (!exactMatchExists) { - // Create new tag with random color onCreateTag(trimmedQuery, newTagColor) onClose() } else if (filteredTags.length > 0) { - // Select the first matching tag if it exists and not already added const firstTag = filteredTags[0] if (!currentTagIds.includes(firstTag.id)) { onAddTag(firstTag.id) @@ -98,7 +122,9 @@ export function TagInputPopup({ onCreateTag, filteredTags, currentTagIds, - onAddTag + onAddTag, + focusedIndex, + visibleTags ] ) @@ -137,8 +163,20 @@ export function TagInputPopup({ ref={inputRef} type="text" value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={(e) => { + setSearchQuery(e.target.value) + setFocusedIndex(-1) + }} placeholder="Type tag name..." + role="combobox" + aria-label="Search or create tag" + aria-expanded={filteredTags.length > 0} + aria-haspopup="listbox" + aria-autocomplete="list" + aria-controls="tag-input-popup-listbox" + aria-activedescendant={ + focusedIndex >= 0 ? `tag-input-option-${focusedIndex}` : undefined + } className={cn( 'flex-1 bg-transparent text-sm', 'placeholder:text-stone-400', @@ -173,12 +211,13 @@ export function TagInputPopup({
{searchQuery ? 'Matching' : 'All Tags'}
-
- {filteredTags.map((tag) => ( +
+ {filteredTags.map((tag, index) => ( handleTagClick(tag)} /> ))} @@ -199,10 +238,11 @@ export function TagInputPopup({ interface TagOptionProps { tag: Tag isSelected: boolean + isFocused?: boolean onClick: () => void } -function TagOption({ tag, isSelected, onClick }: TagOptionProps) { +function TagOption({ tag, isSelected, isFocused = false, onClick }: TagOptionProps) { const colors = getTagColors(tag.color) return ( @@ -216,11 +256,14 @@ function TagOption({ tag, isSelected, onClick }: TagOptionProps) { 'inline-flex items-center gap-1 rounded-full px-2.5 py-1', 'text-xs font-medium', 'transition-all duration-150', - isSelected ? 'opacity-50 cursor-default' : 'hover:opacity-80 cursor-pointer' + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1', + isSelected ? 'opacity-50 cursor-default' : 'hover:opacity-80 cursor-pointer', + isFocused && !isSelected && 'ring-2 ring-offset-1 opacity-100' )} style={{ backgroundColor: colors.background, - color: colors.text + color: colors.text, + ...(isFocused && !isSelected ? { ringColor: colors.text } : {}) }} > {tag.name} diff --git a/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx b/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx index 94c4ea277..2c51a779b 100644 --- a/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx @@ -51,6 +51,7 @@ export const KanbanCardContent = forwardRef { + if (e.key === 'Enter') { + e.preventDefault() + onClick?.() + } else if (e.key === ' ') { + e.preventDefault() + if (isSelectionMode && onToggleSelect) { + onToggleSelect() + } else { + onToggleComplete?.() + } + } + } + return (
{ @@ -91,6 +106,7 @@ export const KanbanCardContent = forwardRef void + updateTask: (taskId: string, updates: Partial) => void + deleteTask: (taskId: string) => void + registerUndo: (description: string, undoFn: () => void) => string + removeUndoEntry: (id: string) => void +} + +export interface UseUndoableTaskActionsReturn { + createTask: (task: Task) => void + deleteTask: (taskId: string) => void + completeTask: (taskId: string) => void + uncompleteTask: (taskId: string) => void + archiveTask: (taskId: string) => void + updateTaskWithUndo: (taskId: string, updates: Partial) => void +} + +export const useUndoableTaskActions = ({ + tasks, + projects, + addTask, + updateTask, + deleteTask, + registerUndo, + removeUndoEntry +}: UseUndoableTaskActionsOptions): UseUndoableTaskActionsReturn => { + const findTask = useCallback( + (taskId: string): Task | undefined => tasks.find((t) => t.id === taskId), + [tasks] + ) + + const findProject = useCallback( + (projectId: string): Project | undefined => projects.find((p) => p.id === projectId), + [projects] + ) + + // ========== CREATE ========== + + const createTaskWithUndo = useCallback( + (task: Task): void => { + addTask(task) + registerUndo(`Create "${task.title}"`, () => { + deleteTask(task.id) + }) + }, + [addTask, deleteTask, registerUndo] + ) + + // ========== DELETE ========== + + const deleteTaskWithUndo = useCallback( + (taskId: string): void => { + const task = findTask(taskId) + if (!task) return + + const snapshot = { ...task } + deleteTask(taskId) + + const undoId = registerUndo(`Delete "${task.title}"`, () => { + addTask(snapshot) + }) + + toast.success('Task deleted', { + description: `"${task.title}" has been deleted.`, + duration: 10000, + action: { + label: 'Undo', + onClick: () => { + removeUndoEntry(undoId) + addTask(snapshot) + } + } + }) + }, + [findTask, deleteTask, addTask, registerUndo, removeUndoEntry] + ) + + // ========== COMPLETE ========== + + const completeTaskWithUndo = useCallback( + (taskId: string): void => { + const task = findTask(taskId) + if (!task) return + + const project = findProject(task.projectId) + if (!project) return + + const currentStatus = project.statuses.find((s) => s.id === task.statusId) + if (!currentStatus) return + + if (currentStatus.type === 'done') { + return + } + + const doneStatus = getDefaultDoneStatus(project) + const completedAt = new Date() + + const subtasks = getSubtasks(taskId, tasks) + const incompleteSubtasks = subtasks.filter((s) => !s.completedAt) + + const subtaskSnapshots = incompleteSubtasks.map((s) => ({ + id: s.id, + statusId: s.statusId, + completedAt: s.completedAt + })) + + if (task.isRepeating && task.repeatConfig && task.dueDate) { + const config = task.repeatConfig + const newCompletedCount = config.completedCount + 1 + const nextDate = calculateNextOccurrence(task.dueDate, config) + const shouldCreate = shouldCreateNextOccurrence({ + ...config, + completedCount: newCompletedCount + }) + + updateTask(taskId, { + statusId: doneStatus?.id || task.statusId, + completedAt, + isRepeating: false, + repeatConfig: null + }) + + incompleteSubtasks.forEach((subtask) => { + updateTask(subtask.id, { + statusId: doneStatus?.id || subtask.statusId, + completedAt + }) + }) + + let nextOccurrenceId: string | null = null + + if (shouldCreate && nextDate) { + const newTask: Task = { + ...task, + id: generateTaskId(), + dueDate: nextDate, + statusId: getDefaultTodoStatus(project)?.id || task.statusId, + completedAt: null, + createdAt: new Date(), + repeatConfig: { + ...config, + completedCount: newCompletedCount + } + } + nextOccurrenceId = newTask.id + addTask(newTask) + toast.success('Task completed!', { + description: `Next occurrence: ${formatDateShort(nextDate)}` + }) + } else { + toast.success('Series complete!', { + description: 'This was the final occurrence.' + }) + } + + const originalSnapshot = { + statusId: task.statusId, + completedAt: task.completedAt, + isRepeating: task.isRepeating, + repeatConfig: task.repeatConfig + } + + registerUndo(`Complete "${task.title}"`, () => { + updateTask(taskId, originalSnapshot) + subtaskSnapshots.forEach((snap) => { + updateTask(snap.id, { statusId: snap.statusId, completedAt: snap.completedAt }) + }) + if (nextOccurrenceId) { + deleteTask(nextOccurrenceId) + } + }) + } else { + updateTask(taskId, { + statusId: doneStatus?.id || task.statusId, + completedAt + }) + + incompleteSubtasks.forEach((subtask) => { + updateTask(subtask.id, { + statusId: doneStatus?.id || subtask.statusId, + completedAt + }) + }) + + if (incompleteSubtasks.length > 0) { + toast.success('Task completed!', { + description: `Also marked ${incompleteSubtasks.length} subtask(s) as done.` + }) + } + + registerUndo(`Complete "${task.title}"`, () => { + updateTask(taskId, { + statusId: task.statusId, + completedAt: null + }) + subtaskSnapshots.forEach((snap) => { + updateTask(snap.id, { statusId: snap.statusId, completedAt: snap.completedAt }) + }) + }) + } + }, + [findTask, findProject, tasks, updateTask, addTask, deleteTask, registerUndo] + ) + + // ========== UNCOMPLETE ========== + + const uncompleteTaskWithUndo = useCallback( + (taskId: string): void => { + const task = findTask(taskId) + if (!task) return + + const project = findProject(task.projectId) + if (!project) return + + const prevStatusId = task.statusId + const prevCompletedAt = task.completedAt + + const todoStatus = getDefaultTodoStatus(project) + updateTask(taskId, { + statusId: todoStatus?.id || task.statusId, + completedAt: null + }) + + registerUndo(`Uncomplete "${task.title}"`, () => { + updateTask(taskId, { + statusId: prevStatusId, + completedAt: prevCompletedAt + }) + }) + }, + [findTask, findProject, updateTask, registerUndo] + ) + + // ========== ARCHIVE ========== + + const archiveTaskWithUndo = useCallback( + (taskId: string): void => { + const task = findTask(taskId) + if (!task) return + + updateTask(taskId, { archivedAt: new Date() }) + + registerUndo(`Archive "${task.title}"`, () => { + updateTask(taskId, { archivedAt: null }) + }) + }, + [findTask, updateTask, registerUndo] + ) + + // ========== UPDATE (discrete fields only) ========== + + const updateTaskWithUndo = useCallback( + (taskId: string, updates: Partial): void => { + const task = findTask(taskId) + + updateTask(taskId, updates) + + if (!task) return + + const undoableKeys = Object.keys(updates).filter((k) => UNDOABLE_FIELDS.has(k)) + if (undoableKeys.length === 0) return + + const previousValues: Partial = {} + for (const key of undoableKeys) { + ;(previousValues as Record)[key] = ( + task as unknown as Record + )[key] + } + + const fieldLabel = + undoableKeys[0] === 'priority' + ? `Priority → ${String((updates as Partial).priority ?? '')}` + : undoableKeys[0] === 'statusId' + ? 'Status changed' + : undoableKeys[0] === 'dueDate' + ? 'Due date changed' + : undoableKeys[0] === 'projectId' + ? 'Moved to project' + : 'Task updated' + + registerUndo(fieldLabel, () => { + updateTask(taskId, previousValues) + }) + }, + [findTask, updateTask, registerUndo] + ) + + return { + createTask: createTaskWithUndo, + deleteTask: deleteTaskWithUndo, + completeTask: completeTaskWithUndo, + uncompleteTask: uncompleteTaskWithUndo, + archiveTask: archiveTaskWithUndo, + updateTaskWithUndo + } +} From 3f0bb37ed6a109cee8143a46f4f879b46e6d4411 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Fri, 20 Mar 2026 00:00:10 +0300 Subject: [PATCH 11/80] feat(undo): add removeUndoEntry for double-fire prevention Toast undo button + Cmd+Z could both fire the same undo action. removeUndoEntry(id) lets consumers yank a specific entry from the stack after consuming it via an alternate path. --- .../src/renderer/src/hooks/use-undo.test.ts | 71 +++++++++++++++++++ .../src/renderer/src/hooks/use-undo.ts | 19 +++++ 2 files changed, 90 insertions(+) diff --git a/apps/desktop/src/renderer/src/hooks/use-undo.test.ts b/apps/desktop/src/renderer/src/hooks/use-undo.test.ts index 93ff093e5..701a6837d 100644 --- a/apps/desktop/src/renderer/src/hooks/use-undo.test.ts +++ b/apps/desktop/src/renderer/src/hooks/use-undo.test.ts @@ -372,6 +372,77 @@ describe('useUndoKeyboardShortcut', () => { }) }) +// ============================================================================ +// removeUndoEntry Tests +// ============================================================================ + +describe('removeUndoEntry', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should remove a specific entry by ID', () => { + const { result } = renderHook(() => useUndoTracker()) + const undoFn1 = vi.fn() + const undoFn2 = vi.fn() + + let id1 = '' + act(() => { + id1 = result.current.registerUndo('Action 1', undoFn1) + result.current.registerUndo('Action 2', undoFn2) + }) + + // #when — remove the first entry + act(() => { + result.current.removeUndoEntry(id1) + }) + + // #then — only undoFn2 remains; undoing should call it + act(() => { + result.current.undo() + }) + expect(undoFn2).toHaveBeenCalledTimes(1) + expect(undoFn1).not.toHaveBeenCalled() + }) + + it('should be a no-op for non-existent ID', () => { + const { result } = renderHook(() => useUndoTracker()) + const undoFn = vi.fn() + + act(() => { + result.current.registerUndo('Action', undoFn) + }) + + // #when — remove non-existent ID + act(() => { + result.current.removeUndoEntry('undo-does-not-exist') + }) + + // #then — original entry still works (verify by executing undo) + act(() => { + result.current.undo() + }) + expect(undoFn).toHaveBeenCalledTimes(1) + }) + + it('should update canUndo when last entry is removed', () => { + const { result } = renderHook(() => useUndoTracker()) + + let id = '' + act(() => { + id = result.current.registerUndo('Only action', vi.fn()) + }) + + act(() => { + result.current.removeUndoEntry(id) + }) + + // #then — fresh hook read sees empty stack + const { result: freshResult } = renderHook(() => useUndoTracker()) + expect(freshResult.current.canUndo).toBe(false) + }) +}) + // ============================================================================ // createUndoableAction Tests // ============================================================================ diff --git a/apps/desktop/src/renderer/src/hooks/use-undo.ts b/apps/desktop/src/renderer/src/hooks/use-undo.ts index 530d07158..477437b30 100644 --- a/apps/desktop/src/renderer/src/hooks/use-undo.ts +++ b/apps/desktop/src/renderer/src/hooks/use-undo.ts @@ -98,6 +98,18 @@ function popUndoEntry(): UndoEntry | undefined { return entry } +function removeUndoEntryById(id: string): boolean { + const idx = globalUndoStack.findIndex((entry) => entry.id === id) + if (idx === -1) return false + + globalUndoStack.splice(idx, 1) + if (globalUndoStack.length === 0) { + stopCleanupInterval() + } + notifyListeners() + return true +} + function getLastUndoEntry(): UndoEntry | undefined { // Filter out expired entries const now = Date.now() @@ -115,6 +127,8 @@ function getLastUndoEntry(): UndoEntry | undefined { interface UseUndoTrackerReturn { /** Register an undo action */ registerUndo: (description: string, undoFn: () => void) => string + /** Remove a specific undo entry by ID (prevents double-fire from toast + Cmd+Z) */ + removeUndoEntry: (id: string) => void /** Execute the last undo action */ undo: () => boolean /** Whether there's an action that can be undone */ @@ -146,6 +160,10 @@ export const useUndoTracker = (): UseUndoTrackerReturn => { return pushUndoEntry({ description, undoFn }) }, []) + const removeUndoEntry = useCallback((id: string): void => { + removeUndoEntryById(id) + }, []) + const undo = useCallback((): boolean => { const entry = popUndoEntry() if (!entry) { @@ -168,6 +186,7 @@ export const useUndoTracker = (): UseUndoTrackerReturn => { return { registerUndo, + removeUndoEntry, undo, canUndo: !!lastEntry, lastActionDescription: lastEntry?.description ?? null From 8106723482ca249d5044ec103dc50f46fada7add Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Fri, 20 Mar 2026 00:00:48 +0300 Subject: [PATCH 12/80] feat(undo): add Cmd+Z undo for all task CRUD and bulk operations New useUndoableTaskActions hook centralizes undo logic for single-task operations (create, delete, complete, uncomplete, archive, field update). useBulkActions gains optional registerUndo/onAddTask params to register all 8 bulk operations with the global undo stack. 41 new test cases. --- .../src/hooks/use-bulk-actions.test.tsx | 219 +++++ .../renderer/src/hooks/use-bulk-actions.ts | 226 +++-- .../hooks/use-undoable-task-actions.test.ts | 776 ++++++++++++++++++ 3 files changed, 1151 insertions(+), 70 deletions(-) create mode 100644 apps/desktop/src/renderer/src/hooks/use-undoable-task-actions.test.ts diff --git a/apps/desktop/src/renderer/src/hooks/use-bulk-actions.test.tsx b/apps/desktop/src/renderer/src/hooks/use-bulk-actions.test.tsx index 5011af60c..91fb65c6e 100644 --- a/apps/desktop/src/renderer/src/hooks/use-bulk-actions.test.tsx +++ b/apps/desktop/src/renderer/src/hooks/use-bulk-actions.test.tsx @@ -744,4 +744,223 @@ describe('useBulkActions', () => { expect(mockOnDeleteTask).not.toHaveBeenCalled() }) }) + + // ========================================================================== + // UNDO INTEGRATION (Cmd+Z) + // ========================================================================== + + describe('undo integration', () => { + let mockRegisterUndo: ReturnType + let mockOnAddTask: ReturnType + + beforeEach(() => { + mockRegisterUndo = vi.fn().mockReturnValue('undo-bulk-1') + mockOnAddTask = vi.fn() + }) + + const renderBulkWithUndo = (selectedIds: string[] = ['task-1', 'task-2']) => { + return renderHook( + () => + useBulkActions({ + selectedIds, + tasks: mockTasks, + projects: [mockProject], + onUpdateTask: mockOnUpdateTask, + onDeleteTask: mockOnDeleteTask, + onComplete: mockOnComplete, + registerUndo: mockRegisterUndo, + onAddTask: mockOnAddTask + }), + { wrapper } + ) + } + + it('bulkComplete should call registerUndo', async () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + await act(async () => { + await result.current.bulkComplete() + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('2'), + expect.any(Function) + ) + }) + + it('bulkComplete undo should restore original states', async () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + await act(async () => { + await result.current.bulkComplete() + }) + + // #when — execute undo + mockOnUpdateTask.mockClear() + const undoFn = mockRegisterUndo.mock.calls[0][1] + undoFn() + + expect(mockOnUpdateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ statusId: 'todo-status' }) + ) + expect(mockOnUpdateTask).toHaveBeenCalledWith( + 'task-2', + expect.objectContaining({ statusId: 'todo-status' }) + ) + }) + + it('bulkDelete should call registerUndo', async () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + await act(async () => { + await result.current.bulkDelete() + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('2'), + expect.any(Function) + ) + }) + + it('bulkDelete undo should re-create all deleted tasks', async () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + await act(async () => { + await result.current.bulkDelete() + }) + + // #when — execute undo + const undoFn = mockRegisterUndo.mock.calls[0][1] + undoFn() + + expect(mockOnAddTask).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-1' })) + expect(mockOnAddTask).toHaveBeenCalledWith(expect.objectContaining({ id: 'task-2' })) + }) + + it('bulkArchive should call registerUndo', async () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + await act(async () => { + await result.current.bulkArchive() + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('2'), + expect.any(Function) + ) + }) + + it('bulkArchive undo should unarchive all', async () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + await act(async () => { + await result.current.bulkArchive() + }) + + // #when — undo + mockOnUpdateTask.mockClear() + const undoFn = mockRegisterUndo.mock.calls[0][1] + undoFn() + + expect(mockOnUpdateTask).toHaveBeenCalledWith('task-1', { archivedAt: null }) + expect(mockOnUpdateTask).toHaveBeenCalledWith('task-2', { archivedAt: null }) + }) + + it('bulkChangePriority should call registerUndo', () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + act(() => { + result.current.bulkChangePriority('high') + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('Priority'), + expect.any(Function) + ) + }) + + it('bulkChangePriority undo should restore original priorities', () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + act(() => { + result.current.bulkChangePriority('high') + }) + + // #when — undo + mockOnUpdateTask.mockClear() + const undoFn = mockRegisterUndo.mock.calls[0][1] + undoFn() + + expect(mockOnUpdateTask).toHaveBeenCalledWith('task-1', { priority: 'none' }) + expect(mockOnUpdateTask).toHaveBeenCalledWith('task-2', { priority: 'none' }) + }) + + it('bulkChangeDueDate should call registerUndo', () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + act(() => { + result.current.bulkChangeDueDate(new Date('2026-04-01')) + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('Due date'), + expect.any(Function) + ) + }) + + it('bulkMoveToProject should call registerUndo', async () => { + const targetProject = createMockProject({ id: 'project-2', name: 'Target' }) + const { result } = renderHook( + () => + useBulkActions({ + selectedIds: ['task-1', 'task-2'], + tasks: mockTasks, + projects: [mockProject, targetProject], + onUpdateTask: mockOnUpdateTask, + onDeleteTask: mockOnDeleteTask, + onComplete: mockOnComplete, + registerUndo: mockRegisterUndo, + onAddTask: mockOnAddTask + }), + { wrapper } + ) + + await act(async () => { + await result.current.bulkMoveToProject('project-2') + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('Move'), + expect.any(Function) + ) + }) + + it('bulkChangeStatus should call registerUndo', () => { + const { result } = renderBulkWithUndo(['task-1', 'task-2']) + + act(() => { + result.current.bulkChangeStatus('progress-status') + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('Status'), + expect.any(Function) + ) + }) + + it('bulkUncomplete should call registerUndo', () => { + // Use task-3 which is already done + const { result } = renderBulkWithUndo(['task-3']) + + act(() => { + result.current.bulkUncomplete() + }) + + expect(mockRegisterUndo).toHaveBeenCalledWith( + expect.stringContaining('1'), + expect.any(Function) + ) + }) + }) }) diff --git a/apps/desktop/src/renderer/src/hooks/use-bulk-actions.ts b/apps/desktop/src/renderer/src/hooks/use-bulk-actions.ts index 1a484691c..338c0b43d 100644 --- a/apps/desktop/src/renderer/src/hooks/use-bulk-actions.ts +++ b/apps/desktop/src/renderer/src/hooks/use-bulk-actions.ts @@ -16,38 +16,25 @@ import { useVault } from '@/hooks/use-vault' // ============================================================================ export interface UseBulkActionsOptions { - /** Array of selected task IDs */ selectedIds: string[] - /** All tasks */ tasks: Task[] - /** All projects */ projects: Project[] - /** Callback to update a single task */ onUpdateTask: (taskId: string, updates: Partial) => void - /** Callback to delete a single task */ onDeleteTask: (taskId: string) => void - /** Callback when bulk action completes (to clear selection) */ onComplete: () => void + registerUndo?: (description: string, undoFn: () => void) => string + onAddTask?: (task: Task) => void } export interface UseBulkActionsReturn { - /** Complete all selected tasks */ bulkComplete: () => void | Promise - /** Uncomplete all selected tasks */ bulkUncomplete: () => void - /** Change priority for all selected tasks */ bulkChangePriority: (priority: Priority) => void - /** Change due date for all selected tasks */ bulkChangeDueDate: (dueDate: Date | null) => void - /** Move all selected tasks to a different project */ bulkMoveToProject: (projectId: string) => void | Promise - /** Change status for all selected tasks (Kanban) */ bulkChangeStatus: (statusId: string) => void - /** Archive all selected tasks */ bulkArchive: () => void | Promise - /** Delete all selected tasks */ bulkDelete: () => void | Promise - /** Get selected tasks */ getSelectedTasks: () => Task[] } @@ -55,20 +42,19 @@ export interface UseBulkActionsReturn { // HOOK // ============================================================================ -/** - * Hook to handle bulk actions on selected tasks - */ export const useBulkActions = ({ selectedIds, tasks, projects, onUpdateTask, onDeleteTask, - onComplete + onComplete, + registerUndo, + onAddTask }: UseBulkActionsOptions): UseBulkActionsReturn => { - // Get vault status to determine if backend operations are available const { status } = useVault() const isVaultOpen = status?.isOpen ?? false + // ========== HELPERS ========== const getSelectedTasks = useCallback((): Task[] => { @@ -91,7 +77,6 @@ export const useBulkActions = ({ return } - // Store original states for undo const originalStates = tasksToComplete.map((task) => ({ id: task.id, statusId: task.statusId, @@ -100,7 +85,6 @@ export const useBulkActions = ({ const taskIds = tasksToComplete.map((t) => t.id) - // T068: Use backend bulk operation when vault is open if (isVaultOpen) { try { const result = await tasksService.bulkComplete(taskIds) @@ -108,14 +92,12 @@ export const useBulkActions = ({ toast.error(extractErrorMessage(result.error, 'Failed to complete tasks')) return } - // State updates happen via event subscriptions in TasksContext } catch (error) { log.error('bulkComplete backend error:', error) toast.error('Failed to complete tasks') return } } else { - // Fallback to individual updates when vault is not open const now = new Date() tasksToComplete.forEach((task) => { const project = projects.find((p) => p.id === task.projectId) @@ -131,27 +113,35 @@ export const useBulkActions = ({ }) } - toast.success( - `${tasksToComplete.length} task${tasksToComplete.length !== 1 ? 's' : ''} completed`, - { - duration: 10000, // T052: 10-second timeout for undo per spec - action: { - label: 'Undo', - onClick: () => { - originalStates.forEach((state) => { - onUpdateTask(state.id, { - statusId: state.statusId, - completedAt: state.completedAt - }) - }) - toast.success('Changes undone') - } + const undoRestore = () => { + originalStates.forEach((state) => { + onUpdateTask(state.id, { + statusId: state.statusId, + completedAt: state.completedAt + }) + }) + } + + const count = tasksToComplete.length + const desc = `Complete ${count} task${count !== 1 ? 's' : ''}` + + if (registerUndo) { + registerUndo(desc, undoRestore) + } + + toast.success(`${count} task${count !== 1 ? 's' : ''} completed`, { + duration: 10000, + action: { + label: 'Undo', + onClick: () => { + undoRestore() + toast.success('Changes undone') } } - ) + }) onComplete() - }, [getSelectedTasks, projects, onUpdateTask, onComplete, isVaultOpen]) + }, [getSelectedTasks, projects, onUpdateTask, onComplete, isVaultOpen, registerUndo]) const bulkUncomplete = useCallback((): void => { const selectedTasks = getSelectedTasks() @@ -167,6 +157,12 @@ export const useBulkActions = ({ return } + const originalStates = tasksToUncomplete.map((task) => ({ + id: task.id, + statusId: task.statusId, + completedAt: task.completedAt + })) + tasksToUncomplete.forEach((task) => { const project = projects.find((p) => p.id === task.projectId) if (!project) return @@ -180,26 +176,51 @@ export const useBulkActions = ({ } }) - toast.success( - `${tasksToUncomplete.length} task${tasksToUncomplete.length !== 1 ? 's' : ''} restored` - ) + const count = tasksToUncomplete.length + + if (registerUndo) { + registerUndo(`Uncomplete ${count} task${count !== 1 ? 's' : ''}`, () => { + originalStates.forEach((state) => { + onUpdateTask(state.id, { + statusId: state.statusId, + completedAt: state.completedAt + }) + }) + }) + } + + toast.success(`${count} task${count !== 1 ? 's' : ''} restored`) onComplete() - }, [getSelectedTasks, projects, onUpdateTask, onComplete]) + }, [getSelectedTasks, projects, onUpdateTask, onComplete, registerUndo]) const bulkChangePriority = useCallback( (priority: Priority): void => { const count = selectedIds.length if (count === 0) return + const originalPriorities = selectedIds.map((taskId) => { + const task = tasks.find((t) => t.id === taskId) + return { id: taskId, priority: task?.priority ?? ('none' as Priority) } + }) + selectedIds.forEach((taskId) => { onUpdateTask(taskId, { priority }) }) + if (registerUndo) { + const label = priority === 'none' ? 'removed' : `set to ${priority}` + registerUndo(`Priority ${label} for ${count} task${count !== 1 ? 's' : ''}`, () => { + originalPriorities.forEach((snap) => { + onUpdateTask(snap.id, { priority: snap.priority }) + }) + }) + } + const priorityLabel = priority === 'none' ? 'removed' : `set to ${priority}` toast.success(`Priority ${priorityLabel} for ${count} task${count !== 1 ? 's' : ''}`) onComplete() }, - [selectedIds, onUpdateTask, onComplete] + [selectedIds, tasks, onUpdateTask, onComplete, registerUndo] ) const bulkChangeDueDate = useCallback( @@ -207,10 +228,23 @@ export const useBulkActions = ({ const count = selectedIds.length if (count === 0) return + const originalDates = selectedIds.map((taskId) => { + const task = tasks.find((t) => t.id === taskId) + return { id: taskId, dueDate: task?.dueDate ?? null } + }) + selectedIds.forEach((taskId) => { onUpdateTask(taskId, { dueDate }) }) + if (registerUndo) { + registerUndo(`Due date changed for ${count} task${count !== 1 ? 's' : ''}`, () => { + originalDates.forEach((snap) => { + onUpdateTask(snap.id, { dueDate: snap.dueDate }) + }) + }) + } + const message = dueDate ? `Due date set for ${count} task${count !== 1 ? 's' : ''}` : `Due date removed from ${count} task${count !== 1 ? 's' : ''}` @@ -218,7 +252,7 @@ export const useBulkActions = ({ toast.success(message) onComplete() }, - [selectedIds, onUpdateTask, onComplete] + [selectedIds, tasks, onUpdateTask, onComplete, registerUndo] ) const bulkMoveToProject = useCallback( @@ -232,7 +266,16 @@ export const useBulkActions = ({ return } - // T070: Use backend bulk operation when vault is open + const originalMoveStates = selectedIds.map((taskId) => { + const task = tasks.find((t) => t.id === taskId) + return { + id: taskId, + projectId: task?.projectId ?? '', + statusId: task?.statusId ?? '', + completedAt: task?.completedAt ?? null + } + }) + if (isVaultOpen) { try { const result = await tasksService.bulkMove(selectedIds, projectId) @@ -240,26 +283,22 @@ export const useBulkActions = ({ toast.error(extractErrorMessage(result.error, 'Failed to move tasks')) return } - // State updates happen via event subscriptions in TasksContext } catch (error) { log.error('bulkMoveToProject backend error:', error) toast.error('Failed to move tasks') return } } else { - // Fallback to individual updates when vault is not open const defaultStatus = getDefaultTodoStatus(targetProject) selectedIds.forEach((taskId) => { const task = tasks.find((t) => t.id === taskId) if (!task) return - // Get current status type to try to match in new project const currentProject = projects.find((p) => p.id === task.projectId) const currentStatus = currentProject?.statuses.find((s) => s.id === task.statusId) const currentStatusType = currentStatus?.type || 'todo' - // Try to find matching status type in target project let newStatus = targetProject.statuses.find((s) => s.type === currentStatusType) if (!newStatus) { newStatus = defaultStatus @@ -270,7 +309,6 @@ export const useBulkActions = ({ statusId: newStatus?.id || targetProject.statuses[0]?.id } - // Handle completed status if (newStatus?.type === 'done' && !task.completedAt) { updates.completedAt = new Date() } else if (newStatus?.type !== 'done' && task.completedAt) { @@ -281,10 +319,22 @@ export const useBulkActions = ({ }) } + if (registerUndo) { + registerUndo(`Move ${count} task${count !== 1 ? 's' : ''}`, () => { + originalMoveStates.forEach((snap) => { + onUpdateTask(snap.id, { + projectId: snap.projectId, + statusId: snap.statusId, + completedAt: snap.completedAt + }) + }) + }) + } + toast.success(`${count} task${count !== 1 ? 's' : ''} moved to ${targetProject.name}`) onComplete() }, - [selectedIds, tasks, projects, onUpdateTask, onComplete, isVaultOpen] + [selectedIds, tasks, projects, onUpdateTask, onComplete, isVaultOpen, registerUndo] ) const bulkChangeStatus = useCallback( @@ -292,7 +342,6 @@ export const useBulkActions = ({ const count = selectedIds.length if (count === 0) return - // Find the status to get its name and type let statusName = '' let statusType: 'todo' | 'in_progress' | 'done' = 'todo' @@ -305,13 +354,21 @@ export const useBulkActions = ({ } } + const originalStatusStates = selectedIds.map((taskId) => { + const task = tasks.find((t) => t.id === taskId) + return { + id: taskId, + statusId: task?.statusId ?? '', + completedAt: task?.completedAt ?? null + } + }) + selectedIds.forEach((taskId) => { const task = tasks.find((t) => t.id === taskId) if (!task) return const updates: Partial = { statusId } - // Handle completedAt based on status type if (statusType === 'done' && !task.completedAt) { updates.completedAt = new Date() } else if (statusType !== 'done' && task.completedAt) { @@ -321,20 +378,29 @@ export const useBulkActions = ({ onUpdateTask(taskId, updates) }) + if (registerUndo) { + registerUndo(`Status → ${statusName} for ${count} task${count !== 1 ? 's' : ''}`, () => { + originalStatusStates.forEach((snap) => { + onUpdateTask(snap.id, { + statusId: snap.statusId, + completedAt: snap.completedAt + }) + }) + }) + } + toast.success(`${count} task${count !== 1 ? 's' : ''} moved to ${statusName}`) onComplete() }, - [selectedIds, tasks, projects, onUpdateTask, onComplete] + [selectedIds, tasks, projects, onUpdateTask, onComplete, registerUndo] ) const bulkArchive = useCallback(async (): Promise => { const count = selectedIds.length if (count === 0) return - // Store for undo const archivedIds = [...selectedIds] - // T071: Use backend bulk operation when vault is open if (isVaultOpen) { try { const result = await tasksService.bulkArchive(selectedIds) @@ -342,41 +408,55 @@ export const useBulkActions = ({ toast.error(extractErrorMessage(result.error, 'Failed to archive tasks')) return } - // State updates happen via event subscriptions in TasksContext } catch (error) { log.error('bulkArchive backend error:', error) toast.error('Failed to archive tasks') return } } else { - // Fallback to individual updates when vault is not open const now = new Date() selectedIds.forEach((taskId) => { onUpdateTask(taskId, { archivedAt: now }) }) } + const undoRestore = () => { + archivedIds.forEach((taskId) => { + onUpdateTask(taskId, { archivedAt: null }) + }) + } + + const desc = `Archive ${count} task${count !== 1 ? 's' : ''}` + + if (registerUndo) { + registerUndo(desc, undoRestore) + } + toast.success(`${count} task${count !== 1 ? 's' : ''} archived`, { - duration: 10000, // T052: 10-second timeout for undo per spec + duration: 10000, action: { label: 'Undo', onClick: () => { - archivedIds.forEach((taskId) => { - onUpdateTask(taskId, { archivedAt: null }) - }) + undoRestore() toast.success('Tasks restored from archive') } } }) onComplete() - }, [selectedIds, onUpdateTask, onComplete, isVaultOpen]) + }, [selectedIds, onUpdateTask, onComplete, isVaultOpen, registerUndo]) const bulkDelete = useCallback(async (): Promise => { const count = selectedIds.length if (count === 0) return - // T069: Use backend bulk operation when vault is open + const deletedSnapshots = selectedIds + .map((taskId) => { + const task = tasks.find((t) => t.id === taskId) + return task ? { ...task } : null + }) + .filter(Boolean) as Task[] + if (isVaultOpen) { try { const result = await tasksService.bulkDelete(selectedIds) @@ -384,25 +464,31 @@ export const useBulkActions = ({ toast.error(extractErrorMessage(result.error, 'Failed to delete tasks')) return } - // State updates happen via event subscriptions in TasksContext (DELETED events) } catch (error) { log.error('bulkDelete backend error:', error) toast.error('Failed to delete tasks') return } } else { - // Fallback to individual deletes when vault is not open selectedIds.forEach((taskId) => { onDeleteTask(taskId) }) } + if (registerUndo && onAddTask && deletedSnapshots.length > 0) { + registerUndo(`Delete ${count} task${count !== 1 ? 's' : ''}`, () => { + deletedSnapshots.forEach((snapshot) => { + onAddTask(snapshot) + }) + }) + } + toast.success(`${count} task${count !== 1 ? 's' : ''} deleted`, { description: 'This action can be undone for a short time.' }) onComplete() - }, [selectedIds, onDeleteTask, onComplete, isVaultOpen]) + }, [selectedIds, tasks, onDeleteTask, onComplete, isVaultOpen, registerUndo, onAddTask]) return { bulkComplete, diff --git a/apps/desktop/src/renderer/src/hooks/use-undoable-task-actions.test.ts b/apps/desktop/src/renderer/src/hooks/use-undoable-task-actions.test.ts new file mode 100644 index 000000000..e202acc66 --- /dev/null +++ b/apps/desktop/src/renderer/src/hooks/use-undoable-task-actions.test.ts @@ -0,0 +1,776 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { useUndoableTaskActions, UNDOABLE_FIELDS } from './use-undoable-task-actions' +import type { Task, Priority } from '@/data/sample-tasks' + +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + info: vi.fn() + } +})) + +vi.mock('@/lib/logger', () => ({ + createLogger: () => ({ + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn() + }) +})) + +import { toast } from 'sonner' + +// ============================================================================ +// FACTORIES +// ============================================================================ + +const makeTask = (overrides: Partial = {}): Task => ({ + id: 'task-1', + title: 'Test task', + description: '', + projectId: 'proj-1', + statusId: 'status-todo', + priority: 'none' as Priority, + dueDate: null, + dueTime: null, + isRepeating: false, + repeatConfig: null, + linkedNoteIds: [], + sourceNoteId: null, + parentId: null, + subtaskIds: [], + createdAt: new Date('2026-01-01'), + completedAt: null, + archivedAt: null, + ...overrides +}) + +const makeSubtask = (parentId: string, overrides: Partial = {}): Task => + makeTask({ + id: `subtask-${Math.random().toString(36).slice(2, 7)}`, + parentId, + title: 'Subtask', + ...overrides + }) + +// ============================================================================ +// SETUP +// ============================================================================ + +function setup(taskOverrides: Partial[] = [{}]) { + const tasks = taskOverrides.map((o) => makeTask(o)) + + const deps = { + tasks, + addTask: vi.fn(), + updateTask: vi.fn(), + deleteTask: vi.fn(), + registerUndo: vi.fn().mockReturnValue('undo-123'), + removeUndoEntry: vi.fn(), + projects: [ + { + id: 'proj-1', + name: 'Project', + color: '#000', + isArchived: false, + statuses: [ + { id: 'status-todo', name: 'To Do', type: 'todo' as const, position: 0, isDefault: true }, + { + id: 'status-progress', + name: 'In Progress', + type: 'in_progress' as const, + position: 1, + isDefault: false + }, + { id: 'status-done', name: 'Done', type: 'done' as const, position: 2, isDefault: true } + ] + } + ] + } + + const { result } = renderHook(() => useUndoableTaskActions(deps)) + return { result, deps } +} + +// ============================================================================ +// TESTS +// ============================================================================ + +describe('useUndoableTaskActions', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // ---------- createTask ---------- + + describe('createTask', () => { + it('should call addTask with the task', () => { + const { result, deps } = setup() + const task = makeTask({ id: 'new-task', title: 'New' }) + + act(() => { + result.current.createTask(task) + }) + + expect(deps.addTask).toHaveBeenCalledWith(task) + }) + + it('should register undo after creating', () => { + const { result, deps } = setup() + const task = makeTask({ id: 'new-task', title: 'New' }) + + act(() => { + result.current.createTask(task) + }) + + expect(deps.registerUndo).toHaveBeenCalledWith( + expect.stringContaining('New'), + expect.any(Function) + ) + }) + + it('should undo by deleting the created task', () => { + const { result, deps } = setup() + const task = makeTask({ id: 'new-task', title: 'New' }) + + act(() => { + result.current.createTask(task) + }) + + // #when — execute the registered undo function + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + expect(deps.deleteTask).toHaveBeenCalledWith('new-task') + }) + }) + + // ---------- deleteTask ---------- + + describe('deleteTask', () => { + it('should capture full task snapshot before delete', () => { + const task = makeTask({ id: 'task-1', title: 'Delete me', priority: 'high' }) + const { result, deps } = setup([{ id: 'task-1', title: 'Delete me', priority: 'high' }]) + + act(() => { + result.current.deleteTask('task-1') + }) + + expect(deps.deleteTask).toHaveBeenCalledWith('task-1') + }) + + it('should register undo that re-creates the task', () => { + const { result, deps } = setup([{ id: 'task-1', title: 'Delete me' }]) + + act(() => { + result.current.deleteTask('task-1') + }) + + expect(deps.registerUndo).toHaveBeenCalledWith( + expect.stringContaining('Delete me'), + expect.any(Function) + ) + + // #when — execute undo + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + expect(deps.addTask).toHaveBeenCalledWith( + expect.objectContaining({ id: 'task-1', title: 'Delete me' }) + ) + }) + + it('should show toast with undo button', () => { + const { result } = setup([{ id: 'task-1', title: 'Gone' }]) + + act(() => { + result.current.deleteTask('task-1') + }) + + expect(toast.success).toHaveBeenCalledWith( + 'Task deleted', + expect.objectContaining({ + duration: 10000, + action: expect.objectContaining({ label: 'Undo' }) + }) + ) + }) + + it('should remove undo entry from stack when toast undo clicked (double-fire prevention)', () => { + const { result, deps } = setup([{ id: 'task-1', title: 'Gone' }]) + + act(() => { + result.current.deleteTask('task-1') + }) + + // #when — simulate toast undo button click + const toastCall = (toast.success as ReturnType).mock.calls[0] + const toastOptions = toastCall[1] + toastOptions.action.onClick() + + expect(deps.removeUndoEntry).toHaveBeenCalledWith('undo-123') + }) + + it('should be a no-op for non-existent task', () => { + const { result, deps } = setup() + + act(() => { + result.current.deleteTask('non-existent') + }) + + expect(deps.deleteTask).not.toHaveBeenCalled() + expect(deps.registerUndo).not.toHaveBeenCalled() + }) + }) + + // ---------- completeTask ---------- + + describe('completeTask', () => { + it('should mark task as done with correct status', () => { + const { result, deps } = setup([{ id: 'task-1', statusId: 'status-todo' }]) + + act(() => { + result.current.completeTask('task-1') + }) + + expect(deps.updateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + statusId: 'status-done', + completedAt: expect.any(Date) + }) + ) + }) + + it('should register undo that restores previous status', () => { + const { result, deps } = setup([{ id: 'task-1', statusId: 'status-progress' }]) + + act(() => { + result.current.completeTask('task-1') + }) + + expect(deps.registerUndo).toHaveBeenCalledWith( + expect.stringContaining('Test task'), + expect.any(Function) + ) + + // #when — undo + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + expect(deps.updateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + statusId: 'status-progress', + completedAt: null + }) + ) + }) + + it('should complete subtasks when parent completed', () => { + const sub1 = makeSubtask('task-1', { id: 'sub-1', statusId: 'status-todo' }) + const sub2 = makeSubtask('task-1', { + id: 'sub-2', + statusId: 'status-done', + completedAt: new Date() + }) + const parent = makeTask({ + id: 'task-1', + subtaskIds: ['sub-1', 'sub-2'], + statusId: 'status-todo' + }) + + const deps = { + tasks: [parent, sub1, sub2], + addTask: vi.fn(), + updateTask: vi.fn(), + deleteTask: vi.fn(), + registerUndo: vi.fn().mockReturnValue('undo-456'), + removeUndoEntry: vi.fn(), + projects: [ + { + id: 'proj-1', + name: 'Project', + color: '#000', + isArchived: false, + statuses: [ + { + id: 'status-todo', + name: 'To Do', + type: 'todo' as const, + position: 0, + isDefault: true + }, + { + id: 'status-done', + name: 'Done', + type: 'done' as const, + position: 2, + isDefault: true + } + ] + } + ] + } + + const { result } = renderHook(() => useUndoableTaskActions(deps)) + + act(() => { + result.current.completeTask('task-1') + }) + + // sub-1 (incomplete) should be completed; sub-2 (already done) should not be touched + const sub1Update = deps.updateTask.mock.calls.find(([id]: [string]) => id === 'sub-1') + expect(sub1Update).toBeDefined() + expect(sub1Update![1]).toMatchObject({ statusId: 'status-done' }) + }) + + it('should undo restoring subtask states', () => { + const sub1 = makeSubtask('task-1', { id: 'sub-1', statusId: 'status-todo' }) + const parent = makeTask({ + id: 'task-1', + subtaskIds: ['sub-1'], + statusId: 'status-progress' + }) + + const deps = { + tasks: [parent, sub1], + addTask: vi.fn(), + updateTask: vi.fn(), + deleteTask: vi.fn(), + registerUndo: vi.fn().mockReturnValue('undo-789'), + removeUndoEntry: vi.fn(), + projects: [ + { + id: 'proj-1', + name: 'Project', + color: '#000', + isArchived: false, + statuses: [ + { + id: 'status-todo', + name: 'To Do', + type: 'todo' as const, + position: 0, + isDefault: true + }, + { + id: 'status-progress', + name: 'In Progress', + type: 'in_progress' as const, + position: 1, + isDefault: false + }, + { + id: 'status-done', + name: 'Done', + type: 'done' as const, + position: 2, + isDefault: true + } + ] + } + ] + } + + const { result } = renderHook(() => useUndoableTaskActions(deps)) + + act(() => { + result.current.completeTask('task-1') + }) + + // #when — undo + deps.updateTask.mockClear() + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + // parent restored + const parentRestore = deps.updateTask.mock.calls.find(([id]: [string]) => id === 'task-1') + expect(parentRestore![1]).toMatchObject({ statusId: 'status-progress', completedAt: null }) + + // subtask restored + const sub1Restore = deps.updateTask.mock.calls.find(([id]: [string]) => id === 'sub-1') + expect(sub1Restore![1]).toMatchObject({ statusId: 'status-todo', completedAt: null }) + }) + }) + + // ---------- completeTask (repeating) ---------- + + describe('completeTask - repeating', () => { + const makeRepeatingTask = (): Task => + makeTask({ + id: 'repeat-1', + title: 'Recurring', + isRepeating: true, + dueDate: new Date('2026-03-01'), + repeatConfig: { + frequency: 'daily', + interval: 1, + endType: 'never', + completedCount: 0, + createdAt: new Date('2026-01-01') + } + }) + + it('should mark original as done and non-repeating', () => { + const task = makeRepeatingTask() + const deps = { + tasks: [task], + addTask: vi.fn(), + updateTask: vi.fn(), + deleteTask: vi.fn(), + registerUndo: vi.fn().mockReturnValue('undo-r1'), + removeUndoEntry: vi.fn(), + projects: [ + { + id: 'proj-1', + name: 'Project', + color: '#000', + isArchived: false, + statuses: [ + { + id: 'status-todo', + name: 'To Do', + type: 'todo' as const, + position: 0, + isDefault: true + }, + { + id: 'status-done', + name: 'Done', + type: 'done' as const, + position: 2, + isDefault: true + } + ] + } + ] + } + + const { result } = renderHook(() => useUndoableTaskActions(deps)) + + act(() => { + result.current.completeTask('repeat-1') + }) + + expect(deps.updateTask).toHaveBeenCalledWith( + 'repeat-1', + expect.objectContaining({ + statusId: 'status-done', + isRepeating: false, + repeatConfig: null + }) + ) + }) + + it('should create next occurrence', () => { + const task = makeRepeatingTask() + const deps = { + tasks: [task], + addTask: vi.fn(), + updateTask: vi.fn(), + deleteTask: vi.fn(), + registerUndo: vi.fn().mockReturnValue('undo-r2'), + removeUndoEntry: vi.fn(), + projects: [ + { + id: 'proj-1', + name: 'Project', + color: '#000', + isArchived: false, + statuses: [ + { + id: 'status-todo', + name: 'To Do', + type: 'todo' as const, + position: 0, + isDefault: true + }, + { + id: 'status-done', + name: 'Done', + type: 'done' as const, + position: 2, + isDefault: true + } + ] + } + ] + } + + const { result } = renderHook(() => useUndoableTaskActions(deps)) + + act(() => { + result.current.completeTask('repeat-1') + }) + + expect(deps.addTask).toHaveBeenCalledWith( + expect.objectContaining({ + isRepeating: true, + completedAt: null + }) + ) + }) + + it('should undo by restoring original repeat config and deleting next occurrence', () => { + const task = makeRepeatingTask() + const deps = { + tasks: [task], + addTask: vi.fn(), + updateTask: vi.fn(), + deleteTask: vi.fn(), + registerUndo: vi.fn().mockReturnValue('undo-r3'), + removeUndoEntry: vi.fn(), + projects: [ + { + id: 'proj-1', + name: 'Project', + color: '#000', + isArchived: false, + statuses: [ + { + id: 'status-todo', + name: 'To Do', + type: 'todo' as const, + position: 0, + isDefault: true + }, + { + id: 'status-done', + name: 'Done', + type: 'done' as const, + position: 2, + isDefault: true + } + ] + } + ] + } + + const { result } = renderHook(() => useUndoableTaskActions(deps)) + + act(() => { + result.current.completeTask('repeat-1') + }) + + // Capture next occurrence ID + const nextTask = deps.addTask.mock.calls[0][0] + const nextId = nextTask.id + + // #when — undo + deps.updateTask.mockClear() + deps.deleteTask.mockClear() + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + // Original restored with repeat config + expect(deps.updateTask).toHaveBeenCalledWith( + 'repeat-1', + expect.objectContaining({ + isRepeating: true, + repeatConfig: expect.objectContaining({ frequency: 'daily' }) + }) + ) + + // Next occurrence deleted + expect(deps.deleteTask).toHaveBeenCalledWith(nextId) + }) + }) + + // ---------- uncompleteTask ---------- + + describe('uncompleteTask', () => { + it('should move to todo status', () => { + const { result, deps } = setup([ + { + id: 'task-1', + statusId: 'status-done', + completedAt: new Date('2026-03-01') + } + ]) + + act(() => { + result.current.uncompleteTask('task-1') + }) + + expect(deps.updateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + statusId: 'status-todo', + completedAt: null + }) + ) + }) + + it('should register undo that re-completes', () => { + const completedAt = new Date('2026-03-01') + const { result, deps } = setup([{ id: 'task-1', statusId: 'status-done', completedAt }]) + + act(() => { + result.current.uncompleteTask('task-1') + }) + + // #when — undo + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + expect(deps.updateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ + statusId: 'status-done', + completedAt + }) + ) + }) + }) + + // ---------- archiveTask ---------- + + describe('archiveTask', () => { + it('should set archivedAt', () => { + const { result, deps } = setup([{ id: 'task-1' }]) + + act(() => { + result.current.archiveTask('task-1') + }) + + expect(deps.updateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ archivedAt: expect.any(Date) }) + ) + }) + + it('should register undo that unarchives', () => { + const { result, deps } = setup([{ id: 'task-1' }]) + + act(() => { + result.current.archiveTask('task-1') + }) + + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + expect(deps.updateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ archivedAt: null }) + ) + }) + }) + + // ---------- updateTaskWithUndo ---------- + + describe('updateTaskWithUndo', () => { + it('should register undo for priority change', () => { + const { result, deps } = setup([{ id: 'task-1', priority: 'none' }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { priority: 'high' }) + }) + + expect(deps.updateTask).toHaveBeenCalledWith('task-1', { priority: 'high' }) + expect(deps.registerUndo).toHaveBeenCalled() + }) + + it('should register undo for status change', () => { + const { result, deps } = setup([{ id: 'task-1', statusId: 'status-todo' }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { statusId: 'status-progress' }) + }) + + expect(deps.registerUndo).toHaveBeenCalled() + }) + + it('should register undo for due date change', () => { + const { result, deps } = setup([{ id: 'task-1', dueDate: null }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { dueDate: new Date('2026-04-01') }) + }) + + expect(deps.registerUndo).toHaveBeenCalled() + }) + + it('should register undo for project change', () => { + const { result, deps } = setup([{ id: 'task-1', projectId: 'proj-1' }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { projectId: 'proj-2' }) + }) + + expect(deps.registerUndo).toHaveBeenCalled() + }) + + it('should NOT register undo for title change', () => { + const { result, deps } = setup([{ id: 'task-1', title: 'Old' }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { title: 'New' }) + }) + + expect(deps.updateTask).toHaveBeenCalledWith('task-1', { title: 'New' }) + expect(deps.registerUndo).not.toHaveBeenCalled() + }) + + it('should NOT register undo for description change', () => { + const { result, deps } = setup([{ id: 'task-1', description: '' }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { description: 'Updated' }) + }) + + expect(deps.registerUndo).not.toHaveBeenCalled() + }) + + it('should restore previous field value on undo', () => { + const { result, deps } = setup([{ id: 'task-1', priority: 'low' }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { priority: 'urgent' }) + }) + + // #when — undo + deps.updateTask.mockClear() + const undoFn = deps.registerUndo.mock.calls[0][1] + undoFn() + + expect(deps.updateTask).toHaveBeenCalledWith( + 'task-1', + expect.objectContaining({ priority: 'low' }) + ) + }) + + it('should handle mixed undoable and non-undoable fields', () => { + const { result, deps } = setup([{ id: 'task-1', title: 'Old', priority: 'none' }]) + + act(() => { + result.current.updateTaskWithUndo('task-1', { title: 'New', priority: 'high' }) + }) + + // should update both fields + expect(deps.updateTask).toHaveBeenCalledWith('task-1', { title: 'New', priority: 'high' }) + // should register undo because priority is undoable + expect(deps.registerUndo).toHaveBeenCalled() + }) + }) + + // ---------- UNDOABLE_FIELDS constant ---------- + + describe('UNDOABLE_FIELDS', () => { + it('should include discrete fields only', () => { + expect(UNDOABLE_FIELDS).toContain('priority') + expect(UNDOABLE_FIELDS).toContain('statusId') + expect(UNDOABLE_FIELDS).toContain('dueDate') + expect(UNDOABLE_FIELDS).toContain('dueTime') + expect(UNDOABLE_FIELDS).toContain('projectId') + expect(UNDOABLE_FIELDS).toContain('archivedAt') + }) + + it('should not include text fields', () => { + expect(UNDOABLE_FIELDS).not.toContain('title') + expect(UNDOABLE_FIELDS).not.toContain('description') + }) + }) +}) From fc1d6c304f16be87325b2b152efb635f34f0a6e9 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Fri, 20 Mar 2026 00:01:05 +0300 Subject: [PATCH 13/80] refactor(tasks): wire task handlers to useUndoableTaskActions Replace inline handler bodies in tasks.tsx with hook calls. Removes ~100 LOC of completion/repeat/delete logic from the page component. Every task action now registers with the global undo stack. --- apps/desktop/src/renderer/src/hooks/index.ts | 1 + apps/desktop/src/renderer/src/pages/tasks.tsx | 167 ++++-------------- 2 files changed, 34 insertions(+), 134 deletions(-) diff --git a/apps/desktop/src/renderer/src/hooks/index.ts b/apps/desktop/src/renderer/src/hooks/index.ts index 92352a95d..d61ca3b49 100644 --- a/apps/desktop/src/renderer/src/hooks/index.ts +++ b/apps/desktop/src/renderer/src/hooks/index.ts @@ -40,6 +40,7 @@ export * from './use-new-note-shortcut' // Undo export * from './use-undo' +export * from './use-undoable-task-actions' // Bookmarks export * from './use-bookmarks' diff --git a/apps/desktop/src/renderer/src/pages/tasks.tsx b/apps/desktop/src/renderer/src/pages/tasks.tsx index 1188387ae..7c4e14b03 100644 --- a/apps/desktop/src/renderer/src/pages/tasks.tsx +++ b/apps/desktop/src/renderer/src/pages/tasks.tsx @@ -18,15 +18,12 @@ import { GroupByDropdown } from '@/components/tasks/filters' import { cn } from '@/lib/utils' -import { extractErrorMessage } from '@/lib/ipc-error' import { getFilteredTasks, getDefaultTodoStatus, - getDefaultDoneStatus, startOfDay, getCompletedTasks, getCompletedTodayTasks, - formatDateShort, getTodayTasks, countActiveFilters, scopeTasksByProject, @@ -40,9 +37,8 @@ import { type SavedFilter, type CompletionFilterType } from '@/data/tasks-data' -import { createDefaultTask, generateTaskId, type Task, type Priority } from '@/data/sample-tasks' +import { createDefaultTask, type Task, type Priority } from '@/data/sample-tasks' import { addDays } from '@/lib/task-utils' // used by handleBulkChangeDueDate -import { calculateNextOccurrence, shouldCreateNextOccurrence } from '@/lib/repeat-utils' import { useFilterState, useSavedFilters, @@ -52,6 +48,7 @@ import { useSubtaskManagement, useUndoTracker } from '@/hooks' +import { useUndoableTaskActions } from '@/hooks/use-undoable-task-actions' import { useTasksContext } from '@/contexts/tasks' import { useSaveFilterShortcut } from '@/hooks/use-save-filter-shortcut' import { useTaskPreferences } from '@/hooks/use-task-preferences' @@ -125,7 +122,17 @@ export const TasksPage = ({ } = useTasksContext() // T051-T054: Undo tracking for Cmd+Z support - const { registerUndo } = useUndoTracker() + const { registerUndo, removeUndoEntry } = useUndoTracker() + + const undoable = useUndoableTaskActions({ + tasks, + projects, + addTask: contextAddTask, + updateTask: contextUpdateTask, + deleteTask: contextDeleteTask, + registerUndo, + removeUndoEntry + }) const { settings: taskPrefs } = useTaskPreferences() const { openTab } = useTabActions() @@ -352,7 +359,9 @@ export const TasksPage = ({ projects, onUpdateTask: contextUpdateTask, onDeleteTask: contextDeleteTask, - onComplete: deselectAll + onComplete: deselectAll, + registerUndo, + onAddTask: contextAddTask }) // Toggle selection mode handler @@ -569,10 +578,9 @@ export const TasksPage = ({ const handleAddTaskFromModal = useCallback( (newTask: Task): void => { - // Use context addTask to persist to database - contextAddTask(newTask) + undoable.createTask(newTask) }, - [contextAddTask] + [undoable] ) // Get default project and due date for the modal based on current selection @@ -644,8 +652,7 @@ export const TasksPage = ({ const newTask = createDefaultTask(projectId, statusId, title, dueDate) newTask.priority = priority - // Use context addTask to persist to database - contextAddTask(newTask) + undoable.createTask(newTask) }, [ selectedId, @@ -653,7 +660,7 @@ export const TasksPage = ({ selectedProject, selectedProjectId, projects, - contextAddTask, + undoable, taskPrefs.defaultProjectId ] ) @@ -673,151 +680,43 @@ export const TasksPage = ({ newTask.completedAt = new Date() } - contextAddTask(newTask) + undoable.createTask(newTask) }, - [selectedProject, projects, contextAddTask] + [selectedProject, projects, undoable] ) const handleToggleComplete = useCallback( (taskId: string): void => { - const taskToComplete = tasks.find((t) => t.id === taskId) - if (!taskToComplete) return + const task = tasks.find((t) => t.id === taskId) + if (!task) return - const project = projects.find((p) => p.id === taskToComplete.projectId) + const project = projects.find((p) => p.id === task.projectId) if (!project) return - const currentStatus = project.statuses.find((s) => s.id === taskToComplete.statusId) + const currentStatus = project.statuses.find((s) => s.id === task.statusId) if (!currentStatus) return if (currentStatus.type === 'done') { - // Uncomplete: move back to todo status - const todoStatus = getDefaultTodoStatus(project) - contextUpdateTask(taskId, { - statusId: todoStatus?.id || taskToComplete.statusId, - completedAt: null - }) - return - } - - const doneStatus = getDefaultDoneStatus(project) - const completedAt = new Date() - - // Get subtasks to also complete them - const subtasks = getSubtasks(taskId, tasks) - const hasSubtasks = subtasks.length > 0 - - if (taskToComplete.isRepeating && taskToComplete.repeatConfig && taskToComplete.dueDate) { - const config = taskToComplete.repeatConfig - const newCompletedCount = config.completedCount + 1 - const nextDate = calculateNextOccurrence(taskToComplete.dueDate, config) - const shouldCreateNext = shouldCreateNextOccurrence({ - ...config, - completedCount: newCompletedCount - }) - - // Mark the completed task as done (no longer repeating) - contextUpdateTask(taskId, { - statusId: doneStatus?.id || taskToComplete.statusId, - completedAt, - isRepeating: false, - repeatConfig: null - }) - - // Also complete all subtasks - if (hasSubtasks) { - subtasks.forEach((subtask) => { - if (!subtask.completedAt) { - contextUpdateTask(subtask.id, { - statusId: doneStatus?.id || subtask.statusId, - completedAt - }) - } - }) - } - - // Create the next occurrence if needed - if (shouldCreateNext && nextDate) { - const newTask: Task = { - ...taskToComplete, - id: generateTaskId(), - dueDate: nextDate, - statusId: getDefaultTodoStatus(project)?.id || taskToComplete.statusId, - completedAt: null, - createdAt: new Date(), - repeatConfig: { - ...config, - completedCount: newCompletedCount - } - } - contextAddTask(newTask) - toast.success('Task completed!', { - description: `Next occurrence: ${formatDateShort(nextDate)}` - }) - } else { - toast.success('Series complete!', { - description: 'This was the final occurrence.' - }) - } + undoable.uncompleteTask(taskId) } else { - // Simple completion: mark as done - contextUpdateTask(taskId, { - statusId: doneStatus?.id || taskToComplete.statusId, - completedAt - }) - - // Also complete all subtasks - if (hasSubtasks) { - const incompleteSubtasks = subtasks.filter((s) => !s.completedAt) - incompleteSubtasks.forEach((subtask) => { - contextUpdateTask(subtask.id, { - statusId: doneStatus?.id || subtask.statusId, - completedAt - }) - }) - if (incompleteSubtasks.length > 0) { - toast.success('Task completed!', { - description: `Also marked ${incompleteSubtasks.length} subtask(s) as done.` - }) - } - } + undoable.completeTask(taskId) } }, - [tasks, projects, contextUpdateTask, contextAddTask] + [tasks, projects, undoable] ) const handleUpdateTask = useCallback( (taskId: string, updates: Partial): void => { - // Use context updateTask to persist to database - contextUpdateTask(taskId, updates) + undoable.updateTaskWithUndo(taskId, updates) }, - [contextUpdateTask] + [undoable] ) const handleDeleteTask = useCallback( (taskId: string): void => { - const task = tasks.find((t) => t.id === taskId) - if (!task) return - - const deletedTask = { ...task } - - contextDeleteTask(taskId) - - // T051-T054: Register undo for Cmd+Z support - const undoFn = () => { - contextAddTask(deletedTask) - } - registerUndo(`Delete "${task.title}"`, undoFn) - - toast.success('Task deleted', { - description: `"${task.title}" has been deleted.`, - duration: 10000, // T052: 10-second timeout for undo per spec - action: { - label: 'Undo', - onClick: undoFn - } - }) + undoable.deleteTask(taskId) }, - [tasks, contextDeleteTask, contextAddTask, registerUndo] + [undoable] ) const handleAddTaskWithDate = useCallback( From 7af20347d722cd21c45bbb5cba024dc1969d3c60 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Fri, 20 Mar 2026 22:25:59 +0300 Subject: [PATCH 14/80] chore: remove calendar view feature Calendar view was never shipped to users. Remove all components, CSS variables, utility functions, and tests to reduce dead code. --- apps/desktop/src/renderer/src/assets/base.css | 65 ---- .../tasks/calendar/calendar-drag-overlay.tsx | 63 ---- .../tasks/calendar/calendar-grid.tsx | 107 ------ .../tasks/calendar/calendar-header.tsx | 139 ------- .../tasks/calendar/calendar-task-item.tsx | 120 ------ .../tasks/calendar/calendar-view.tsx | 348 ------------------ .../components/tasks/calendar/day-cell.tsx | 187 ---------- .../tasks/calendar/day-detail-popover.tsx | 286 -------------- .../src/components/tasks/calendar/index.ts | 7 - .../src/renderer/src/data/tasks-data.ts | 5 +- .../src/renderer/src/lib/task-utils.test.ts | 281 -------------- .../src/renderer/src/lib/task-utils.ts | 110 ------ apps/desktop/src/renderer/src/pages/tasks.tsx | 65 +--- 13 files changed, 3 insertions(+), 1780 deletions(-) delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/calendar-drag-overlay.tsx delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/calendar-grid.tsx delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/calendar-header.tsx delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/calendar-task-item.tsx delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/calendar-view.tsx delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/day-cell.tsx delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/day-detail-popover.tsx delete mode 100644 apps/desktop/src/renderer/src/components/tasks/calendar/index.ts diff --git a/apps/desktop/src/renderer/src/assets/base.css b/apps/desktop/src/renderer/src/assets/base.css index 631a19a52..812b6d3b5 100644 --- a/apps/desktop/src/renderer/src/assets/base.css +++ b/apps/desktop/src/renderer/src/assets/base.css @@ -285,28 +285,6 @@ --graph-bg: #f6f5f0; --graph-label-color: #1a1a1a; - /* ===== CALENDAR VIEW ===== */ - --cal-bg: #f6f5f0; - --cal-cell-bg: transparent; - --cal-cell-outside-bg: #efefe9; - --cal-cell-weekend-bg: #f0efe8; - --cal-today-bg: #fffbeb; - --cal-today-border: #f59e0b; - --cal-today-badge: #f59e0b; - --cal-today-label: #d97706; - --cal-date-current: #4a4a4a; - --cal-date-outside: #c4c4be; - --cal-weekday: #8c8c8c; - --cal-task-bg: #f6f5f0; - --cal-task-bg-today: rgba(255, 255, 255, 0.8); - --cal-task-text: #4a4a4a; - --cal-task-text-today: #1a1a1a; - --cal-task-overdue-bg: #fef2f2; - --cal-task-overdue-text: #dc2626; - --cal-overflow: #8c8c8c; - --cal-month-text: #1a1a1a; - --cal-grid-gap: 1px; - /* ===== QUEUE LIST ===== */ --queue-bg: #eae8e1; --queue-number-bg: #d4d1c5; @@ -575,28 +553,6 @@ --graph-bg: #0e0e10; --graph-label-color: #e8e6e1; - /* ===== CALENDAR VIEW - Dark Mode ===== */ - --cal-bg: #0e0e10; - --cal-cell-bg: transparent; - --cal-cell-outside-bg: rgba(255, 255, 255, 0.03); - --cal-cell-weekend-bg: rgba(255, 255, 255, 0.02); - --cal-today-bg: rgba(245, 158, 11, 0.08); - --cal-today-border: #d97706; - --cal-today-badge: #d97706; - --cal-today-label: #fbbf24; - --cal-date-current: #d4d4d4; - --cal-date-outside: #525252; - --cal-weekday: #737373; - --cal-task-bg: rgba(255, 255, 255, 0.04); - --cal-task-bg-today: rgba(255, 255, 255, 0.08); - --cal-task-text: #d4d4d4; - --cal-task-text-today: #f5f5f5; - --cal-task-overdue-bg: rgba(239, 68, 68, 0.1); - --cal-task-overdue-text: #f87171; - --cal-overflow: #737373; - --cal-month-text: #f5f5f5; - --cal-grid-gap: 1px; - /* ===== QUEUE LIST - Dark Mode ===== */ --queue-bg: #161618; --queue-number-bg: #2a2a2e; @@ -1298,27 +1254,6 @@ del.bn-inline-content { --color-destructive-foreground: var(--destructive-foreground); --color-ring: var(--ring); - /* ===== CALENDAR VIEW ===== */ - --color-cal-bg: var(--cal-bg); - --color-cal-cell-bg: var(--cal-cell-bg); - --color-cal-cell-outside-bg: var(--cal-cell-outside-bg); - --color-cal-cell-weekend-bg: var(--cal-cell-weekend-bg); - --color-cal-today-bg: var(--cal-today-bg); - --color-cal-today-border: var(--cal-today-border); - --color-cal-today-badge: var(--cal-today-badge); - --color-cal-today-label: var(--cal-today-label); - --color-cal-date-current: var(--cal-date-current); - --color-cal-date-outside: var(--cal-date-outside); - --color-cal-weekday: var(--cal-weekday); - --color-cal-task-bg: var(--cal-task-bg); - --color-cal-task-bg-today: var(--cal-task-bg-today); - --color-cal-task-text: var(--cal-task-text); - --color-cal-task-text-today: var(--cal-task-text-today); - --color-cal-task-overdue-bg: var(--cal-task-overdue-bg); - --color-cal-task-overdue-text: var(--cal-task-overdue-text); - --color-cal-overflow: var(--cal-overflow); - --color-cal-month-text: var(--cal-month-text); - /* ===== QUEUE LIST ===== */ --color-queue-bg: var(--queue-bg); --color-queue-number-bg: var(--queue-number-bg); diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-drag-overlay.tsx b/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-drag-overlay.tsx deleted file mode 100644 index 859602e18..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-drag-overlay.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react' -import { DragOverlay } from '@dnd-kit/core' - -import { cn } from '@/lib/utils' -import type { Task } from '@/data/sample-tasks' -import { isBefore, startOfDay } from '@/lib/task-utils' - -interface CalendarDragOverlayProps { - activeTask: Task | null -} - -const getPriorityBarColor = (priority: Task['priority']): string => { - switch (priority) { - case 'urgent': - return 'var(--task-priority-urgent)' - case 'high': - return 'var(--task-priority-high)' - case 'medium': - return 'var(--task-priority-medium)' - case 'low': - return 'var(--task-priority-low)' - default: - return 'var(--cal-weekday)' - } -} - -export const CalendarDragOverlay = ({ - activeTask -}: CalendarDragOverlayProps): React.JSX.Element => { - if (!activeTask) { - return - } - - const isCompleted = !!activeTask.completedAt - const isOverdue = - activeTask.dueDate !== null && - isBefore(startOfDay(activeTask.dueDate), startOfDay(new Date())) && - !isCompleted - - return ( - -
-
-
- ) -} - -export default CalendarDragOverlay diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-grid.tsx b/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-grid.tsx deleted file mode 100644 index 44b1ca5ee..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-grid.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useMemo } from 'react' - -import { DayCell } from './day-cell' -import { formatDateKey, type CalendarDay } from '@/lib/task-utils' -import type { Task } from '@/data/sample-tasks' - -const WEEKDAYS_SUN_START = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] -const WEEKDAYS_MON_START = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - -interface CalendarGridProps { - days: CalendarDay[] - tasksByDate: Map - allTasks?: Task[] - weekStartsOn?: 0 | 1 - selectedDate: Date | null - focusedDate: Date | null - maxVisibleTasks?: number - isCompact?: boolean - onOpenDay: (date: Date) => void - onTaskClick: (taskId: string) => void - onAddTask: (date: Date) => void -} - -export const CalendarGrid = ({ - days, - tasksByDate, - allTasks = [], - weekStartsOn = 0, - selectedDate, - focusedDate, - maxVisibleTasks = 3, - isCompact = false, - onOpenDay, - onTaskClick, - onAddTask -}: CalendarGridProps): React.JSX.Element => { - const weekdayLabels = useMemo( - () => (weekStartsOn === 0 ? WEEKDAYS_SUN_START : WEEKDAYS_MON_START), - [weekStartsOn] - ) - - const weeks = useMemo(() => { - const result: CalendarDay[][] = [] - for (let i = 0; i < days.length; i += 7) { - result.push(days.slice(i, i + 7)) - } - return result - }, [days]) - - return ( -
- {/* Weekday header */} -
- {weekdayLabels.map((day) => ( -
- {day} -
- ))} -
- - {/* Week rows */} - {weeks.map((week, weekIndex) => ( -
- {week.map((day) => { - const dateKey = formatDateKey(day.date) - const dayTasks = tasksByDate.get(dateKey) || [] - const isSelected = - selectedDate !== null && - selectedDate.getFullYear() === day.date.getFullYear() && - selectedDate.getMonth() === day.date.getMonth() && - selectedDate.getDate() === day.date.getDate() - const isFocused = - focusedDate !== null && - focusedDate.getFullYear() === day.date.getFullYear() && - focusedDate.getMonth() === day.date.getMonth() && - focusedDate.getDate() === day.date.getDate() - - return ( - - ) - })} -
- ))} -
- ) -} - -export default CalendarGrid diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-header.tsx b/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-header.tsx deleted file mode 100644 index c71a3d0db..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-header.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react' -import { ChevronLeft, ChevronRight, Filter } from '@/lib/icons' - -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuCheckboxItem, - DropdownMenuTrigger, - DropdownMenuSeparator -} from '@/components/ui/dropdown-menu' -import type { Project } from '@/data/tasks-data' - -interface CalendarHeaderProps { - currentMonth: Date - onPreviousMonth: () => void - onNextMonth: () => void - onToday: () => void - showCompleted: boolean - onToggleCompleted: (value: boolean) => void - projects?: Project[] - projectFilter: string | null - onProjectFilterChange: (projectId: string | null) => void -} - -export const CalendarHeader = ({ - currentMonth, - onPreviousMonth, - onNextMonth, - onToday, - showCompleted, - onToggleCompleted, - projects, - projectFilter, - onProjectFilterChange -}: CalendarHeaderProps): React.JSX.Element => { - const monthLabel = currentMonth.toLocaleDateString('en-US', { - month: 'long', - year: 'numeric' - }) - - const selectedProject = projects?.find((p) => p.id === projectFilter) - - return ( -
-
- {/* Month navigation */} -
- - -

- {monthLabel} -

- - -
- - {/* Today pill */} - -
- -
- {/* Project Filter */} - {projects && projects.length > 0 && ( - - - - - - onProjectFilterChange(null)} - > - All Projects - - - {projects - .filter((p) => !p.isArchived) - .map((project) => ( - onProjectFilterChange(project.id)} - > - - {project.name} - - ))} - - - )} - - -
-
- ) -} - -export default CalendarHeader diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-task-item.tsx b/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-task-item.tsx deleted file mode 100644 index 397b706fb..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-task-item.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { useMemo } from 'react' - -import { cn } from '@/lib/utils' -import type { Task } from '@/data/sample-tasks' -import { isBefore, startOfDay } from '@/lib/task-utils' -import { getSubtasks, calculateProgress } from '@/lib/subtask-utils' -import { MiniProgressBar } from '@/components/tasks/mini-progress-bar' - -interface CalendarTaskItemProps { - task: Task - allTasks?: Task[] - compact?: boolean - isToday?: boolean - onClick?: (taskId: string) => void -} - -const getPriorityBarColor = (priority: Task['priority']): string => { - switch (priority) { - case 'urgent': - return 'var(--task-priority-urgent)' - case 'high': - return 'var(--task-priority-high)' - case 'medium': - return 'var(--task-priority-medium)' - case 'low': - return 'var(--task-priority-low)' - default: - return 'var(--cal-weekday)' - } -} - -export const CalendarTaskItem = ({ - task, - allTasks = [], - compact = false, - isToday = false, - onClick -}: CalendarTaskItemProps): React.JSX.Element => { - const isCompleted = !!task.completedAt - const isOverdue = - task.dueDate !== null && - isBefore(startOfDay(task.dueDate), startOfDay(new Date())) && - !isCompleted - - const subtasks = useMemo(() => { - if (allTasks.length === 0) return [] - return getSubtasks(task.id, allTasks) - }, [task.id, allTasks]) - - const subtaskProgress = useMemo(() => { - return calculateProgress(subtasks) - }, [subtasks]) - - const hasSubtasks = subtasks.length > 0 - - if (compact) { - return ( - - ) - } - - const handleClick = (): void => { - if (onClick) onClick(task.id) - } - - const handleKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - onClick?.(task.id) - } - } - - return ( -
- {/* Priority bar */} -
- ) -} - -export default CalendarTaskItem diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-view.tsx b/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-view.tsx deleted file mode 100644 index 2f634b3ea..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/calendar-view.tsx +++ /dev/null @@ -1,348 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' - -import { CalendarHeader } from './calendar-header' -import { CalendarGrid } from './calendar-grid' -import { DayDetailPopover } from './day-detail-popover' -import { - addDays, - addMonths, - addWeeks, - endOfMonth, - endOfWeek, - formatDateKey, - formatDateShort, - getCalendarDays, - groupTasksByCalendarDate, - isTaskCompleted, - startOfDay, - startOfMonth, - subMonths, - isBefore, - isAfter, - isSameDay, - type CalendarDay -} from '@/lib/task-utils' -import { calculateNextOccurrence } from '@/lib/repeat-utils' -import type { Project } from '@/data/tasks-data' -import type { Task } from '@/data/sample-tasks' -import { ScrollArea } from '@/components/ui/scroll-area' - -type SelectionType = 'view' | 'project' - -interface CalendarViewProps { - tasks: Task[] - projects: Project[] - selectedId: string - selectedType: SelectionType - onUpdateTask: (taskId: string, updates: Partial) => void - onTaskClick?: (taskId: string) => void - onAddTaskWithDate: (date: Date) => void - onToggleComplete: (taskId: string) => void - // Selection props - isSelectionMode?: boolean - selectedIds?: Set - onToggleSelect?: (taskId: string) => void -} - -const useIsCompact = (): boolean => { - const [compact, setCompact] = useState(false) - - useEffect(() => { - const handleResize = (): void => { - setCompact(window.innerWidth < 768) - } - handleResize() - window.addEventListener('resize', handleResize) - return () => window.removeEventListener('resize', handleResize) - }, []) - - return compact -} - -export const CalendarView = ({ - tasks, - projects, - selectedId, - selectedType, - onUpdateTask, - onTaskClick, - onAddTaskWithDate, - onToggleComplete, - // Selection props - isSelectionMode = false, - selectedIds, - onToggleSelect -}: CalendarViewProps): React.JSX.Element => { - const [currentMonth, setCurrentMonth] = useState(startOfDay(new Date())) - const [selectedDate, setSelectedDate] = useState(null) - const [focusedDate, setFocusedDate] = useState(null) - const [isDayDetailOpen, setIsDayDetailOpen] = useState(false) - const [showCompleted, setShowCompleted] = useState(false) - const [projectFilter, setProjectFilter] = useState(null) - - const isCompact = useIsCompact() - - const calendarDays: CalendarDay[] = useMemo(() => getCalendarDays(currentMonth), [currentMonth]) - - const visibleStart = useMemo( - () => calendarDays[0]?.date || startOfDay(currentMonth), - [calendarDays, currentMonth] - ) - const visibleEnd = useMemo( - () => calendarDays[calendarDays.length - 1]?.date || endOfWeek(currentMonth), - [calendarDays, currentMonth] - ) - - // Generate occurrences for repeating tasks within visible range - const expandRepeatingTasks = useCallback( - (taskList: Task[], rangeStart: Date, rangeEnd: Date): Task[] => { - const expanded: Task[] = [] - - taskList.forEach((task) => { - if (!task.dueDate) return - - // For non-repeating tasks, just check if in range - if (!task.isRepeating || !task.repeatConfig) { - const taskDate = startOfDay(task.dueDate) - if ( - taskDate.getTime() >= rangeStart.getTime() && - taskDate.getTime() <= rangeEnd.getTime() - ) { - expanded.push(task) - } - return - } - - // For repeating tasks, generate occurrences within the range - let currentDate = startOfDay(task.dueDate) - let occurrenceCount = 0 - const maxOccurrences = 50 // Safety limit - - while (occurrenceCount < maxOccurrences) { - // If current date is past the range end, stop - if (isAfter(currentDate, rangeEnd)) break - - // If current date is within range, add an occurrence - if (!isBefore(currentDate, rangeStart) && !isAfter(currentDate, rangeEnd)) { - const occurrence: Task = { - ...task, - // Create unique ID for each occurrence to avoid key conflicts - id: isSameDay(currentDate, task.dueDate) - ? task.id - : `${task.id}-occ-${formatDateKey(currentDate)}`, - dueDate: currentDate - } - expanded.push(occurrence) - } - - // Calculate next occurrence - const next = calculateNextOccurrence(currentDate, task.repeatConfig) - if (!next) break - - currentDate = next - occurrenceCount++ - } - }) - - return expanded - }, - [] - ) - - const visibleTasks = useMemo(() => { - const rangeStart = startOfDay(visibleStart) - const rangeEnd = startOfDay(visibleEnd) - - // First filter by project and completed status - const filteredTasks = tasks.filter((task) => { - // Apply project filter (only in All Tasks view) - if (selectedType === 'view' && selectedId === 'all' && projectFilter) { - if (task.projectId !== projectFilter) return false - } - - if (!showCompleted && isTaskCompleted(task, projects)) { - return false - } - return true - }) - - // Then expand repeating tasks - return expandRepeatingTasks(filteredTasks, rangeStart, rangeEnd) - }, [ - tasks, - visibleStart, - visibleEnd, - showCompleted, - projects, - selectedType, - selectedId, - projectFilter, - expandRepeatingTasks - ]) - - const tasksByDate = useMemo( - () => groupTasksByCalendarDate(visibleTasks, startOfDay(visibleStart), startOfDay(visibleEnd)), - [visibleTasks, visibleStart, visibleEnd] - ) - - const goToPreviousMonth = useCallback(() => { - setCurrentMonth((prev) => subMonths(prev, 1)) - }, []) - - const goToNextMonth = useCallback(() => { - setCurrentMonth((prev) => addMonths(prev, 1)) - }, []) - - const goToToday = useCallback(() => { - const today = startOfDay(new Date()) - setCurrentMonth(today) - setSelectedDate(today) - }, []) - - const handleOpenDay = useCallback((date: Date) => { - setSelectedDate(date) - setIsDayDetailOpen(true) - }, []) - - const handleAddTask = useCallback( - (date: Date) => { - onAddTaskWithDate(startOfDay(date)) - }, - [onAddTaskWithDate] - ) - // Drag-and-drop (rescheduling and project moves) is handled by the shared DragProvider. - - const handleKeyDown = (e: React.KeyboardEvent): void => { - // Skip if in an input/textarea - const target = e.target as HTMLElement - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return - - switch (e.key) { - case 'ArrowLeft': - e.preventDefault() - if (focusedDate) { - // Navigate to previous day - setFocusedDate(addDays(focusedDate, -1)) - } else { - goToPreviousMonth() - } - break - case 'ArrowRight': - e.preventDefault() - if (focusedDate) { - // Navigate to next day - setFocusedDate(addDays(focusedDate, 1)) - } else { - goToNextMonth() - } - break - case 'ArrowUp': - e.preventDefault() - if (focusedDate) { - // Navigate to previous week - setFocusedDate(addWeeks(focusedDate, -1)) - } - break - case 'ArrowDown': - e.preventDefault() - if (focusedDate) { - // Navigate to next week - setFocusedDate(addWeeks(focusedDate, 1)) - } - break - case 'Home': - e.preventDefault() - setFocusedDate(startOfMonth(currentMonth)) - break - case 'End': - e.preventDefault() - setFocusedDate(endOfMonth(currentMonth)) - break - case 'Enter': - e.preventDefault() - if (focusedDate) { - handleOpenDay(focusedDate) - } - break - case ' ': - e.preventDefault() - if (focusedDate) { - handleAddTask(focusedDate) - } - break - case 'Escape': - e.preventDefault() - setFocusedDate(null) - break - case 't': - case 'T': - e.preventDefault() - goToToday() - break - default: - break - } - } - - const selectedDateTasks = useMemo(() => { - if (!selectedDate) return [] - const key = formatDateKey(selectedDate) - return tasksByDate.get(key) || [] - }, [selectedDate, tasksByDate]) - - // Show project filter only in All Tasks view - const showProjectFilter = selectedType === 'view' && selectedId === 'all' - - return ( -
- - - - {})} - onAddTask={handleAddTask} - /> - - - setIsDayDetailOpen(false)} - onTaskClick={onTaskClick ?? (() => {})} - onToggleComplete={onToggleComplete} - onAddTask={handleAddTask} - // Selection props - isSelectionMode={isSelectionMode} - selectedIds={selectedIds} - onToggleSelect={onToggleSelect} - /> -
- ) -} - -export default CalendarView diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/day-cell.tsx b/apps/desktop/src/renderer/src/components/tasks/calendar/day-cell.tsx deleted file mode 100644 index 9d92a4977..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/day-cell.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React, { useMemo } from 'react' -import { useDraggable, useDroppable } from '@dnd-kit/core' - -import { cn } from '@/lib/utils' -import { CalendarTaskItem } from './calendar-task-item' -import { formatDateKey, type CalendarDay } from '@/lib/task-utils' -import type { Task } from '@/data/sample-tasks' - -interface DayCellProps { - day: CalendarDay - tasks: Task[] - allTasks?: Task[] - maxVisible?: number - isSelected?: boolean - isFocused?: boolean - isCompact?: boolean - onOpenDay: (date: Date) => void - onTaskClick: (taskId: string) => void - onAddTask: (date: Date) => void -} - -const DraggableCalendarTask = ({ - task, - children -}: { - task: Task - children: React.ReactNode -}): React.JSX.Element => { - const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ - id: task.id, - data: { - type: 'calendar-task', - task, - sourceType: 'calendar' - } - }) - - const style = useMemo(() => { - if (!transform) return undefined - return { - transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` - } - }, [transform]) - - return ( -
- {children} -
- ) -} - -export const DayCell = ({ - day, - tasks, - allTasks = [], - maxVisible = 3, - isSelected = false, - isFocused = false, - isCompact = false, - onOpenDay, - onTaskClick, - onAddTask -}: DayCellProps): React.JSX.Element => { - const { setNodeRef, isOver } = useDroppable({ - id: formatDateKey(day.date), - data: { type: 'date', date: day.date } - }) - - const visibleTasks = tasks.slice(0, maxVisible) - const overflowCount = Math.max(tasks.length - maxVisible, 0) - - const handleCellClick = (e: React.MouseEvent): void => { - const target = e.target as HTMLElement - if (target.closest('[data-task-item]')) return - onAddTask(day.date) - } - - const handleDayKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === 'Enter') { - e.preventDefault() - onOpenDay(day.date) - } - if (e.key === ' ' || e.key === 'Spacebar') { - e.preventDefault() - onAddTask(day.date) - } - } - - return ( -
- {/* Day number */} -
- {day.isToday ? ( - <> - - {day.date.getDate()} - - - Today - - - ) : ( - - {day.date.getDate()} - - )} -
- - {/* Tasks */} -
- {visibleTasks.map((task) => ( - -
- onTaskClick(task.id)} - /> -
-
- ))} -
- - {/* Overflow */} - {overflowCount > 0 && ( - - )} -
- ) -} - -export default DayCell diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/day-detail-popover.tsx b/apps/desktop/src/renderer/src/components/tasks/calendar/day-detail-popover.tsx deleted file mode 100644 index ade245c1a..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/day-detail-popover.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import React, { useMemo, useState } from 'react' -import { X, ChevronDown } from '@/lib/icons' - -import { Dialog, DialogContent } from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { SelectionCheckbox } from '@/components/tasks/bulk-actions' -import { SubtaskBadge } from '@/components/tasks/subtask-badge' -import { cn } from '@/lib/utils' -import { formatDayName } from '@/lib/task-utils' -import { getSubtasks, calculateProgress } from '@/lib/subtask-utils' -import { priorityConfig, type Task } from '@/data/sample-tasks' - -interface DayDetailPopoverProps { - date: Date | null - tasks: Task[] - allTasks?: Task[] - isOpen: boolean - onClose: () => void - onTaskClick: (taskId: string) => void - onToggleComplete: (taskId: string) => void - onAddTask: (date: Date) => void - // Selection props - isSelectionMode?: boolean - selectedIds?: Set - onToggleSelect?: (taskId: string) => void -} - -const sortTasks = (tasks: Task[]): Task[] => { - return [...tasks].sort((a, b) => { - if (a.dueTime && b.dueTime && a.dueTime !== b.dueTime) { - return a.dueTime.localeCompare(b.dueTime) - } - if (a.dueTime && !b.dueTime) return -1 - if (!a.dueTime && b.dueTime) return 1 - - const pa = priorityConfig[a.priority].order - const pb = priorityConfig[b.priority].order - if (pa !== pb) return pa - pb - - return a.title.localeCompare(b.title) - }) -} - -export const DayDetailPopover = ({ - date, - tasks, - allTasks = [], - isOpen, - onClose, - onTaskClick, - onToggleComplete, - onAddTask, - // Selection props - isSelectionMode = false, - selectedIds, - onToggleSelect -}: DayDetailPopoverProps): React.JSX.Element | null => { - const [expandedTasks, setExpandedTasks] = useState>(new Set()) - const sortedTasks = useMemo(() => sortTasks(tasks), [tasks]) - const title = date - ? `${formatDayName(date)}, ${date.toLocaleDateString('en-US', { - month: 'long', - day: 'numeric' - })}` - : '' - - const handleAdd = (): void => { - if (!date) return - onAddTask(date) - onClose() - } - - const handleTaskClick = (taskId: string): void => { - // In selection mode, clicking toggles selection - if (isSelectionMode && onToggleSelect) { - onToggleSelect(taskId) - return - } - onTaskClick(taskId) - } - - const handleSelectionCheckboxChange = (taskId: string): void => { - onToggleSelect?.(taskId) - } - - const toggleExpanded = (taskId: string): void => { - setExpandedTasks((prev) => { - const next = new Set(prev) - if (next.has(taskId)) { - next.delete(taskId) - } else { - next.add(taskId) - } - return next - }) - } - - return ( - !open && onClose()}> - -
-
-

{title}

-

- {tasks.length} task{tasks.length !== 1 ? 's' : ''} -

-
- -
- -
- {sortedTasks.length === 0 && ( -

No tasks for this day.

- )} - -
- {sortedTasks.map((task) => { - const isCheckedForSelection = selectedIds?.has(task.id) ?? false - const taskSubtasks = allTasks.length > 0 ? getSubtasks(task.id, allTasks) : [] - const taskHasSubtasks = taskSubtasks.length > 0 - const subtaskProgress = calculateProgress(taskSubtasks) - const isExpanded = expandedTasks.has(task.id) - - return ( -
- {/* Parent task row */} - - ) : ( - - )} - - {/* Selection checkbox - visible only in selection mode */} - {onToggleSelect && isSelectionMode && ( -
e.stopPropagation()}> - handleSelectionCheckboxChange(task.id)} - aria-label={`Select ${task.title}`} - /> -
- )} - - {/* Task completion checkbox */} -
e.stopPropagation()}> - onToggleComplete(task.id)} - aria-label="Toggle complete" - /> -
-
- - {task.dueTime || '—'} - - {task.priority !== 'none' && ( -
- - - {/* Subtask badge (if has subtasks and not expanded) */} - {taskHasSubtasks && !isExpanded && ( -
- -
- )} - - {/* Expanded subtasks list */} - {taskHasSubtasks && isExpanded && ( -
- {taskSubtasks.map((subtask, index) => { - const isLastSubtask = index === taskSubtasks.length - 1 - - return ( - - ) - })} -
- )} -
- ) - })} -
-
- -
- -
-
-
- ) -} - -export default DayDetailPopover diff --git a/apps/desktop/src/renderer/src/components/tasks/calendar/index.ts b/apps/desktop/src/renderer/src/components/tasks/calendar/index.ts deleted file mode 100644 index 845ce2a1a..000000000 --- a/apps/desktop/src/renderer/src/components/tasks/calendar/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './calendar-view' -export * from './calendar-header' -export * from './calendar-grid' -export * from './day-cell' -export * from './calendar-task-item' -export * from './day-detail-popover' -export * from './calendar-drag-overlay' diff --git a/apps/desktop/src/renderer/src/data/tasks-data.ts b/apps/desktop/src/renderer/src/data/tasks-data.ts index 3e7b95ced..ee152108c 100644 --- a/apps/desktop/src/renderer/src/data/tasks-data.ts +++ b/apps/desktop/src/renderer/src/data/tasks-data.ts @@ -46,12 +46,11 @@ export interface TaskView { // VIEW MODE TYPES // ============================================================================ -export type ViewMode = 'list' | 'kanban' | 'calendar' +export type ViewMode = 'list' | 'kanban' export const viewModes: { id: ViewMode; label: string }[] = [ { id: 'list', label: 'List' }, - { id: 'kanban', label: 'Kanban' }, - { id: 'calendar', label: 'Calendar' } + { id: 'kanban', label: 'Kanban' } ] export const LIST_ONLY_VIEWS = ['today', 'completed'] diff --git a/apps/desktop/src/renderer/src/lib/task-utils.test.ts b/apps/desktop/src/renderer/src/lib/task-utils.test.ts index f6221974a..da708d7b3 100644 --- a/apps/desktop/src/renderer/src/lib/task-utils.test.ts +++ b/apps/desktop/src/renderer/src/lib/task-utils.test.ts @@ -52,7 +52,6 @@ import { getDefaultDoneStatus, // Task Sorting (T077-T078) sortTasksByPriorityAndDate, - sortTasksForDay, sortTasksByTimeAndPriority, sortOverdueTasks, sortTasksAdvanced, @@ -62,8 +61,6 @@ import { // Calendar Helpers (T081) formatDateKey, parseDateKey, - getCalendarDays, - groupTasksByCalendarDate, // Task Filtering - Basic (T082) filterBySearch, filterByProjects, @@ -1422,99 +1419,6 @@ describe('Task Utils', () => { }) }) - describe('sortTasksForDay', () => { - it('should put timed tasks first, then untimed', () => { - const tasks = [ - createMockTask({ id: 't1', dueTime: null }), - createMockTask({ id: 't2', dueTime: '14:30' }), - createMockTask({ id: 't3', dueTime: null }) - ] - - const sorted = sortTasksForDay(tasks) - - expect(sorted[0].id).toBe('t2') - }) - - it('should sort timed tasks chronologically', () => { - const tasks = [ - createMockTask({ id: 't1', dueTime: '14:30' }), - createMockTask({ id: 't2', dueTime: '09:00' }), - createMockTask({ id: 't3', dueTime: '18:00' }), - createMockTask({ id: 't4', dueTime: '12:00' }) - ] - - const sorted = sortTasksForDay(tasks) - - expect(sorted.map((t: Task) => t.id)).toEqual(['t2', 't4', 't1', 't3']) - }) - - it('should sort untimed tasks by priority', () => { - const tasks = [ - createMockTask({ id: 't1', dueTime: null, priority: 'low' }), - createMockTask({ id: 't2', dueTime: null, priority: 'urgent' }), - createMockTask({ id: 't3', dueTime: null, priority: 'high' }) - ] - - const sorted = sortTasksForDay(tasks) - - expect(sorted.map((t: Task) => t.id)).toEqual(['t2', 't3', 't1']) - }) - - it('should sort by title when priority is equal (untimed)', () => { - const tasks = [ - createMockTask({ id: 't1', title: 'Zebra task', dueTime: null, priority: 'medium' }), - createMockTask({ id: 't2', title: 'Apple task', dueTime: null, priority: 'medium' }), - createMockTask({ id: 't3', title: 'Mango task', dueTime: null, priority: 'medium' }) - ] - - const sorted = sortTasksForDay(tasks) - - expect(sorted.map((t: Task) => t.id)).toEqual(['t2', 't3', 't1']) - }) - - it('should handle mixed timed and untimed tasks with varying priorities', () => { - const tasks = [ - createMockTask({ id: 't1', dueTime: null, priority: 'urgent' }), - createMockTask({ id: 't2', dueTime: '14:00', priority: 'low' }), - createMockTask({ id: 't3', dueTime: null, priority: 'low' }), - createMockTask({ id: 't4', dueTime: '09:00', priority: 'high' }) - ] - - const sorted = sortTasksForDay(tasks) - - expect(sorted.map((t: Task) => t.id)).toEqual(['t4', 't2', 't1', 't3']) - }) - - it('should handle empty array', () => { - const sorted = sortTasksForDay([]) - expect(sorted).toEqual([]) - }) - - it('should handle all tasks with same time (sort by priority)', () => { - const tasks = [ - createMockTask({ id: 't1', dueTime: '10:00', priority: 'low' }), - createMockTask({ id: 't2', dueTime: '10:00', priority: 'high' }), - createMockTask({ id: 't3', dueTime: '10:00', priority: 'medium' }) - ] - - const sorted = sortTasksForDay(tasks) - - expect(sorted.map((t: Task) => t.id)).toEqual(['t2', 't3', 't1']) - }) - - it('should handle edge times (midnight and end of day)', () => { - const tasks = [ - createMockTask({ id: 't1', dueTime: '23:59' }), - createMockTask({ id: 't2', dueTime: '00:00' }), - createMockTask({ id: 't3', dueTime: '12:00' }) - ] - - const sorted = sortTasksForDay(tasks) - - expect(sorted.map((t: Task) => t.id)).toEqual(['t2', 't3', 't1']) - }) - }) - describe('sortTasksByTimeAndPriority', () => { it('should put tasks with time before tasks without time', () => { const tasks = [ @@ -2301,191 +2205,6 @@ describe('Task Utils', () => { expect(date.getDate()).toBe(29) }) }) - - describe('getCalendarDays', () => { - it('should return calendar days for a month', () => { - const january2026 = new Date('2026-01-01') - const days = getCalendarDays(january2026) - - expect(days.length).toBeGreaterThan(0) - expect(days.length % 7).toBe(0) - }) - - it('should mark days in current month correctly', () => { - const january2026 = new Date('2026-01-15') - const days = getCalendarDays(january2026) - - const jan15 = days.find( - (d: { date: Date; isCurrentMonth: boolean }) => - d.date.getMonth() === 0 && d.date.getDate() === 15 && d.date.getFullYear() === 2026 - ) - expect(jan15?.isCurrentMonth).toBe(true) - }) - - it('should mark overflow days from previous month', () => { - const january2026 = new Date('2026-01-01') - const days = getCalendarDays(january2026, 0) - - expect(days[0].date.getDay()).toBe(0) - expect(days[0].isCurrentMonth).toBe(false) - }) - - it('should mark overflow days from next month', () => { - const january2026 = new Date('2026-01-31') - const days = getCalendarDays(january2026, 0) - - const lastDay = days[days.length - 1] - expect(lastDay.date.getDay()).toBe(6) - }) - - it('should mark today correctly', () => { - const currentMonth = new Date('2026-01-14') - const days = getCalendarDays(currentMonth) - - const today = days.find((d: { isToday: boolean }) => d.isToday) - expect(today).toBeDefined() - expect(today?.date.getDate()).toBe(14) - }) - - it('should mark weekends correctly', () => { - const january2026 = new Date('2026-01-01') - const days = getCalendarDays(january2026) - - const weekends = days.filter((d: { isWeekend: boolean }) => d.isWeekend) - weekends.forEach((d: { date: Date }) => { - expect([0, 6]).toContain(d.date.getDay()) - }) - }) - - it('should respect weekStartsOn = 0 (Sunday)', () => { - const january2026 = new Date('2026-01-01') - const days = getCalendarDays(january2026, 0) - - expect(days[0].date.getDay()).toBe(0) - }) - - it('should respect weekStartsOn = 1 (Monday)', () => { - const january2026 = new Date('2026-01-01') - const days = getCalendarDays(january2026, 1) - - expect(days[0].date.getDay()).toBe(1) - }) - - it('should include all days of the month', () => { - const january2026 = new Date('2026-01-15') - const days = getCalendarDays(january2026) - - for (let day = 1; day <= 31; day++) { - const found = days.some( - (d: { date: Date }) => - d.date.getMonth() === 0 && d.date.getDate() === day && d.date.getFullYear() === 2026 - ) - expect(found).toBe(true) - } - }) - - it('should handle February with 28 days', () => { - const february2026 = new Date('2026-02-15') - const days = getCalendarDays(february2026) - - for (let day = 1; day <= 28; day++) { - const found = days.some( - (d: { date: Date }) => - d.date.getMonth() === 1 && d.date.getDate() === day && d.date.getFullYear() === 2026 - ) - expect(found).toBe(true) - } - }) - }) - - describe('groupTasksByCalendarDate', () => { - it('should group tasks by date key within range', () => { - const tasks = [ - createMockTask({ id: 't1', dueDate: new Date('2026-01-14') }), - createMockTask({ id: 't2', dueDate: new Date('2026-01-15') }), - createMockTask({ id: 't3', dueDate: new Date('2026-01-14') }) - ] - - const start = new Date('2026-01-10') - const end = new Date('2026-01-20') - const grouped = groupTasksByCalendarDate(tasks, start, end) - - expect(grouped.get('2026-01-14')).toHaveLength(2) - expect(grouped.get('2026-01-15')).toHaveLength(1) - }) - - it('should exclude tasks outside date range', () => { - const tasks = [ - createMockTask({ id: 't1', dueDate: new Date('2026-01-05') }), - createMockTask({ id: 't2', dueDate: new Date('2026-01-15') }), - createMockTask({ id: 't3', dueDate: new Date('2026-01-25') }) - ] - - const start = new Date('2026-01-10') - const end = new Date('2026-01-20') - const grouped = groupTasksByCalendarDate(tasks, start, end) - - expect(grouped.get('2026-01-05')).toBeUndefined() - expect(grouped.get('2026-01-15')).toHaveLength(1) - expect(grouped.get('2026-01-25')).toBeUndefined() - }) - - it('should skip tasks without due date', () => { - const tasks = [ - createMockTask({ id: 't1', dueDate: null }), - createMockTask({ id: 't2', dueDate: new Date('2026-01-15') }) - ] - - const start = new Date('2026-01-10') - const end = new Date('2026-01-20') - const grouped = groupTasksByCalendarDate(tasks, start, end) - - let totalTasks = 0 - grouped.forEach((tasksForDay: Task[]) => { - totalTasks += tasksForDay.length - }) - expect(totalTasks).toBe(1) - }) - - it('should sort tasks within each day', () => { - const date = new Date('2026-01-15') - const tasks = [ - createMockTask({ id: 't1', dueDate: date, dueTime: '15:00' }), - createMockTask({ id: 't2', dueDate: date, dueTime: '09:00' }), - createMockTask({ id: 't3', dueDate: date, dueTime: '12:00' }) - ] - - const start = new Date('2026-01-10') - const end = new Date('2026-01-20') - const grouped = groupTasksByCalendarDate(tasks, start, end) - - const dayTasks = grouped.get('2026-01-15')! - expect(dayTasks.map((t: Task) => t.id)).toEqual(['t2', 't3', 't1']) - }) - - it('should include boundary dates (inclusive)', () => { - // Use local date constructors to avoid timezone issues - const tasks = [ - createMockTask({ id: 't1', dueDate: new Date(2026, 0, 10, 12) }), - createMockTask({ id: 't2', dueDate: new Date(2026, 0, 20, 12) }) - ] - - const start = startOfDay(new Date(2026, 0, 10)) - const end = endOfDay(new Date(2026, 0, 20)) - const grouped = groupTasksByCalendarDate(tasks, start, end) - - expect(grouped.get('2026-01-10')).toHaveLength(1) - expect(grouped.get('2026-01-20')).toHaveLength(1) - }) - - it('should return empty map for empty tasks', () => { - const start = new Date('2026-01-10') - const end = new Date('2026-01-20') - const grouped = groupTasksByCalendarDate([], start, end) - - expect(grouped.size).toBe(0) - }) - }) }) // ============================================================================ diff --git a/apps/desktop/src/renderer/src/lib/task-utils.ts b/apps/desktop/src/renderer/src/lib/task-utils.ts index 392640aba..24dd93174 100644 --- a/apps/desktop/src/renderer/src/lib/task-utils.ts +++ b/apps/desktop/src/renderer/src/lib/task-utils.ts @@ -438,17 +438,6 @@ export const groupTasksByStatus = ( // TASK GROUPING - BY COMPLETION DATE // ============================================================================ -// ============================================================================ -// CALENDAR HELPERS -// ============================================================================ - -export interface CalendarDay { - date: Date - isCurrentMonth: boolean - isToday: boolean - isWeekend: boolean -} - /** * Format date to yyyy-MM-dd key */ @@ -459,105 +448,6 @@ export const formatDateKey = (date: Date): string => { return `${year}-${month}-${day}` } -/** - * Build visible calendar days for a month (includes overflow days) - */ -export const getCalendarDays = (month: Date, weekStartsOn: 0 | 1 = 0): CalendarDay[] => { - const start = startOfWeek(startOfMonth(month), weekStartsOn) - const end = endOfWeek(endOfMonth(month), weekStartsOn) - - const days: CalendarDay[] = [] - let current = start - - while (current <= end) { - const dayDate = new Date(current) - days.push({ - date: dayDate, - isCurrentMonth: isSameMonth(dayDate, month), - isToday: isSameDay(dayDate, startOfDay(new Date())), - isWeekend: [0, 6].includes(dayDate.getDay()) - }) - current = addDays(current, 1) - } - - return days -} - -/** - * Convert HH:MM to minutes since midnight - */ -const timeToMinutes = (time: string | null): number | null => { - if (!time) return null - const [hoursStr, minutesStr] = time.split(':') - const hours = Number(hoursStr) - const minutes = Number(minutesStr) - if (Number.isNaN(hours) || Number.isNaN(minutes)) return null - return hours * 60 + minutes -} - -/** - * Sort tasks for a single day: - * 1) Timed tasks first (chronological) - * 2) Untimed tasks next (by priority) - * 3) Tie-breaker by title - */ -export const sortTasksForDay = (tasks: Task[]): Task[] => { - return [...tasks].sort((a, b) => { - const aMinutes = timeToMinutes(a.dueTime) - const bMinutes = timeToMinutes(b.dueTime) - - const aHasTime = aMinutes !== null - const bHasTime = bMinutes !== null - - // Timed before untimed - if (aHasTime && !bHasTime) return -1 - if (!aHasTime && bHasTime) return 1 - - // Both timed: chronological - if (aHasTime && bHasTime && aMinutes !== bMinutes) { - return aMinutes - bMinutes - } - - // Priority (lower order is higher priority) - const pa = priorityConfig[a.priority].order - const pb = priorityConfig[b.priority].order - if (pa !== pb) return pa - pb - - // Title - return a.title.localeCompare(b.title) - }) -} - -/** - * Group tasks by date key within a visible range - */ -export const groupTasksByCalendarDate = ( - tasks: Task[], - visibleStart: Date, - visibleEnd: Date -): Map => { - const map = new Map() - - tasks.forEach((task) => { - if (!task.dueDate) return - const taskDate = startOfDay(task.dueDate) - if (!isWithinInterval(taskDate, { start: visibleStart, end: visibleEnd })) return - - const key = formatDateKey(taskDate) - if (!map.has(key)) { - map.set(key, []) - } - map.get(key)!.push(task) - }) - - // Sort each bucket for consistent display - map.forEach((value, key) => { - map.set(key, sortTasksForDay(value)) - }) - - return map -} - // ============================================================================ // TASK FILTERING // ============================================================================ diff --git a/apps/desktop/src/renderer/src/pages/tasks.tsx b/apps/desktop/src/renderer/src/pages/tasks.tsx index 7c4e14b03..dae2001c4 100644 --- a/apps/desktop/src/renderer/src/pages/tasks.tsx +++ b/apps/desktop/src/renderer/src/pages/tasks.tsx @@ -7,7 +7,6 @@ import { ProjectsTabContent } from '@/components/tasks/projects/projects-tab-con import { ProjectSelector } from '@/components/tasks/projects/project-selector' import { AddTaskModal } from '@/components/tasks/add-task-modal' import { ProjectModal } from '@/components/tasks/project-modal' -import { CalendarView } from '@/components/tasks/calendar' import { KanbanBoard } from '@/components/tasks/kanban' import { QuickAddInput } from '@/components/tasks/quick-add-input' import { TaskDetailDrawer } from '@/components/tasks/task-detail-drawer' @@ -281,7 +280,7 @@ export const TasksPage = ({ if (activeInternalTab === 'today') { return ['list'] } - return ['list', 'kanban', 'calendar'] + return ['list', 'kanban'] }, [activeInternalTab]) // Reset to list view if current view becomes unavailable @@ -719,21 +718,6 @@ export const TasksPage = ({ [undoable] ) - const handleAddTaskWithDate = useCallback( - (date: Date): void => { - const projectId = resolveModalDefaultProject( - { selectedType, selectedProject }, - taskPrefs.defaultProjectId, - selectedProjectId - ) - setAddTaskPrefillProjectId(projectId) - setAddTaskPrefillDueDate(date) - setAddTaskPrefillTitle('') - setIsAddTaskModalOpen(true) - }, - [selectedProject, selectedType, selectedProjectId, taskPrefs.defaultProjectId] - ) - // ========== BULK ACTION HANDLERS ========== const handleBulkChangePriority = useCallback( @@ -992,34 +976,6 @@ export const TasksPage = ({ )} - {availableViews.includes('calendar') && ( - - )}
)}
@@ -1144,25 +1100,6 @@ export const TasksPage = ({ />
)} - - {/* Calendar View - All Tab */} - {activeInternalTab === 'all' && activeView === 'calendar' && ( -
- -
- )} {/* Task Detail Drawer */} From 8efe6eabaa6c7a452cc1c199cca3da55228fdd18 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Fri, 20 Mar 2026 22:26:10 +0300 Subject: [PATCH 15/80] chore: remove stale items feature Stale items UI (age indicators, thresholds, nudge messages) was unused. Remove components, utilities, and 346 lines of tests. --- .../src/components/stale/age-indicator.tsx | 36 -- .../renderer/src/components/stale/index.ts | 4 - .../components/stale/stale-action-footer.tsx | 44 --- .../src/components/stale/stale-item-row.tsx | 180 --------- .../src/components/stale/stale-section.tsx | 125 ------- .../src/renderer/src/lib/stale-utils.test.ts | 346 ------------------ .../src/renderer/src/lib/stale-utils.ts | 99 ----- 7 files changed, 834 deletions(-) delete mode 100644 apps/desktop/src/renderer/src/components/stale/age-indicator.tsx delete mode 100644 apps/desktop/src/renderer/src/components/stale/index.ts delete mode 100644 apps/desktop/src/renderer/src/components/stale/stale-action-footer.tsx delete mode 100644 apps/desktop/src/renderer/src/components/stale/stale-item-row.tsx delete mode 100644 apps/desktop/src/renderer/src/components/stale/stale-section.tsx delete mode 100644 apps/desktop/src/renderer/src/lib/stale-utils.test.ts delete mode 100644 apps/desktop/src/renderer/src/lib/stale-utils.ts diff --git a/apps/desktop/src/renderer/src/components/stale/age-indicator.tsx b/apps/desktop/src/renderer/src/components/stale/age-indicator.tsx deleted file mode 100644 index a1cee86cb..000000000 --- a/apps/desktop/src/renderer/src/components/stale/age-indicator.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { cn } from '@/lib/utils' -import { getDaysInInbox, formatAge } from '@/lib/stale-utils' -import type { InboxItem, InboxItemListItem } from '@/types' - -interface AgeIndicatorProps { - item: InboxItem | InboxItemListItem - className?: string -} - -/** - * Displays how long an item has been in the inbox - * Uses subtle amber coloring that escalates slightly with age - */ -export const AgeIndicator = ({ item, className }: AgeIndicatorProps): React.JSX.Element => { - const days = getDaysInInbox(item) - const ageText = formatAge(days) - - // Subtle color escalation based on age - const getIndicatorColor = (): string => { - if (days >= 30) return 'text-amber-600 dark:text-amber-400' - if (days >= 14) return 'text-amber-500 dark:text-amber-500' - return 'text-amber-500/70 dark:text-amber-500/70' - } - - return ( -
- - {ageText} -
- ) -} diff --git a/apps/desktop/src/renderer/src/components/stale/index.ts b/apps/desktop/src/renderer/src/components/stale/index.ts deleted file mode 100644 index df39e787c..000000000 --- a/apps/desktop/src/renderer/src/components/stale/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { StaleSection, StaleSectionHeader } from './stale-section' -export { StaleItemRow } from './stale-item-row' -export { StaleActionFooter } from './stale-action-footer' -export { AgeIndicator } from './age-indicator' diff --git a/apps/desktop/src/renderer/src/components/stale/stale-action-footer.tsx b/apps/desktop/src/renderer/src/components/stale/stale-action-footer.tsx deleted file mode 100644 index cee62e14f..000000000 --- a/apps/desktop/src/renderer/src/components/stale/stale-action-footer.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Button } from '@/components/ui/button' -import { getNudgeMessage } from '@/lib/stale-utils' - -interface StaleActionFooterProps { - itemCount: number - onFileAllToUnsorted: () => void - onReviewOneByOne: () => void -} - -/** - * Footer component for the stale section with nudge message and action buttons - */ -export const StaleActionFooter = ({ - itemCount, - onFileAllToUnsorted, - onReviewOneByOne -}: StaleActionFooterProps): React.JSX.Element => { - const nudgeMessage = getNudgeMessage(itemCount) - - return ( -
-

{nudgeMessage}

-
- - or - -
-
- ) -} diff --git a/apps/desktop/src/renderer/src/components/stale/stale-item-row.tsx b/apps/desktop/src/renderer/src/components/stale/stale-item-row.tsx deleted file mode 100644 index 6776c98ed..000000000 --- a/apps/desktop/src/renderer/src/components/stale/stale-item-row.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { Link, FileText, Image, Mic, Scissors, FileIcon, Share2 } from '@/lib/icons' - -import { Checkbox } from '@/components/ui/checkbox' -import { QuickActions } from '@/components/quick-actions' -import { AgeIndicator } from '@/components/stale/age-indicator' -import { formatTimestamp } from '@/lib/inbox-utils' -import { cn } from '@/lib/utils' -import { type DisplayDensity, DENSITY_CONFIG } from '@/hooks/use-display-density' -import type { InboxItemListItem, InboxItemType } from '@/types' - -// Type alias for convenience (backend type) -type InboxItem = InboxItemListItem - -// Icon component based on item type -const TypeIcon = ({ type }: { type: InboxItemType }): React.JSX.Element => { - const iconClass = 'size-4 text-[var(--muted-foreground)]' - - switch (type) { - case 'link': - return - case 'note': - return
{visibleProjects.map((project) => { @@ -72,8 +72,8 @@ export function ProjectPanel({ /> {project.name} diff --git a/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-panel.tsx b/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-panel.tsx index ac29050cc..b4825021b 100644 --- a/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-panel.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-panel.tsx @@ -35,7 +35,7 @@ export function StatusPanel({
- Status + Status
{statuses.map((status) => { @@ -54,8 +54,8 @@ export function StatusPanel({ {status.name} diff --git a/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-project-picker-panel.tsx b/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-project-picker-panel.tsx index 38992d1c3..0a9957926 100644 --- a/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-project-picker-panel.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/filters/filter-panels/status-project-picker-panel.tsx @@ -30,10 +30,10 @@ export function StatusProjectPickerPanel({ > - Status + Status
- Pick a project + Pick a project
{visibleProjects.map((project) => ( @@ -47,8 +47,8 @@ export function StatusProjectPickerPanel({ className="shrink-0 rounded-[3px] size-2.5" style={{ backgroundColor: project.color }} /> - {project.name} - + {project.name} + ))}
diff --git a/apps/desktop/src/renderer/src/components/tasks/filters/group-by-dropdown.tsx b/apps/desktop/src/renderer/src/components/tasks/filters/group-by-dropdown.tsx index 01cd4a0fe..162bbd4f7 100644 --- a/apps/desktop/src/renderer/src/components/tasks/filters/group-by-dropdown.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/filters/group-by-dropdown.tsx @@ -69,13 +69,13 @@ export const GroupByDropdown = ({ className={cn( 'flex items-center shrink-0 rounded-[5px] py-1 px-2 gap-1 border transition-colors', isOpen || isNonDefault - ? 'border-foreground/20 bg-foreground/5 text-text-primary' - : 'border-border text-text-secondary hover:bg-surface-active/50', + ? 'border-foreground/20 bg-foreground/5 text-foreground/90' + : 'border-border text-muted-foreground hover:bg-surface-active/50', className )} > - Group by + Group by @@ -84,7 +84,7 @@ export const GroupByDropdown = ({ align="end" sideOffset={8} > -
+
{VISIBLE_FIELDS.map((field) => { const isSelected = sort.field === field @@ -101,8 +101,8 @@ export const GroupByDropdown = ({ > {GROUP_FIELD_LABELS[field]} @@ -116,7 +116,7 @@ export const GroupByDropdown = ({ {/* Direction toggle */}
- + {DIRECTION_LABELS[sort.direction]}
diff --git a/apps/desktop/src/renderer/src/components/tasks/filters/more-filters-dropdown.tsx b/apps/desktop/src/renderer/src/components/tasks/filters/more-filters-dropdown.tsx index 8fda0ed46..7243f7e3f 100644 --- a/apps/desktop/src/renderer/src/components/tasks/filters/more-filters-dropdown.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/filters/more-filters-dropdown.tsx @@ -126,7 +126,7 @@ export const MoreFiltersDropdown = ({ onClick={() => setShowStatusPanel(true)} className="flex items-center py-[9px] px-4 gap-2.5 hover:bg-accent focus:outline-none transition-colors" > - + Status {selectedStatusIds.length > 0 && ( @@ -143,7 +143,7 @@ export const MoreFiltersDropdown = ({ onClick={() => setShowStatusPanel(false)} className="flex items-center py-[9px] px-4 gap-2.5 hover:bg-accent focus:outline-none transition-colors" > - + Has time set - + Recurring only setShowStatusPanel(false)} className="flex items-center py-2.5 px-4 gap-1.5 bg-surface border-b border-border" > - + Status
diff --git a/apps/desktop/src/renderer/src/components/tasks/filters/priority-filter.tsx b/apps/desktop/src/renderer/src/components/tasks/filters/priority-filter.tsx index 36374ce55..50133e9ef 100644 --- a/apps/desktop/src/renderer/src/components/tasks/filters/priority-filter.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/filters/priority-filter.tsx @@ -150,7 +150,7 @@ export const PriorityFilter = ({ className={cn( 'text-[13px] leading-4', isSelected ? 'font-medium text-foreground' : 'text-foreground', - priority === 'none' && !isSelected && 'text-text-secondary' + priority === 'none' && !isSelected && 'text-muted-foreground' )} > {display.label} diff --git a/apps/desktop/src/renderer/src/components/tasks/filters/saved-filters-section.tsx b/apps/desktop/src/renderer/src/components/tasks/filters/saved-filters-section.tsx index 3203a4da7..afd21e46f 100644 --- a/apps/desktop/src/renderer/src/components/tasks/filters/saved-filters-section.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/filters/saved-filters-section.tsx @@ -55,7 +55,7 @@ export const SavedFiltersSection = ({ placeholder={hasActiveFilters ? 'Save current filter...' : 'Set filters first'} disabled={!hasActiveFilters} aria-label="Filter name" - className="flex-1 min-w-0 bg-transparent text-[12px] leading-4 text-foreground placeholder:text-text-tertiary outline-none disabled:opacity-40" + className="flex-1 min-w-0 bg-transparent text-[13px] leading-4 text-foreground placeholder:text-muted-foreground/40 outline-none disabled:opacity-40" />
)}
diff --git a/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx b/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx index 2c51a779b..f3fd719bf 100644 --- a/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-card.tsx @@ -140,8 +140,8 @@ export const KanbanCardContent = forwardRef {task.title} diff --git a/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-column.tsx b/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-column.tsx index e2d2caf50..8a2e5a750 100644 --- a/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-column.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/kanban/kanban-column.tsx @@ -148,8 +148,8 @@ export const KanbanColumn = ({ )} {column.title} diff --git a/apps/desktop/src/renderer/src/components/tasks/parent-task-row.tsx b/apps/desktop/src/renderer/src/components/tasks/parent-task-row.tsx index 024a9c583..b10f695ea 100644 --- a/apps/desktop/src/renderer/src/components/tasks/parent-task-row.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/parent-task-row.tsx @@ -293,14 +293,14 @@ export const ParentTaskRow = ({ {task.title} diff --git a/apps/desktop/src/renderer/src/components/tasks/quick-add-input.tsx b/apps/desktop/src/renderer/src/components/tasks/quick-add-input.tsx index 394c608ca..ac184615d 100644 --- a/apps/desktop/src/renderer/src/components/tasks/quick-add-input.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/quick-add-input.tsx @@ -441,7 +441,7 @@ export const QuickAddInput = ({ aria-hidden="true" className={cn( 'pointer-events-none absolute inset-0 overflow-hidden whitespace-pre leading-[normal]', - compact ? 'text-[12px]' : 'text-sm' + compact ? 'text-[13px]' : 'text-sm' )} > {value && hasSpecialSyntax(value) && } @@ -457,12 +457,12 @@ export const QuickAddInput = ({ onKeyDown={handleKeyDown} placeholder={placeholder} className={cn( - 'relative w-full bg-transparent outline-none caret-text-primary', - compact ? 'text-[12px] leading-4' : 'text-sm', + 'relative w-full bg-transparent outline-none caret-foreground', + compact ? 'text-[13px] leading-4' : 'text-sm', value && hasSpecialSyntax(value) ? 'text-transparent selection:bg-primary/20 selection:text-transparent placeholder:text-muted-foreground/40' : isFocused - ? 'text-text-primary placeholder:text-muted-foreground/40' + ? 'text-foreground/90 placeholder:text-muted-foreground/40' : 'text-muted-foreground placeholder:text-muted-foreground/40' )} aria-label="Quick add task" diff --git a/apps/desktop/src/renderer/src/components/tasks/sortable-subtask-row.tsx b/apps/desktop/src/renderer/src/components/tasks/sortable-subtask-row.tsx index 3caa3f3e4..7f92a2367 100644 --- a/apps/desktop/src/renderer/src/components/tasks/sortable-subtask-row.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/sortable-subtask-row.tsx @@ -82,10 +82,10 @@ export const SortableSubtaskRow = ({ {/* Title */} {subtask.title} diff --git a/apps/desktop/src/renderer/src/components/tasks/subtask-row.tsx b/apps/desktop/src/renderer/src/components/tasks/subtask-row.tsx index 307f410b1..76d82a1d0 100644 --- a/apps/desktop/src/renderer/src/components/tasks/subtask-row.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/subtask-row.tsx @@ -62,10 +62,8 @@ export const SubtaskRow = ({ {subtask.title} diff --git a/apps/desktop/src/renderer/src/components/tasks/task-row.tsx b/apps/desktop/src/renderer/src/components/tasks/task-row.tsx index 6b623c51b..48186f5c4 100644 --- a/apps/desktop/src/renderer/src/components/tasks/task-row.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/task-row.tsx @@ -150,10 +150,10 @@ export const TaskRow = ({ {task.title} diff --git a/apps/desktop/src/renderer/src/components/tasks/tasks-tab-bar.tsx b/apps/desktop/src/renderer/src/components/tasks/tasks-tab-bar.tsx index 7517320ba..310765fc6 100644 --- a/apps/desktop/src/renderer/src/components/tasks/tasks-tab-bar.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/tasks-tab-bar.tsx @@ -106,7 +106,7 @@ export const TasksTabBar = ({ return (
@@ -137,15 +137,15 @@ export const TasksTabBar = ({ index > 0 && 'border-l border-border', isActive ? 'bg-foreground text-background font-medium' - : 'text-text-secondary hover:text-text-primary hover:bg-surface-active/50' + : 'text-muted-foreground hover:text-foreground/90 hover:bg-surface-active/50' )} > - {tab.label} + {tab.label} {count} @@ -164,7 +164,7 @@ export const TasksTabBar = ({ 'group/pill flex items-center whitespace-nowrap border-l border-border transition-colors', isActive ? 'saved-filter-active bg-task-star/15 text-task-star font-medium' - : 'text-text-tertiary hover:text-text-primary hover:bg-surface-active/50' + : 'text-muted-foreground/60 hover:text-foreground/90 hover:bg-surface-active/50' )} > {filtered.map((p) => { @@ -319,8 +319,8 @@ function ProjectDropdown({ /> {p.name} diff --git a/apps/desktop/src/renderer/src/components/tasks/today-task-row.tsx b/apps/desktop/src/renderer/src/components/tasks/today-task-row.tsx index 50e762d1a..d9eaaf29b 100644 --- a/apps/desktop/src/renderer/src/components/tasks/today-task-row.tsx +++ b/apps/desktop/src/renderer/src/components/tasks/today-task-row.tsx @@ -123,7 +123,9 @@ export const TodayTaskRow = ({ {/* Title */} - {task.title} + + {task.title} + {/* Repeat indicator */} {task.isRepeating && task.repeatConfig && ( diff --git a/apps/desktop/src/renderer/src/components/ui/filter-footer.tsx b/apps/desktop/src/renderer/src/components/ui/filter-footer.tsx index f0697854f..de1fb83e8 100644 --- a/apps/desktop/src/renderer/src/components/ui/filter-footer.tsx +++ b/apps/desktop/src/renderer/src/components/ui/filter-footer.tsx @@ -28,7 +28,7 @@ export function FilterFooter({ @@ -38,7 +38,7 @@ export function FilterFooter({ onClick={onApply} className="flex items-center rounded-sm py-[5px] px-3.5 gap-1 bg-foreground hover:bg-foreground/80 transition-colors" > - {applyLabel} + {applyLabel}
) diff --git a/apps/desktop/src/renderer/src/components/ui/filter-option-row.tsx b/apps/desktop/src/renderer/src/components/ui/filter-option-row.tsx index 60af0f022..1c960cab6 100644 --- a/apps/desktop/src/renderer/src/components/ui/filter-option-row.tsx +++ b/apps/desktop/src/renderer/src/components/ui/filter-option-row.tsx @@ -40,8 +40,8 @@ export function FilterOptionRow({ {icon} {label} diff --git a/apps/desktop/src/renderer/src/components/ui/filter-search-header.tsx b/apps/desktop/src/renderer/src/components/ui/filter-search-header.tsx index df09cbbe7..314461057 100644 --- a/apps/desktop/src/renderer/src/components/ui/filter-search-header.tsx +++ b/apps/desktop/src/renderer/src/components/ui/filter-search-header.tsx @@ -19,13 +19,13 @@ export function FilterSearchHeader({ return (
{leading} - + onChange(e.target.value)} placeholder={placeholder} - className="flex-1 min-w-0 bg-transparent text-[12px] text-foreground placeholder:text-text-tertiary outline-none leading-4" + className="flex-1 min-w-0 bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground/40 outline-none leading-4" onClick={(e) => e.stopPropagation()} />
diff --git a/apps/desktop/src/renderer/src/pages/tasks.tsx b/apps/desktop/src/renderer/src/pages/tasks.tsx index dae2001c4..17676c2fd 100644 --- a/apps/desktop/src/renderer/src/pages/tasks.tsx +++ b/apps/desktop/src/renderer/src/pages/tasks.tsx @@ -848,7 +848,7 @@ export const TasksPage = ({ {/* Main Content Area */}
{/* Page Header — compact single-row toolbar */} -
+
@@ -907,7 +907,7 @@ export const TasksPage = ({ strokeLinecap="round" /> - Filter + Filter {filtersActive && ( {countActiveFilters(filters)} From 2872836c15ee153fe51c0cbaf57c921f9fad5361 Mon Sep 17 00:00:00 2001 From: Kaan Karaca Date: Fri, 20 Mar 2026 22:26:48 +0300 Subject: [PATCH 18/80] feat(inbox): redesign inbox with toolbar, filters, and search Add PageToolbar to inbox header with inline capture input. Redesign health view with type-breakdown cards and Pill badges. Add archived search, type filters, and snoozed item toggle. Refactor archived item rows with icon config map. --- .../inbox/inbox-archived-item-row.tsx | 218 +++---- .../components/inbox/inbox-archived-view.tsx | 162 ++--- .../src/components/inbox/inbox-list.tsx | 256 +++++--- .../inbox/inbox-segment-control.tsx | 66 +- .../renderer/src/hooks/use-inbox-keyboard.ts | 11 +- .../src/renderer/src/lib/inbox-utils.ts | 16 + apps/desktop/src/renderer/src/pages/inbox.tsx | 272 +++++++- .../src/pages/inbox/inbox-archived-view.tsx | 13 +- .../src/pages/inbox/inbox-health-view.tsx | 589 ++++++++++-------- .../src/pages/inbox/inbox-list-view.tsx | 296 +-------- .../renderer/src/pages/inbox/triage-view.tsx | 8 +- 11 files changed, 962 insertions(+), 945 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/inbox/inbox-archived-item-row.tsx b/apps/desktop/src/renderer/src/components/inbox/inbox-archived-item-row.tsx index a24c149c9..d5896c85a 100644 --- a/apps/desktop/src/renderer/src/components/inbox/inbox-archived-item-row.tsx +++ b/apps/desktop/src/renderer/src/components/inbox/inbox-archived-item-row.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { formatDistanceToNow } from 'date-fns' +import { format } from 'date-fns' import { FileText, Link, @@ -10,8 +10,6 @@ import { Share2, Bell, StickyNote, - RotateCcw, - Trash2, X, Check, Loader2 @@ -23,6 +21,21 @@ interface ArchivedInboxItem extends InboxItemListItem { archivedAt?: Date | string } +const ICON_SIZE = 14 + +const TYPE_ICON_CONFIG: Record = { + link: { icon: Link, className: 'text-accent-purple' }, + note: { icon: FileText, className: 'text-muted-foreground' }, + image: { icon: Image, className: 'text-task-complete' }, + voice: { icon: Mic, className: 'text-task-star' }, + clip: { icon: Paperclip, className: 'text-accent-purple' }, + pdf: { icon: FileIcon, className: 'text-destructive' }, + social: { icon: Share2, className: 'text-accent-cyan' }, + reminder: { icon: Bell, className: 'text-task-star' } +} + +const DEFAULT_ICON_CONFIG = { icon: StickyNote, className: 'text-muted-foreground' } + export interface InboxArchivedItemRowProps { item: ArchivedInboxItem onUnarchive: (id: string) => void @@ -40,30 +53,11 @@ export function InboxArchivedItemRow({ }: InboxArchivedItemRowProps): React.JSX.Element { const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) - const getIcon = (): React.JSX.Element => { - const iconClass = 'w-4 h-4 text-muted-foreground/60' - - switch (item.type) { - case 'link': - return - case 'note': - return