diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f217b1e0..64fb1b54f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Format: weekly entries grouped by feature area. --- +## 2026-04-14 — Vim Hint Mode (#202) + +### Added +- Add Vim-style hint mode: Alt+F overlays typed labels on clickable elements for mouseless navigation, with single-char mnemonics, two-char fallback on first-letter collisions, backspace undo, and Escape to dismiss + +--- + ## 2026-04-12 — Windows Inbox Folder Filing Fix ### Fixed 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 f108d5991..a2c759945 100644 --- a/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts +++ b/apps/desktop/src/main/ipc/generated-ipc-invoke-map.ts @@ -2,2373 +2,316 @@ /* 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: false; error: string } - | { 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' - | 'project' - | 'journal' - | 'task' - | 'settings' - | 'inbox' - | 'tag_definition' - | 'folder_config' - 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' - | 'project' - | 'journal' - | 'task' - | 'settings' - | 'inbox' - | 'tag_definition' - | 'folder_config' - 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' - | 'project' - | 'journal' - | 'task' - | 'settings' - | 'inbox' - | 'tag_definition' - | 'folder_config' - 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< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/folder-view-api').DeleteViewResponse - > - > - '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< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/folder-view-api').SetConfigResponse - > - > - '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< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/folder-view-api').SetViewResponse - > - > - 'graph:get-graph-data': (...args: []) => Awaited<{ - nodes: { - id: string - type: 'note' | 'project' | 'journal' | 'task' - 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' | 'project' | 'journal' | 'task' - 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-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: [unknown] - ) => Awaited> - 'inbox:capture-image': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:capture-link': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:capture-pdf': ( - ...args: [unknown] - ) => Awaited> - 'inbox:capture-text': ( - ...args: [any] - ) => Awaited< - Promise - > - 'inbox:capture-voice': ( - ...args: [any] - ) => Awaited< - Promise - > - '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-jobs': ( - ...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< - Promise<{ - suggestions: import('../../../../../packages/domain-inbox/src/types').InboxFilingSuggestion[] - }> - > - '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:preview-link': (...args: [string]) => Awaited< - Promise< - | { - title: string - domain: string - favicon: string | undefined - image: string | undefined - description: string | undefined - } - | { - title: string - domain: string - favicon?: undefined - image?: undefined - description?: undefined - } - > - > - '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: [string, string, string, string, number, string[], string[]] - ) => Awaited< - Promise<{ success: false; error: string } | { success: boolean; error?: string | undefined }> - > - '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:add-property-option': ( - ...args: [{ propertyName: string; option: { value: string; color: string } }] - ) => Awaited> - 'notes:add-status-option': ( - ...args: [ - { - propertyName: string - categoryKey: 'todo' | 'in_progress' | 'done' - option: { value: string; color: string } - } - ] - ) => Awaited> - 'notes:create': ( - ...args: [ - { - title: string - content?: string | undefined - folder?: string | undefined - tags?: string[] | undefined - template?: string | undefined - } - ] - ) => Awaited< - Promise< - { success: false; error: string } | { success: boolean; note: import('../vault/notes').Note } - > - > - 'notes:create-folder': ( - ...args: [string] - ) => Awaited> - 'notes:create-property-definition': ( - ...args: [ - { - name: string - type: 'number' | 'date' | 'text' | 'select' | 'checkbox' | 'url' | 'status' | 'multiselect' - options?: { value: string; color: string; default?: boolean | undefined }[] | undefined - defaultValue?: unknown - color?: string | undefined - } - ] - ) => Awaited< - Promise< - | { success: false; error: string } - | { - success: boolean - definition: - | import('../../../../../packages/contracts/src/property-types').PropertyDefinition - | undefined - } - | { - success: boolean - definition: { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - } - } - > - > - 'notes:delete': ( - ...args: [string] - ) => Awaited> - 'notes:delete-attachment': ( - ...args: [{ noteId: string; filename: string }] - ) => Awaited> - 'notes:delete-folder': ( - ...args: [string] - ) => Awaited> - 'notes:delete-property-definition': ( - ...args: [{ name: string }] - ) => Awaited> - 'notes:delete-version': ( - ...args: [string] - ) => Awaited> - 'notes:ensure-property-definition': ( - ...args: [{ name: string; type: 'select' | 'status' | 'multiselect' }] - ) => Awaited> - 'notes:exists': (...args: [string]) => Awaited> - 'notes:export-html': ( - ...args: [ - { - noteId: string - includeMetadata?: boolean | undefined - pageSize?: 'A4' | 'Letter' | 'Legal' | undefined - } - ] - ) => Awaited< - Promise< - | { success: false; error: string } - | { 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: false; error: string } - | { 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: false; error: string } | { success: boolean; positions: Record } - > - > - '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: false; error: string } - | { success: boolean; positions: { path: string; position: number; folderPath: 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< - Promise<{ success: false; error: string } | import('../vault/notes').ImportFilesResult> - > - '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: false; error: string } | { success: boolean; note: import('../vault/notes').Note } - > - > - 'notes:open-external': (...args: [string]) => Awaited> - 'notes:preview-by-title': (...args: [string]) => Awaited< - Promise<{ - id: string - title: string - emoji: string | null - snippet: string | null - tags: { name: string; color: string }[] - createdAt: string - } | null> - > - 'notes:remove-property-option': ( - ...args: [{ propertyName: string; optionValue: string }] - ) => Awaited> - 'notes:rename': ( - ...args: [{ id: string; newTitle: string }] - ) => Awaited< - Promise< - { success: false; error: string } | { success: boolean; note: import('../vault/notes').Note } - > - > - 'notes:rename-folder': ( - ...args: [{ oldPath: string; newPath: string }] - ) => Awaited> - 'notes:rename-property-option': ( - ...args: [{ propertyName: string; oldValue: string; newValue: string }] - ) => Awaited> - 'notes:reorder': ( - ...args: [{ folderPath: string; notePaths: string[] }] - ) => Awaited> - '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: false; error: string } | { success: boolean; note: import('../vault/notes').Note } - > - > - 'notes:reveal-in-finder': (...args: [string]) => Awaited> - 'notes:set-folder-config': ( - ...args: [ - { - folderPath: string - config: { - icon?: string | null | undefined - template?: string | undefined - inherit?: boolean | undefined - } - } - ] - ) => Awaited> - 'notes:set-local-only': ( - ...args: [{ id: string; localOnly: boolean }] - ) => Awaited< - Promise< - { success: false; error: string } | { success: boolean; note: import('../vault/notes').Note } - > - > - '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: false; error: string } | { success: boolean; note: import('../vault/notes').Note } - > - > - 'notes:update-option-color': ( - ...args: [{ propertyName: string; optionValue: string; newColor: string }] - ) => Awaited> - 'notes:update-property-definition': ( - ...args: [ - { - name: string - type?: - | 'number' - | 'date' - | 'text' - | 'select' - | 'checkbox' - | 'url' - | 'status' - | 'multiselect' - | undefined - options?: { value: string; color: string; default?: boolean | undefined }[] | undefined - defaultValue?: unknown - color?: string | undefined - } - ] - ) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; definition: null; error: string } - | { - success: boolean - definition: - | import('../../../../../packages/contracts/src/property-types').PropertyDefinition - | undefined - error?: undefined - } - | { - success: boolean - definition: - | { - type: string - name: string - createdAt: string - options: string | null - defaultValue: string | null - color: string | null - } - | undefined - error?: undefined - } - > - > - '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< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/properties-api').RenamePropertyResponse - > - > - 'properties:set': ( - ...args: [{ entityId: string; properties: Record }] - ) => Awaited< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/properties-api').SetPropertiesResponse - > - > - 'quick-capture:get-clipboard': (...args: []) => Awaited - 'reminder:bulk-dismiss': ( - ...args: [{ reminderIds: string[] }] - ) => Awaited< - Promise<{ success: false; error: string } | { success: boolean; dismissedCount: number }> - > - '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: false; error: string } - | { - success: boolean - reminder: import('../../../../../packages/contracts/src/reminders-api').Reminder - } - > - > - 'reminder:delete': ( - ...args: [string] - ) => Awaited< - Promise<{ success: boolean; error: string } | { success: boolean; error?: undefined }> - > - 'reminder:dismiss': (...args: [string]) => Awaited< - Promise< - | { success: false; error: string } - | { 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: false; error: string } - | { 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: false; error: string } - | { 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' | 'journal' | 'task' | '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' | 'journal' | 'task' | '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:downloadVoiceModel': ( - ...args: [] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; 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' | 'gelasio' | 'geist' | 'inter' - accentColor: string - startOnBoot: boolean - language: string - onboardingCompleted: boolean - createInSelectedFolder: 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:getVoiceModelStatus': ( - ...args: [] - ) => Awaited - 'settings:getVoiceRecordingReadiness': ( - ...args: [] - ) => Awaited> - 'settings:getVoiceTranscriptionOpenAIKeyStatus': ( - ...args: [] - ) => Awaited> - 'settings:getVoiceTranscriptionSettings': ( - ...args: [] - ) => Awaited<{ provider: 'local' | 'openai' }> - 'settings:loadAIModel': ( - ...args: [] - ) => Awaited< - Promise< - | { success: false; error: string } - | { 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: false; error: string } - | { 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' | 'gelasio' | 'geist' | 'inter' - accentColor: string - startOnBoot: boolean - language: string - onboardingCompleted: boolean - createInSelectedFolder: 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 }> - 'settings:setVoiceTranscriptionOpenAIKey': ( - ...args: [{ apiKey: string }] - ) => Awaited< - Promise<{ success: boolean; error?: undefined } | { success: boolean; error: string }> - > - 'settings:setVoiceTranscriptionSettings': ( - ...args: [Partial<{ provider: 'local' | 'openai' }>] - ) => 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' - | 'gelasio' - | 'geist' - | 'inter' - | undefined - accentColor?: string | undefined - startOnBoot?: boolean | undefined - language?: string | undefined - createInSelectedFolder?: boolean | 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> - '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< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/tags-api').DeleteTagResponse - > - > - '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 - includeDescendants?: boolean | undefined - } - ] - ) => Awaited< - Promise - > - 'tags:merge': ( - ...args: [{ source: string; target: string }] - ) => Awaited< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/tags-api').MergeTagResponse - > - > - 'tags:pin-note-to-tag': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/tags-api').TagOperationResponse - > - > - 'tags:remove-from-note': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/tags-api').TagOperationResponse - > - > - 'tags:rename': ( - ...args: [{ oldName: string; newName: string }] - ) => Awaited< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/tags-api').RenameTagResponse - > - > - 'tags:unpin-note-from-tag': ( - ...args: [{ noteId: string; tag: string }] - ) => Awaited< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/tags-api').TagOperationResponse - > - > - 'tags:update-color': ( - ...args: [{ tag: string; color: string }] - ) => Awaited< - Promise< - | { success: false; error: string } - | import('../../../../../packages/contracts/src/tags-api').TagOperationResponse - > - > - 'tasks:archive': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; error: string } - | { success: boolean; error?: undefined } - > - > - '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< - Promise< - | { success: false; error: string } - | { success: boolean; task: null; error: string } - | { - success: boolean - task: import('../../../../../packages/domain-tasks/src/types').Task - error?: undefined - } - > - > - 'tasks:convert-to-subtask': (...args: [{ taskId: string; parentId: string }]) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; task: null; error: string } - | { - success: boolean - task: import('../../../../../packages/domain-tasks/src/types').Task - error?: undefined - } - > - > - 'tasks:convert-to-task': (...args: [string]) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; task: null; error: string } - | { - success: boolean - task: import('../../../../../packages/domain-tasks/src/types').Task - 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: false; error: string } - | { success: boolean; task: import('../../../../../packages/domain-tasks/src/types').Task } - > - > - 'tasks:delete': ( - ...args: [string] - ) => Awaited> - 'tasks:duplicate': (...args: [string]) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; task: null; error: string } - | { - success: boolean - task: import('../../../../../packages/domain-tasks/src/types').Task - error?: undefined - } - > - > - '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< - Promise< - | { success: false; error: string } - | { success: boolean; task: null; error: string } - | { - success: boolean - task: import('../../../../../packages/domain-tasks/src/types').Task - error?: undefined - } - > - > - 'tasks:project-archive': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; error: string } - | { success: boolean; error?: undefined } - > - > - '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: false; error: string } - | { - success: boolean - project: import('../../../../../packages/domain-tasks/src/types').ProjectWithStatuses - } - > - > - 'tasks:project-delete': ( - ...args: [string] - ) => Awaited> - 'tasks:project-get': ( - ...args: [string] - ) => Awaited< - Promise< - import('../../../../../packages/domain-tasks/src/types').ProjectWithStatuses | undefined - > - > - 'tasks:project-list': (...args: []) => Awaited< - Promise<{ - projects: import('../../../../../packages/domain-tasks/src/types').ProjectWithStats[] - }> - > - '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< - Promise< - | { success: false; error: string } - | { success: boolean; project: null; error: string } - | { - success: boolean - project: import('../../../../../packages/domain-tasks/src/types').ProjectWithStatuses - error?: undefined - } - > - > - '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< - Promise< - | { success: false; error: string } - | { - success: boolean - status: import('../../../../../packages/domain-tasks/src/types').Status - } - > - > - '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< - Promise< - | { success: false; error: string } - | { success: boolean; error: string; status?: undefined } - | { - success: boolean - status: import('../../../../../packages/domain-tasks/src/types').Status - error?: undefined - } - > - > - 'tasks:unarchive': ( - ...args: [string] - ) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; error: string } - | { success: boolean; error?: undefined } - > - > - 'tasks:uncomplete': (...args: [string]) => Awaited< - Promise< - | { success: false; error: string } - | { success: boolean; task: null; error: string } - | { - success: boolean - task: import('../../../../../packages/domain-tasks/src/types').Task - 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: false; error: string } - | { success: boolean; task: null; error: string } - | { - success: boolean - task: import('../../../../../packages/domain-tasks/src/types').Task - 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: false; error: string } - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - } - > - > - 'templates:delete': ( - ...args: [string] - ) => Awaited> - 'templates:duplicate': (...args: [{ id: string; newName: string }]) => Awaited< - Promise< - | { success: false; error: string } - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - } - > - > - '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: false; error: string } - | { - success: boolean - template: import('../../../../../packages/contracts/src/templates-api').Template - } - > - > - '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:reveal': (...args: []) => 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" | "project" | "journal" | "task" | "settings" | "inbox" | "tag_definition" | "folder_config"; 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" | "project" | "journal" | "task" | "settings" | "inbox" | "tag_definition" | "folder_config"; 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" | "project" | "journal" | "task" | "settings" | "inbox" | "tag_definition" | "folder_config"; 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" | "project" | "journal" | "task"; 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" | "project" | "journal" | "task"; 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-file": (...args: [any]) => Awaited> + "inbox:bulk-snooze": (...args: [any]) => Awaited> + "inbox:bulk-tag": (...args: [any]) => Awaited> + "inbox:capture-clip": (...args: [unknown]) => Awaited> + "inbox:capture-image": (...args: [any]) => Awaited> + "inbox:capture-link": (...args: [any]) => Awaited> + "inbox:capture-pdf": (...args: [unknown]) => 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-jobs": (...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:preview-link": (...args: [string]) => 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: [string, string, string, string, number, string[], string[]]) => 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:add-property-option": (...args: [{ propertyName: string; option: { value: string; color: string; }; }]) => Awaited> + "notes:add-status-option": (...args: [{ propertyName: string; categoryKey: "todo" | "in_progress" | "done"; option: { value: string; color: string; }; }]) => Awaited> + "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" | "select" | "checkbox" | "url" | "status" | "multiselect"; options?: { value: string; color: string; default?: boolean | undefined; }[] | 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-property-definition": (...args: [{ name: string; }]) => Awaited> + "notes:delete-version": (...args: [string]) => Awaited> + "notes:ensure-property-definition": (...args: [{ name: string; type: "select" | "status" | "multiselect"; }]) => 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; }>> + "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:preview-by-title": (...args: [string]) => Awaited> + "notes:remove-property-option": (...args: [{ propertyName: string; optionValue: string; }]) => Awaited> + "notes:rename": (...args: [{ id: string; newTitle: string; }]) => Awaited> + "notes:rename-folder": (...args: [{ oldPath: string; newPath: string; }]) => Awaited> + "notes:rename-property-option": (...args: [{ propertyName: string; oldValue: string; newValue: 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: { icon?: string | null | undefined; 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-option-color": (...args: [{ propertyName: string; optionValue: string; newColor: string; }]) => Awaited> + "notes:update-property-definition": (...args: [{ name: string; type?: "number" | "date" | "text" | "select" | "checkbox" | "url" | "status" | "multiselect" | undefined; options?: { value: string; color: string; default?: boolean | undefined; }[] | 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" | "journal" | "task" | "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" | "journal" | "task" | "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:downloadVoiceModel": (...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" | "gelasio" | "geist" | "inter"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; createInSelectedFolder: 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:getVoiceModelStatus": (...args: []) => Awaited + "settings:getVoiceRecordingReadiness": (...args: []) => Awaited> + "settings:getVoiceTranscriptionOpenAIKeyStatus": (...args: []) => Awaited> + "settings:getVoiceTranscriptionSettings": (...args: []) => Awaited<{ provider: "local" | "openai"; }> + "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" | "gelasio" | "geist" | "inter"; accentColor: string; startOnBoot: boolean; language: string; onboardingCompleted: boolean; createInSelectedFolder: 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; }> + "settings:setVoiceTranscriptionOpenAIKey": (...args: [{ apiKey: string; }]) => Awaited> + "settings:setVoiceTranscriptionSettings": (...args: [Partial<{ provider: "local" | "openai"; }>]) => 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" | "gelasio" | "geist" | "inter" | undefined; accentColor?: string | undefined; startOnBoot?: boolean | undefined; language?: string | undefined; createInSelectedFolder?: boolean | 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; includeDescendants?: boolean | 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:reveal": (...args: []) => 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/preload/generated-rpc.ts b/apps/desktop/src/preload/generated-rpc.ts index 633a23bf6..643b2691f 100644 --- a/apps/desktop/src/preload/generated-rpc.ts +++ b/apps/desktop/src/preload/generated-rpc.ts @@ -2,11 +2,7 @@ export type { GeneratedRpcApi } from '@memry/rpc' import type { GeneratedRpcApi } from '@memry/rpc' -import type { - MainIpcInvokeArgs, - MainIpcInvokeChannel, - MainIpcInvokeResult -} from '../main/ipc/generated-ipc-invoke-map' +import type { MainIpcInvokeArgs, MainIpcInvokeChannel, MainIpcInvokeResult } from '../main/ipc/generated-ipc-invoke-map' export interface GeneratedRpcDeps { invoke( @@ -24,257 +20,119 @@ export function createGeneratedRpcApi({ }: GeneratedRpcDeps): GeneratedRpcApi { return { notes: { - create: ((input) => invoke('notes:create', input)) as GeneratedRpcApi['notes']['create'], - get: ((id) => invoke('notes:get', id)) as GeneratedRpcApi['notes']['get'], - getByPath: ((path) => - invoke('notes:get-by-path', path)) as GeneratedRpcApi['notes']['getByPath'], - getFile: ((id) => invoke('notes:get-file', id)) as GeneratedRpcApi['notes']['getFile'], - resolveByTitle: ((title) => - invoke('notes:resolve-by-title', title)) as GeneratedRpcApi['notes']['resolveByTitle'], - previewByTitle: ((title) => - invoke('notes:preview-by-title', title)) as GeneratedRpcApi['notes']['previewByTitle'], - update: ((input) => invoke('notes:update', input)) as GeneratedRpcApi['notes']['update'], - rename: ((id, newTitle) => - invoke('notes:rename', { id, newTitle })) as GeneratedRpcApi['notes']['rename'], - move: ((id, newFolder) => - invoke('notes:move', { id, newFolder })) as GeneratedRpcApi['notes']['move'], - delete: ((id) => invoke('notes:delete', id)) as GeneratedRpcApi['notes']['delete'], - list: ((options) => invoke('notes:list', options ?? {})) as GeneratedRpcApi['notes']['list'], - getTags: (() => invoke('notes:get-tags')) as GeneratedRpcApi['notes']['getTags'], - getLinks: ((id) => invoke('notes:get-links', id)) as GeneratedRpcApi['notes']['getLinks'], - getFolders: (() => invoke('notes:get-folders')) as GeneratedRpcApi['notes']['getFolders'], - createFolder: ((path) => - invoke('notes:create-folder', path)) as GeneratedRpcApi['notes']['createFolder'], - renameFolder: ((oldPath, newPath) => - invoke('notes:rename-folder', { - oldPath, - newPath - })) as GeneratedRpcApi['notes']['renameFolder'], - deleteFolder: ((path) => - invoke('notes:delete-folder', path)) as GeneratedRpcApi['notes']['deleteFolder'], - exists: ((titleOrPath) => - invoke('notes:exists', titleOrPath)) as GeneratedRpcApi['notes']['exists'], - openExternal: ((id) => - invoke('notes:open-external', id)) as GeneratedRpcApi['notes']['openExternal'], - revealInFinder: ((id) => - invoke('notes:reveal-in-finder', id)) as GeneratedRpcApi['notes']['revealInFinder'], - getPropertyDefinitions: (() => - invoke( - 'notes:get-property-definitions' - )) as GeneratedRpcApi['notes']['getPropertyDefinitions'], - createPropertyDefinition: ((input) => - invoke( - 'notes:create-property-definition', - input - )) as GeneratedRpcApi['notes']['createPropertyDefinition'], - updatePropertyDefinition: ((input) => - invoke( - 'notes:update-property-definition', - input - )) as GeneratedRpcApi['notes']['updatePropertyDefinition'], - ensurePropertyDefinition: ((name, type) => - invoke('notes:ensure-property-definition', { - name, - type - })) as GeneratedRpcApi['notes']['ensurePropertyDefinition'], - addPropertyOption: ((propertyName, option) => - invoke('notes:add-property-option', { - propertyName, - option - })) as GeneratedRpcApi['notes']['addPropertyOption'], - addStatusOption: ((propertyName, categoryKey, option) => - invoke('notes:add-status-option', { - propertyName, - categoryKey, - option - })) as GeneratedRpcApi['notes']['addStatusOption'], - removePropertyOption: ((propertyName, optionValue) => - invoke('notes:remove-property-option', { - propertyName, - optionValue - })) as GeneratedRpcApi['notes']['removePropertyOption'], - renamePropertyOption: ((propertyName, oldValue, newValue) => - invoke('notes:rename-property-option', { - propertyName, - oldValue, - newValue - })) as GeneratedRpcApi['notes']['renamePropertyOption'], - updateOptionColor: ((propertyName, optionValue, newColor) => - invoke('notes:update-option-color', { - propertyName, - optionValue, - newColor - })) as GeneratedRpcApi['notes']['updateOptionColor'], - deletePropertyDefinition: ((name) => - invoke('notes:delete-property-definition', { - name - })) as GeneratedRpcApi['notes']['deletePropertyDefinition'], + create: ((input) => invoke("notes:create", input)) as GeneratedRpcApi["notes"]["create"], + get: ((id) => invoke("notes:get", id)) as GeneratedRpcApi["notes"]["get"], + getByPath: ((path) => invoke("notes:get-by-path", path)) as GeneratedRpcApi["notes"]["getByPath"], + getFile: ((id) => invoke("notes:get-file", id)) as GeneratedRpcApi["notes"]["getFile"], + resolveByTitle: ((title) => invoke("notes:resolve-by-title", title)) as GeneratedRpcApi["notes"]["resolveByTitle"], + previewByTitle: ((title) => invoke("notes:preview-by-title", title)) as GeneratedRpcApi["notes"]["previewByTitle"], + update: ((input) => invoke("notes:update", input)) as GeneratedRpcApi["notes"]["update"], + rename: ((id, newTitle) => invoke("notes:rename", { id, newTitle })) as GeneratedRpcApi["notes"]["rename"], + move: ((id, newFolder) => invoke("notes:move", { id, newFolder })) as GeneratedRpcApi["notes"]["move"], + delete: ((id) => invoke("notes:delete", id)) as GeneratedRpcApi["notes"]["delete"], + list: ((options) => invoke("notes:list", options ?? {})) as GeneratedRpcApi["notes"]["list"], + getTags: (() => invoke("notes:get-tags")) as GeneratedRpcApi["notes"]["getTags"], + getLinks: ((id) => invoke("notes:get-links", id)) as GeneratedRpcApi["notes"]["getLinks"], + getFolders: (() => invoke("notes:get-folders")) as GeneratedRpcApi["notes"]["getFolders"], + createFolder: ((path) => invoke("notes:create-folder", path)) as GeneratedRpcApi["notes"]["createFolder"], + renameFolder: ((oldPath, newPath) => invoke("notes:rename-folder", { oldPath, newPath })) as GeneratedRpcApi["notes"]["renameFolder"], + deleteFolder: ((path) => invoke("notes:delete-folder", path)) as GeneratedRpcApi["notes"]["deleteFolder"], + exists: ((titleOrPath) => invoke("notes:exists", titleOrPath)) as GeneratedRpcApi["notes"]["exists"], + openExternal: ((id) => invoke("notes:open-external", id)) as GeneratedRpcApi["notes"]["openExternal"], + revealInFinder: ((id) => invoke("notes:reveal-in-finder", id)) as GeneratedRpcApi["notes"]["revealInFinder"], + getPropertyDefinitions: (() => invoke("notes:get-property-definitions")) as GeneratedRpcApi["notes"]["getPropertyDefinitions"], + createPropertyDefinition: ((input) => invoke("notes:create-property-definition", input)) as GeneratedRpcApi["notes"]["createPropertyDefinition"], + updatePropertyDefinition: ((input) => invoke("notes:update-property-definition", input)) as GeneratedRpcApi["notes"]["updatePropertyDefinition"], + ensurePropertyDefinition: ((name, type) => invoke("notes:ensure-property-definition", { name, type })) as GeneratedRpcApi["notes"]["ensurePropertyDefinition"], + addPropertyOption: ((propertyName, option) => invoke("notes:add-property-option", { propertyName, option })) as GeneratedRpcApi["notes"]["addPropertyOption"], + addStatusOption: ((propertyName, categoryKey, option) => invoke("notes:add-status-option", { propertyName, categoryKey, option })) as GeneratedRpcApi["notes"]["addStatusOption"], + removePropertyOption: ((propertyName, optionValue) => invoke("notes:remove-property-option", { propertyName, optionValue })) as GeneratedRpcApi["notes"]["removePropertyOption"], + renamePropertyOption: ((propertyName, oldValue, newValue) => invoke("notes:rename-property-option", { propertyName, oldValue, newValue })) as GeneratedRpcApi["notes"]["renamePropertyOption"], + updateOptionColor: ((propertyName, optionValue, newColor) => invoke("notes:update-option-color", { propertyName, optionValue, newColor })) as GeneratedRpcApi["notes"]["updateOptionColor"], + deletePropertyDefinition: ((name) => invoke("notes:delete-property-definition", { name })) as GeneratedRpcApi["notes"]["deletePropertyDefinition"], uploadAttachment: (async (noteId, file) => - invoke('notes:upload-attachment', { + invoke("notes:upload-attachment", { noteId, filename: file.name, data: Array.from(new Uint8Array(await file.arrayBuffer())) - })) as GeneratedRpcApi['notes']['uploadAttachment'], - listAttachments: ((noteId) => - invoke('notes:list-attachments', noteId)) as GeneratedRpcApi['notes']['listAttachments'], - deleteAttachment: ((noteId, filename) => - invoke('notes:delete-attachment', { - noteId, - filename - })) as GeneratedRpcApi['notes']['deleteAttachment'], - getFolderConfig: ((folderPath) => - invoke( - 'notes:get-folder-config', - folderPath - )) as GeneratedRpcApi['notes']['getFolderConfig'], - setFolderConfig: ((folderPath, config) => - invoke('notes:set-folder-config', { - folderPath, - config - })) as GeneratedRpcApi['notes']['setFolderConfig'], - getFolderTemplate: ((folderPath) => - invoke( - 'notes:get-folder-template', - folderPath - )) as GeneratedRpcApi['notes']['getFolderTemplate'], - exportPdf: ((input) => - invoke('notes:export-pdf', input)) as GeneratedRpcApi['notes']['exportPdf'], - exportHtml: ((input) => - invoke('notes:export-html', input)) as GeneratedRpcApi['notes']['exportHtml'], - getVersions: ((noteId) => - invoke('notes:get-versions', noteId)) as GeneratedRpcApi['notes']['getVersions'], - getVersion: ((snapshotId) => - invoke('notes:get-version', snapshotId)) as GeneratedRpcApi['notes']['getVersion'], - restoreVersion: ((snapshotId) => - invoke('notes:restore-version', snapshotId)) as GeneratedRpcApi['notes']['restoreVersion'], - deleteVersion: ((snapshotId) => - invoke('notes:delete-version', snapshotId)) as GeneratedRpcApi['notes']['deleteVersion'], - getPositions: ((folderPath) => - invoke('notes:get-positions', { folderPath })) as GeneratedRpcApi['notes']['getPositions'], - getAllPositions: (() => - invoke('notes:get-all-positions')) as GeneratedRpcApi['notes']['getAllPositions'], - reorder: ((folderPath, notePaths) => - invoke('notes:reorder', { folderPath, notePaths })) as GeneratedRpcApi['notes']['reorder'], - importFiles: ((sourcePaths, targetFolder) => - invoke('notes:import-files', { - sourcePaths, - targetFolder - })) as GeneratedRpcApi['notes']['importFiles'], - showImportDialog: (() => - invoke('notes:show-import-dialog')) as GeneratedRpcApi['notes']['showImportDialog'], - setLocalOnly: ((id, localOnly) => - invoke('notes:set-local-only', { - id, - localOnly - })) as GeneratedRpcApi['notes']['setLocalOnly'], - getLocalOnlyCount: (() => - invoke('notes:get-local-only-count')) as GeneratedRpcApi['notes']['getLocalOnlyCount'] + })) as GeneratedRpcApi["notes"]["uploadAttachment"], + listAttachments: ((noteId) => invoke("notes:list-attachments", noteId)) as GeneratedRpcApi["notes"]["listAttachments"], + deleteAttachment: ((noteId, filename) => invoke("notes:delete-attachment", { noteId, filename })) as GeneratedRpcApi["notes"]["deleteAttachment"], + getFolderConfig: ((folderPath) => invoke("notes:get-folder-config", folderPath)) as GeneratedRpcApi["notes"]["getFolderConfig"], + setFolderConfig: ((folderPath, config) => invoke("notes:set-folder-config", { folderPath, config })) as GeneratedRpcApi["notes"]["setFolderConfig"], + getFolderTemplate: ((folderPath) => invoke("notes:get-folder-template", folderPath)) as GeneratedRpcApi["notes"]["getFolderTemplate"], + exportPdf: ((input) => invoke("notes:export-pdf", input)) as GeneratedRpcApi["notes"]["exportPdf"], + exportHtml: ((input) => invoke("notes:export-html", input)) as GeneratedRpcApi["notes"]["exportHtml"], + getVersions: ((noteId) => invoke("notes:get-versions", noteId)) as GeneratedRpcApi["notes"]["getVersions"], + getVersion: ((snapshotId) => invoke("notes:get-version", snapshotId)) as GeneratedRpcApi["notes"]["getVersion"], + restoreVersion: ((snapshotId) => invoke("notes:restore-version", snapshotId)) as GeneratedRpcApi["notes"]["restoreVersion"], + deleteVersion: ((snapshotId) => invoke("notes:delete-version", snapshotId)) as GeneratedRpcApi["notes"]["deleteVersion"], + getPositions: ((folderPath) => invoke("notes:get-positions", { folderPath })) as GeneratedRpcApi["notes"]["getPositions"], + getAllPositions: (() => invoke("notes:get-all-positions")) as GeneratedRpcApi["notes"]["getAllPositions"], + reorder: ((folderPath, notePaths) => invoke("notes:reorder", { folderPath, notePaths })) as GeneratedRpcApi["notes"]["reorder"], + importFiles: ((sourcePaths, targetFolder) => invoke("notes:import-files", { sourcePaths, targetFolder })) as GeneratedRpcApi["notes"]["importFiles"], + showImportDialog: (() => invoke("notes:show-import-dialog")) as GeneratedRpcApi["notes"]["showImportDialog"], + setLocalOnly: ((id, localOnly) => invoke("notes:set-local-only", { id, localOnly })) as GeneratedRpcApi["notes"]["setLocalOnly"], + getLocalOnlyCount: (() => invoke("notes:get-local-only-count")) as GeneratedRpcApi["notes"]["getLocalOnlyCount"], }, tasks: { - create: ((input) => invoke('tasks:create', input)) as GeneratedRpcApi['tasks']['create'], - get: ((id) => invoke('tasks:get', id)) as GeneratedRpcApi['tasks']['get'], - update: ((input) => invoke('tasks:update', input)) as GeneratedRpcApi['tasks']['update'], - delete: ((id) => invoke('tasks:delete', id)) as GeneratedRpcApi['tasks']['delete'], - list: ((options) => invoke('tasks:list', options ?? {})) as GeneratedRpcApi['tasks']['list'], - complete: ((input) => - invoke('tasks:complete', input)) as GeneratedRpcApi['tasks']['complete'], - uncomplete: ((id) => - invoke('tasks:uncomplete', id)) as GeneratedRpcApi['tasks']['uncomplete'], - archive: ((id) => invoke('tasks:archive', id)) as GeneratedRpcApi['tasks']['archive'], - unarchive: ((id) => invoke('tasks:unarchive', id)) as GeneratedRpcApi['tasks']['unarchive'], - move: ((input) => invoke('tasks:move', input)) as GeneratedRpcApi['tasks']['move'], - reorder: ((taskIds, positions) => - invoke('tasks:reorder', { taskIds, positions })) as GeneratedRpcApi['tasks']['reorder'], - duplicate: ((id) => invoke('tasks:duplicate', id)) as GeneratedRpcApi['tasks']['duplicate'], - getSubtasks: ((parentId) => - invoke('tasks:get-subtasks', parentId)) as GeneratedRpcApi['tasks']['getSubtasks'], - convertToSubtask: ((taskId, parentId) => - invoke('tasks:convert-to-subtask', { - taskId, - parentId - })) as GeneratedRpcApi['tasks']['convertToSubtask'], - convertToTask: ((taskId) => - invoke('tasks:convert-to-task', taskId)) as GeneratedRpcApi['tasks']['convertToTask'], - createProject: ((input) => - invoke('tasks:project-create', input)) as GeneratedRpcApi['tasks']['createProject'], - getProject: ((id) => - invoke('tasks:project-get', id)) as GeneratedRpcApi['tasks']['getProject'], - updateProject: ((input) => - invoke('tasks:project-update', input)) as GeneratedRpcApi['tasks']['updateProject'], - deleteProject: ((id) => - invoke('tasks:project-delete', id)) as GeneratedRpcApi['tasks']['deleteProject'], - listProjects: (() => - invoke('tasks:project-list')) as GeneratedRpcApi['tasks']['listProjects'], - archiveProject: ((id) => - invoke('tasks:project-archive', id)) as GeneratedRpcApi['tasks']['archiveProject'], - reorderProjects: ((projectIds, positions) => - invoke('tasks:project-reorder', { - projectIds, - positions - })) as GeneratedRpcApi['tasks']['reorderProjects'], - createStatus: ((input) => - invoke('tasks:status-create', input)) as GeneratedRpcApi['tasks']['createStatus'], - updateStatus: ((id, updates) => - invoke('tasks:status-update', { - id, - ...updates - })) as GeneratedRpcApi['tasks']['updateStatus'], - deleteStatus: ((id) => - invoke('tasks:status-delete', id)) as GeneratedRpcApi['tasks']['deleteStatus'], - reorderStatuses: ((statusIds, positions) => - invoke('tasks:status-reorder', { - statusIds, - positions - })) as GeneratedRpcApi['tasks']['reorderStatuses'], - listStatuses: ((projectId) => - invoke('tasks:status-list', projectId)) as GeneratedRpcApi['tasks']['listStatuses'], - getTags: (() => invoke('tasks:get-tags')) as GeneratedRpcApi['tasks']['getTags'], - bulkComplete: ((ids) => - invoke('tasks:bulk-complete', { ids })) as GeneratedRpcApi['tasks']['bulkComplete'], - bulkDelete: ((ids) => - invoke('tasks:bulk-delete', { ids })) as GeneratedRpcApi['tasks']['bulkDelete'], - bulkMove: ((ids, projectId) => - invoke('tasks:bulk-move', { ids, projectId })) as GeneratedRpcApi['tasks']['bulkMove'], - bulkArchive: ((ids) => - invoke('tasks:bulk-archive', { ids })) as GeneratedRpcApi['tasks']['bulkArchive'], - getStats: (() => invoke('tasks:get-stats')) as GeneratedRpcApi['tasks']['getStats'], - getToday: (() => invoke('tasks:get-today')) as GeneratedRpcApi['tasks']['getToday'], - getUpcoming: ((days) => - invoke('tasks:get-upcoming', { - days: days ?? 7 - })) as GeneratedRpcApi['tasks']['getUpcoming'], - getOverdue: (() => invoke('tasks:get-overdue')) as GeneratedRpcApi['tasks']['getOverdue'], - getLinkedTasks: ((noteId) => - invoke('tasks:get-linked-tasks', noteId)) as GeneratedRpcApi['tasks']['getLinkedTasks'], - seedPerformanceTest: (() => - invoke('tasks:seed-performance-test')) as GeneratedRpcApi['tasks']['seedPerformanceTest'], - seedDemo: (() => invoke('tasks:seed-demo')) as GeneratedRpcApi['tasks']['seedDemo'] + create: ((input) => invoke("tasks:create", input)) as GeneratedRpcApi["tasks"]["create"], + get: ((id) => invoke("tasks:get", id)) as GeneratedRpcApi["tasks"]["get"], + update: ((input) => invoke("tasks:update", input)) as GeneratedRpcApi["tasks"]["update"], + delete: ((id) => invoke("tasks:delete", id)) as GeneratedRpcApi["tasks"]["delete"], + list: ((options) => invoke("tasks:list", options ?? {})) as GeneratedRpcApi["tasks"]["list"], + complete: ((input) => invoke("tasks:complete", input)) as GeneratedRpcApi["tasks"]["complete"], + uncomplete: ((id) => invoke("tasks:uncomplete", id)) as GeneratedRpcApi["tasks"]["uncomplete"], + archive: ((id) => invoke("tasks:archive", id)) as GeneratedRpcApi["tasks"]["archive"], + unarchive: ((id) => invoke("tasks:unarchive", id)) as GeneratedRpcApi["tasks"]["unarchive"], + move: ((input) => invoke("tasks:move", input)) as GeneratedRpcApi["tasks"]["move"], + reorder: ((taskIds, positions) => invoke("tasks:reorder", { taskIds, positions })) as GeneratedRpcApi["tasks"]["reorder"], + duplicate: ((id) => invoke("tasks:duplicate", id)) as GeneratedRpcApi["tasks"]["duplicate"], + getSubtasks: ((parentId) => invoke("tasks:get-subtasks", parentId)) as GeneratedRpcApi["tasks"]["getSubtasks"], + convertToSubtask: ((taskId, parentId) => invoke("tasks:convert-to-subtask", { taskId, parentId })) as GeneratedRpcApi["tasks"]["convertToSubtask"], + convertToTask: ((taskId) => invoke("tasks:convert-to-task", taskId)) as GeneratedRpcApi["tasks"]["convertToTask"], + createProject: ((input) => invoke("tasks:project-create", input)) as GeneratedRpcApi["tasks"]["createProject"], + getProject: ((id) => invoke("tasks:project-get", id)) as GeneratedRpcApi["tasks"]["getProject"], + updateProject: ((input) => invoke("tasks:project-update", input)) as GeneratedRpcApi["tasks"]["updateProject"], + deleteProject: ((id) => invoke("tasks:project-delete", id)) as GeneratedRpcApi["tasks"]["deleteProject"], + listProjects: (() => invoke("tasks:project-list")) as GeneratedRpcApi["tasks"]["listProjects"], + archiveProject: ((id) => invoke("tasks:project-archive", id)) as GeneratedRpcApi["tasks"]["archiveProject"], + reorderProjects: ((projectIds, positions) => invoke("tasks:project-reorder", { projectIds, positions })) as GeneratedRpcApi["tasks"]["reorderProjects"], + createStatus: ((input) => invoke("tasks:status-create", input)) as GeneratedRpcApi["tasks"]["createStatus"], + updateStatus: ((id, updates) => invoke("tasks:status-update", { id, ...updates })) as GeneratedRpcApi["tasks"]["updateStatus"], + deleteStatus: ((id) => invoke("tasks:status-delete", id)) as GeneratedRpcApi["tasks"]["deleteStatus"], + reorderStatuses: ((statusIds, positions) => invoke("tasks:status-reorder", { statusIds, positions })) as GeneratedRpcApi["tasks"]["reorderStatuses"], + listStatuses: ((projectId) => invoke("tasks:status-list", projectId)) as GeneratedRpcApi["tasks"]["listStatuses"], + getTags: (() => invoke("tasks:get-tags")) as GeneratedRpcApi["tasks"]["getTags"], + bulkComplete: ((ids) => invoke("tasks:bulk-complete", { ids })) as GeneratedRpcApi["tasks"]["bulkComplete"], + bulkDelete: ((ids) => invoke("tasks:bulk-delete", { ids })) as GeneratedRpcApi["tasks"]["bulkDelete"], + bulkMove: ((ids, projectId) => invoke("tasks:bulk-move", { ids, projectId })) as GeneratedRpcApi["tasks"]["bulkMove"], + bulkArchive: ((ids) => invoke("tasks:bulk-archive", { ids })) as GeneratedRpcApi["tasks"]["bulkArchive"], + getStats: (() => invoke("tasks:get-stats")) as GeneratedRpcApi["tasks"]["getStats"], + getToday: (() => invoke("tasks:get-today")) as GeneratedRpcApi["tasks"]["getToday"], + getUpcoming: ((days) => invoke("tasks:get-upcoming", { days: days ?? 7 })) as GeneratedRpcApi["tasks"]["getUpcoming"], + getOverdue: (() => invoke("tasks:get-overdue")) as GeneratedRpcApi["tasks"]["getOverdue"], + getLinkedTasks: ((noteId) => invoke("tasks:get-linked-tasks", noteId)) as GeneratedRpcApi["tasks"]["getLinkedTasks"], + seedPerformanceTest: (() => invoke("tasks:seed-performance-test")) as GeneratedRpcApi["tasks"]["seedPerformanceTest"], + seedDemo: (() => invoke("tasks:seed-demo")) as GeneratedRpcApi["tasks"]["seedDemo"], }, inbox: { - captureText: ((input) => - invoke('inbox:capture-text', input)) as GeneratedRpcApi['inbox']['captureText'], - captureLink: ((input) => - invoke('inbox:capture-link', input)) as GeneratedRpcApi['inbox']['captureLink'], - previewLink: ((url) => - invoke('inbox:preview-link', url)) as GeneratedRpcApi['inbox']['previewLink'], - captureImage: ((input) => - invoke('inbox:capture-image', input)) as GeneratedRpcApi['inbox']['captureImage'], - captureVoice: ((input) => - invoke('inbox:capture-voice', input)) as GeneratedRpcApi['inbox']['captureVoice'], - captureClip: ((input) => - invoke('inbox:capture-clip', input)) as GeneratedRpcApi['inbox']['captureClip'], - capturePdf: ((input) => - invoke('inbox:capture-pdf', input)) as GeneratedRpcApi['inbox']['capturePdf'], - get: ((id) => invoke('inbox:get', id)) as GeneratedRpcApi['inbox']['get'], - list: ((options) => invoke('inbox:list', options ?? {})) as GeneratedRpcApi['inbox']['list'], - update: ((input) => invoke('inbox:update', input)) as GeneratedRpcApi['inbox']['update'], - archive: ((id) => invoke('inbox:archive', id)) as GeneratedRpcApi['inbox']['archive'], - file: ((input) => invoke('inbox:file', input)) as GeneratedRpcApi['inbox']['file'], - getSuggestions: ((itemId) => - invoke('inbox:get-suggestions', itemId)) as GeneratedRpcApi['inbox']['getSuggestions'], + captureText: ((input) => invoke("inbox:capture-text", input)) as GeneratedRpcApi["inbox"]["captureText"], + captureLink: ((input) => invoke("inbox:capture-link", input)) as GeneratedRpcApi["inbox"]["captureLink"], + previewLink: ((url) => invoke("inbox:preview-link", url)) as GeneratedRpcApi["inbox"]["previewLink"], + captureImage: ((input) => invoke("inbox:capture-image", input)) as GeneratedRpcApi["inbox"]["captureImage"], + captureVoice: ((input) => invoke("inbox:capture-voice", input)) as GeneratedRpcApi["inbox"]["captureVoice"], + captureClip: ((input) => invoke("inbox:capture-clip", input)) as GeneratedRpcApi["inbox"]["captureClip"], + capturePdf: ((input) => invoke("inbox:capture-pdf", input)) as GeneratedRpcApi["inbox"]["capturePdf"], + get: ((id) => invoke("inbox:get", id)) as GeneratedRpcApi["inbox"]["get"], + list: ((options) => invoke("inbox:list", options ?? {})) as GeneratedRpcApi["inbox"]["list"], + update: ((input) => invoke("inbox:update", input)) as GeneratedRpcApi["inbox"]["update"], + archive: ((id) => invoke("inbox:archive", id)) as GeneratedRpcApi["inbox"]["archive"], + file: ((input) => invoke("inbox:file", input)) as GeneratedRpcApi["inbox"]["file"], + getSuggestions: ((itemId) => invoke("inbox:get-suggestions", itemId)) as GeneratedRpcApi["inbox"]["getSuggestions"], trackSuggestion: ((input) => invoke( - 'inbox:track-suggestion', + "inbox:track-suggestion", input.itemId, input.itemType, input.suggestedTo, @@ -282,133 +140,59 @@ export function createGeneratedRpcApi({ input.confidence, input.suggestedTags ?? [], input.actualTags ?? [] - )) as GeneratedRpcApi['inbox']['trackSuggestion'], - convertToNote: ((itemId) => - invoke('inbox:convert-to-note', itemId)) as GeneratedRpcApi['inbox']['convertToNote'], - convertToTask: ((itemId) => - invoke('inbox:convert-to-task', itemId)) as GeneratedRpcApi['inbox']['convertToTask'], - linkToNote: ((itemId, noteId, tags) => - invoke( - 'inbox:link-to-note', - itemId, - noteId, - tags ?? [] - )) as GeneratedRpcApi['inbox']['linkToNote'], - addTag: ((itemId, tag) => - invoke('inbox:add-tag', itemId, tag)) as GeneratedRpcApi['inbox']['addTag'], - removeTag: ((itemId, tag) => - invoke('inbox:remove-tag', itemId, tag)) as GeneratedRpcApi['inbox']['removeTag'], - getTags: (() => invoke('inbox:get-tags')) as GeneratedRpcApi['inbox']['getTags'], - snooze: ((input) => invoke('inbox:snooze', input)) as GeneratedRpcApi['inbox']['snooze'], - unsnooze: ((itemId) => - invoke('inbox:unsnooze', itemId)) as GeneratedRpcApi['inbox']['unsnooze'], - getSnoozed: (() => invoke('inbox:get-snoozed')) as GeneratedRpcApi['inbox']['getSnoozed'], - markViewed: ((itemId) => - invoke('inbox:mark-viewed', itemId)) as GeneratedRpcApi['inbox']['markViewed'], - bulkFile: ((input) => - invoke('inbox:bulk-file', input)) as GeneratedRpcApi['inbox']['bulkFile'], - bulkArchive: ((input) => - invoke('inbox:bulk-archive', input)) as GeneratedRpcApi['inbox']['bulkArchive'], - bulkTag: ((input) => invoke('inbox:bulk-tag', input)) as GeneratedRpcApi['inbox']['bulkTag'], - bulkSnooze: ((input) => - invoke('inbox:bulk-snooze', input)) as GeneratedRpcApi['inbox']['bulkSnooze'], - fileAllStale: (() => - invoke('inbox:file-all-stale')) as GeneratedRpcApi['inbox']['fileAllStale'], - retryTranscription: ((itemId) => - invoke( - 'inbox:retry-transcription', - itemId - )) as GeneratedRpcApi['inbox']['retryTranscription'], - retryMetadata: ((itemId) => - invoke('inbox:retry-metadata', itemId)) as GeneratedRpcApi['inbox']['retryMetadata'], - getStats: (() => invoke('inbox:get-stats')) as GeneratedRpcApi['inbox']['getStats'], - getJobs: ((options) => - invoke('inbox:get-jobs', options ?? {})) as GeneratedRpcApi['inbox']['getJobs'], - getPatterns: (() => invoke('inbox:get-patterns')) as GeneratedRpcApi['inbox']['getPatterns'], - getStaleThreshold: (() => - invoke('inbox:get-stale-threshold')) as GeneratedRpcApi['inbox']['getStaleThreshold'], - setStaleThreshold: ((days) => - invoke('inbox:set-stale-threshold', days)) as GeneratedRpcApi['inbox']['setStaleThreshold'], - listArchived: ((options) => - invoke('inbox:list-archived', options ?? {})) as GeneratedRpcApi['inbox']['listArchived'], - unarchive: ((id) => invoke('inbox:unarchive', id)) as GeneratedRpcApi['inbox']['unarchive'], - deletePermanent: ((id) => - invoke('inbox:delete-permanent', id)) as GeneratedRpcApi['inbox']['deletePermanent'], - getFilingHistory: ((options) => - invoke( - 'inbox:get-filing-history', - options ?? {} - )) as GeneratedRpcApi['inbox']['getFilingHistory'], - undoFile: ((id) => invoke('inbox:undo-file', id)) as GeneratedRpcApi['inbox']['undoFile'], - undoArchive: ((id) => - invoke('inbox:undo-archive', id)) as GeneratedRpcApi['inbox']['undoArchive'] + )) as GeneratedRpcApi["inbox"]["trackSuggestion"], + convertToNote: ((itemId) => invoke("inbox:convert-to-note", itemId)) as GeneratedRpcApi["inbox"]["convertToNote"], + convertToTask: ((itemId) => invoke("inbox:convert-to-task", itemId)) as GeneratedRpcApi["inbox"]["convertToTask"], + linkToNote: ((itemId, noteId, tags) => invoke("inbox:link-to-note", itemId, noteId, tags ?? [])) as GeneratedRpcApi["inbox"]["linkToNote"], + addTag: ((itemId, tag) => invoke("inbox:add-tag", itemId, tag)) as GeneratedRpcApi["inbox"]["addTag"], + removeTag: ((itemId, tag) => invoke("inbox:remove-tag", itemId, tag)) as GeneratedRpcApi["inbox"]["removeTag"], + getTags: (() => invoke("inbox:get-tags")) as GeneratedRpcApi["inbox"]["getTags"], + snooze: ((input) => invoke("inbox:snooze", input)) as GeneratedRpcApi["inbox"]["snooze"], + unsnooze: ((itemId) => invoke("inbox:unsnooze", itemId)) as GeneratedRpcApi["inbox"]["unsnooze"], + getSnoozed: (() => invoke("inbox:get-snoozed")) as GeneratedRpcApi["inbox"]["getSnoozed"], + markViewed: ((itemId) => invoke("inbox:mark-viewed", itemId)) as GeneratedRpcApi["inbox"]["markViewed"], + bulkFile: ((input) => invoke("inbox:bulk-file", input)) as GeneratedRpcApi["inbox"]["bulkFile"], + bulkArchive: ((input) => invoke("inbox:bulk-archive", input)) as GeneratedRpcApi["inbox"]["bulkArchive"], + bulkTag: ((input) => invoke("inbox:bulk-tag", input)) as GeneratedRpcApi["inbox"]["bulkTag"], + bulkSnooze: ((input) => invoke("inbox:bulk-snooze", input)) as GeneratedRpcApi["inbox"]["bulkSnooze"], + fileAllStale: (() => invoke("inbox:file-all-stale")) as GeneratedRpcApi["inbox"]["fileAllStale"], + retryTranscription: ((itemId) => invoke("inbox:retry-transcription", itemId)) as GeneratedRpcApi["inbox"]["retryTranscription"], + retryMetadata: ((itemId) => invoke("inbox:retry-metadata", itemId)) as GeneratedRpcApi["inbox"]["retryMetadata"], + getStats: (() => invoke("inbox:get-stats")) as GeneratedRpcApi["inbox"]["getStats"], + getJobs: ((options) => invoke("inbox:get-jobs", options ?? {})) as GeneratedRpcApi["inbox"]["getJobs"], + getPatterns: (() => invoke("inbox:get-patterns")) as GeneratedRpcApi["inbox"]["getPatterns"], + getStaleThreshold: (() => invoke("inbox:get-stale-threshold")) as GeneratedRpcApi["inbox"]["getStaleThreshold"], + setStaleThreshold: ((days) => invoke("inbox:set-stale-threshold", days)) as GeneratedRpcApi["inbox"]["setStaleThreshold"], + listArchived: ((options) => invoke("inbox:list-archived", options ?? {})) as GeneratedRpcApi["inbox"]["listArchived"], + unarchive: ((id) => invoke("inbox:unarchive", id)) as GeneratedRpcApi["inbox"]["unarchive"], + deletePermanent: ((id) => invoke("inbox:delete-permanent", id)) as GeneratedRpcApi["inbox"]["deletePermanent"], + getFilingHistory: ((options) => invoke("inbox:get-filing-history", options ?? {})) as GeneratedRpcApi["inbox"]["getFilingHistory"], + undoFile: ((id) => invoke("inbox:undo-file", id)) as GeneratedRpcApi["inbox"]["undoFile"], + undoArchive: ((id) => invoke("inbox:undo-archive", id)) as GeneratedRpcApi["inbox"]["undoArchive"], }, settings: { - get: ((key) => invoke('settings:get', key)) as GeneratedRpcApi['settings']['get'], - set: ((key, value) => - invoke('settings:set', { key, value })) as GeneratedRpcApi['settings']['set'], - getJournalSettings: (() => - invoke('settings:getJournalSettings')) as GeneratedRpcApi['settings']['getJournalSettings'], - setJournalSettings: ((settings) => - invoke( - 'settings:setJournalSettings', - settings - )) as GeneratedRpcApi['settings']['setJournalSettings'], - getAISettings: (() => - invoke('settings:getAISettings')) as GeneratedRpcApi['settings']['getAISettings'], - setAISettings: ((settings) => - invoke('settings:setAISettings', settings)) as GeneratedRpcApi['settings']['setAISettings'], - getVoiceTranscriptionSettings: (() => - invoke( - 'settings:getVoiceTranscriptionSettings' - )) as GeneratedRpcApi['settings']['getVoiceTranscriptionSettings'], - setVoiceTranscriptionSettings: ((settings) => - invoke( - 'settings:setVoiceTranscriptionSettings', - settings - )) as GeneratedRpcApi['settings']['setVoiceTranscriptionSettings'], - getVoiceModelStatus: (() => - invoke( - 'settings:getVoiceModelStatus' - )) as GeneratedRpcApi['settings']['getVoiceModelStatus'], - downloadVoiceModel: (() => - invoke('settings:downloadVoiceModel')) as GeneratedRpcApi['settings']['downloadVoiceModel'], - getVoiceRecordingReadiness: (() => - invoke( - 'settings:getVoiceRecordingReadiness' - )) as GeneratedRpcApi['settings']['getVoiceRecordingReadiness'], - getVoiceTranscriptionOpenAIKeyStatus: (() => - invoke( - 'settings:getVoiceTranscriptionOpenAIKeyStatus' - )) as GeneratedRpcApi['settings']['getVoiceTranscriptionOpenAIKeyStatus'], - setVoiceTranscriptionOpenAIKey: ((apiKey) => - invoke('settings:setVoiceTranscriptionOpenAIKey', { - apiKey - })) as GeneratedRpcApi['settings']['setVoiceTranscriptionOpenAIKey'], - getAIModelStatus: (() => - invoke('settings:getAIModelStatus')) as GeneratedRpcApi['settings']['getAIModelStatus'], - loadAIModel: (() => - invoke('settings:loadAIModel')) as GeneratedRpcApi['settings']['loadAIModel'], - reindexEmbeddings: (() => - invoke('settings:reindexEmbeddings')) as GeneratedRpcApi['settings']['reindexEmbeddings'], - getTabSettings: (() => - invoke('settings:getTabSettings')) as GeneratedRpcApi['settings']['getTabSettings'], - setTabSettings: ((settings) => - invoke( - 'settings:setTabSettings', - settings - )) as GeneratedRpcApi['settings']['setTabSettings'], - getNoteEditorSettings: (() => - invoke( - 'settings:getNoteEditorSettings' - )) as GeneratedRpcApi['settings']['getNoteEditorSettings'], - setNoteEditorSettings: ((settings) => - invoke( - 'settings:setNoteEditorSettings', - settings - )) as GeneratedRpcApi['settings']['setNoteEditorSettings'], + get: ((key) => invoke("settings:get", key)) as GeneratedRpcApi["settings"]["get"], + set: ((key, value) => invoke("settings:set", { key, value })) as GeneratedRpcApi["settings"]["set"], + getJournalSettings: (() => invoke("settings:getJournalSettings")) as GeneratedRpcApi["settings"]["getJournalSettings"], + setJournalSettings: ((settings) => invoke("settings:setJournalSettings", settings)) as GeneratedRpcApi["settings"]["setJournalSettings"], + getAISettings: (() => invoke("settings:getAISettings")) as GeneratedRpcApi["settings"]["getAISettings"], + setAISettings: ((settings) => invoke("settings:setAISettings", settings)) as GeneratedRpcApi["settings"]["setAISettings"], + getVoiceTranscriptionSettings: (() => invoke("settings:getVoiceTranscriptionSettings")) as GeneratedRpcApi["settings"]["getVoiceTranscriptionSettings"], + setVoiceTranscriptionSettings: ((settings) => invoke("settings:setVoiceTranscriptionSettings", settings)) as GeneratedRpcApi["settings"]["setVoiceTranscriptionSettings"], + getVoiceModelStatus: (() => invoke("settings:getVoiceModelStatus")) as GeneratedRpcApi["settings"]["getVoiceModelStatus"], + downloadVoiceModel: (() => invoke("settings:downloadVoiceModel")) as GeneratedRpcApi["settings"]["downloadVoiceModel"], + getVoiceRecordingReadiness: (() => invoke("settings:getVoiceRecordingReadiness")) as GeneratedRpcApi["settings"]["getVoiceRecordingReadiness"], + getVoiceTranscriptionOpenAIKeyStatus: (() => invoke("settings:getVoiceTranscriptionOpenAIKeyStatus")) as GeneratedRpcApi["settings"]["getVoiceTranscriptionOpenAIKeyStatus"], + setVoiceTranscriptionOpenAIKey: ((apiKey) => invoke("settings:setVoiceTranscriptionOpenAIKey", { apiKey })) as GeneratedRpcApi["settings"]["setVoiceTranscriptionOpenAIKey"], + getAIModelStatus: (() => invoke("settings:getAIModelStatus")) as GeneratedRpcApi["settings"]["getAIModelStatus"], + loadAIModel: (() => invoke("settings:loadAIModel")) as GeneratedRpcApi["settings"]["loadAIModel"], + reindexEmbeddings: (() => invoke("settings:reindexEmbeddings")) as GeneratedRpcApi["settings"]["reindexEmbeddings"], + getTabSettings: (() => invoke("settings:getTabSettings")) as GeneratedRpcApi["settings"]["getTabSettings"], + setTabSettings: ((settings) => invoke("settings:setTabSettings", settings)) as GeneratedRpcApi["settings"]["setTabSettings"], + getNoteEditorSettings: (() => invoke("settings:getNoteEditorSettings")) as GeneratedRpcApi["settings"]["getNoteEditorSettings"], + setNoteEditorSettings: ((settings) => invoke("settings:setNoteEditorSettings", settings)) as GeneratedRpcApi["settings"]["setNoteEditorSettings"], getStartupThemeSync: (() => { - const raw = invokeSync('settings:getStartupThemeSync') as + const raw = invokeSync("settings:getStartupThemeSync") as | 'light' | 'dark' | 'white' @@ -416,134 +200,53 @@ export function createGeneratedRpcApi({ | { theme?: 'light' | 'dark' | 'white' | 'system' } | null | undefined - return typeof raw === 'string' ? raw : (raw?.theme ?? 'system') - }) as GeneratedRpcApi['settings']['getStartupThemeSync'], - getGeneralSettings: (() => - invoke('settings:getGeneralSettings')) as GeneratedRpcApi['settings']['getGeneralSettings'], - setGeneralSettings: ((settings) => - invoke( - 'settings:setGeneralSettings', - settings - )) as GeneratedRpcApi['settings']['setGeneralSettings'], - getEditorSettings: (() => - invoke('settings:getEditorSettings')) as GeneratedRpcApi['settings']['getEditorSettings'], - setEditorSettings: ((settings) => - invoke( - 'settings:setEditorSettings', - settings - )) as GeneratedRpcApi['settings']['setEditorSettings'], - getTaskSettings: (() => - invoke('settings:getTaskSettings')) as GeneratedRpcApi['settings']['getTaskSettings'], - setTaskSettings: ((settings) => - invoke( - 'settings:setTaskSettings', - settings - )) as GeneratedRpcApi['settings']['setTaskSettings'], - getKeyboardSettings: (() => - invoke( - 'settings:getKeyboardSettings' - )) as GeneratedRpcApi['settings']['getKeyboardSettings'], - setKeyboardSettings: ((settings) => - invoke( - 'settings:setKeyboardSettings', - settings - )) as GeneratedRpcApi['settings']['setKeyboardSettings'], - resetKeyboardSettings: (() => - invoke( - 'settings:resetKeyboardSettings' - )) as GeneratedRpcApi['settings']['resetKeyboardSettings'], - getSyncSettings: (() => - invoke('settings:getSyncSettings')) as GeneratedRpcApi['settings']['getSyncSettings'], - setSyncSettings: ((settings) => - invoke( - 'settings:setSyncSettings', - settings - )) as GeneratedRpcApi['settings']['setSyncSettings'], - getBackupSettings: (() => - invoke('settings:getBackupSettings')) as GeneratedRpcApi['settings']['getBackupSettings'], - setBackupSettings: ((settings) => - invoke( - 'settings:setBackupSettings', - settings - )) as GeneratedRpcApi['settings']['setBackupSettings'], - getGraphSettings: (() => - invoke('settings:getGraphSettings')) as GeneratedRpcApi['settings']['getGraphSettings'], - setGraphSettings: ((settings) => - invoke( - 'settings:setGraphSettings', - settings - )) as GeneratedRpcApi['settings']['setGraphSettings'], - registerGlobalCapture: (() => - invoke( - 'settings:registerGlobalCapture' - )) as GeneratedRpcApi['settings']['registerGlobalCapture'] + return typeof raw === 'string' ? raw : raw?.theme ?? 'system' + }) as GeneratedRpcApi["settings"]["getStartupThemeSync"], + getGeneralSettings: (() => invoke("settings:getGeneralSettings")) as GeneratedRpcApi["settings"]["getGeneralSettings"], + setGeneralSettings: ((settings) => invoke("settings:setGeneralSettings", settings)) as GeneratedRpcApi["settings"]["setGeneralSettings"], + getEditorSettings: (() => invoke("settings:getEditorSettings")) as GeneratedRpcApi["settings"]["getEditorSettings"], + setEditorSettings: ((settings) => invoke("settings:setEditorSettings", settings)) as GeneratedRpcApi["settings"]["setEditorSettings"], + getTaskSettings: (() => invoke("settings:getTaskSettings")) as GeneratedRpcApi["settings"]["getTaskSettings"], + setTaskSettings: ((settings) => invoke("settings:setTaskSettings", settings)) as GeneratedRpcApi["settings"]["setTaskSettings"], + getKeyboardSettings: (() => invoke("settings:getKeyboardSettings")) as GeneratedRpcApi["settings"]["getKeyboardSettings"], + setKeyboardSettings: ((settings) => invoke("settings:setKeyboardSettings", settings)) as GeneratedRpcApi["settings"]["setKeyboardSettings"], + resetKeyboardSettings: (() => invoke("settings:resetKeyboardSettings")) as GeneratedRpcApi["settings"]["resetKeyboardSettings"], + getSyncSettings: (() => invoke("settings:getSyncSettings")) as GeneratedRpcApi["settings"]["getSyncSettings"], + setSyncSettings: ((settings) => invoke("settings:setSyncSettings", settings)) as GeneratedRpcApi["settings"]["setSyncSettings"], + getBackupSettings: (() => invoke("settings:getBackupSettings")) as GeneratedRpcApi["settings"]["getBackupSettings"], + setBackupSettings: ((settings) => invoke("settings:setBackupSettings", settings)) as GeneratedRpcApi["settings"]["setBackupSettings"], + getGraphSettings: (() => invoke("settings:getGraphSettings")) as GeneratedRpcApi["settings"]["getGraphSettings"], + setGraphSettings: ((settings) => invoke("settings:setGraphSettings", settings)) as GeneratedRpcApi["settings"]["setGraphSettings"], + registerGlobalCapture: (() => invoke("settings:registerGlobalCapture")) as GeneratedRpcApi["settings"]["registerGlobalCapture"], }, - onNoteCreated: ((callback) => - subscribe('notes:created', callback)) as GeneratedRpcApi['onNoteCreated'], - onNoteUpdated: ((callback) => - subscribe('notes:updated', callback)) as GeneratedRpcApi['onNoteUpdated'], - onNoteDeleted: ((callback) => - subscribe('notes:deleted', callback)) as GeneratedRpcApi['onNoteDeleted'], - onNoteRenamed: ((callback) => - subscribe('notes:renamed', callback)) as GeneratedRpcApi['onNoteRenamed'], - onNoteMoved: ((callback) => - subscribe('notes:moved', callback)) as GeneratedRpcApi['onNoteMoved'], - onNoteExternalChange: ((callback) => - subscribe('notes:external-change', callback)) as GeneratedRpcApi['onNoteExternalChange'], - onTagsChanged: ((callback) => - subscribe('notes:tags-changed', callback)) as GeneratedRpcApi['onTagsChanged'], - onFolderConfigUpdated: ((callback) => - subscribe( - 'notes:folder-config-updated', - callback - )) as GeneratedRpcApi['onFolderConfigUpdated'], - onTaskCreated: ((callback) => - subscribe('tasks:created', callback)) as GeneratedRpcApi['onTaskCreated'], - onTaskUpdated: ((callback) => - subscribe('tasks:updated', callback)) as GeneratedRpcApi['onTaskUpdated'], - onTaskDeleted: ((callback) => - subscribe('tasks:deleted', callback)) as GeneratedRpcApi['onTaskDeleted'], - onTaskCompleted: ((callback) => - subscribe('tasks:completed', callback)) as GeneratedRpcApi['onTaskCompleted'], - onTaskMoved: ((callback) => - subscribe('tasks:moved', callback)) as GeneratedRpcApi['onTaskMoved'], - onProjectCreated: ((callback) => - subscribe('tasks:project-created', callback)) as GeneratedRpcApi['onProjectCreated'], - onProjectUpdated: ((callback) => - subscribe('tasks:project-updated', callback)) as GeneratedRpcApi['onProjectUpdated'], - onProjectDeleted: ((callback) => - subscribe('tasks:project-deleted', callback)) as GeneratedRpcApi['onProjectDeleted'], - onInboxCaptured: ((callback) => - subscribe('inbox:captured', callback)) as GeneratedRpcApi['onInboxCaptured'], - onInboxUpdated: ((callback) => - subscribe('inbox:updated', callback)) as GeneratedRpcApi['onInboxUpdated'], - onInboxArchived: ((callback) => - subscribe('inbox:archived', callback)) as GeneratedRpcApi['onInboxArchived'], - onInboxFiled: ((callback) => - subscribe('inbox:filed', callback)) as GeneratedRpcApi['onInboxFiled'], - onInboxSnoozed: ((callback) => - subscribe('inbox:snoozed', callback)) as GeneratedRpcApi['onInboxSnoozed'], - onInboxSnoozeDue: ((callback) => - subscribe('inbox:snooze-due', callback)) as GeneratedRpcApi['onInboxSnoozeDue'], - onInboxTranscriptionComplete: ((callback) => - subscribe( - 'inbox:transcription-complete', - callback - )) as GeneratedRpcApi['onInboxTranscriptionComplete'], - onInboxMetadataComplete: ((callback) => - subscribe('inbox:metadata-complete', callback)) as GeneratedRpcApi['onInboxMetadataComplete'], - onInboxProcessingError: ((callback) => - subscribe('inbox:processing-error', callback)) as GeneratedRpcApi['onInboxProcessingError'], - onSettingsChanged: ((callback) => - subscribe('settings:changed', callback)) as GeneratedRpcApi['onSettingsChanged'], - onEmbeddingProgress: ((callback) => - subscribe('settings:embeddingProgress', callback)) as GeneratedRpcApi['onEmbeddingProgress'], - onVoiceModelProgress: ((callback) => - subscribe( - 'settings:voiceModelProgress', - callback - )) as GeneratedRpcApi['onVoiceModelProgress'], - onSettingsOpenRequested: ((callback) => - subscribe('settings:openSection', callback)) as GeneratedRpcApi['onSettingsOpenRequested'] + onNoteCreated: ((callback) => subscribe("notes:created", callback)) as GeneratedRpcApi["onNoteCreated"], + onNoteUpdated: ((callback) => subscribe("notes:updated", callback)) as GeneratedRpcApi["onNoteUpdated"], + onNoteDeleted: ((callback) => subscribe("notes:deleted", callback)) as GeneratedRpcApi["onNoteDeleted"], + onNoteRenamed: ((callback) => subscribe("notes:renamed", callback)) as GeneratedRpcApi["onNoteRenamed"], + onNoteMoved: ((callback) => subscribe("notes:moved", callback)) as GeneratedRpcApi["onNoteMoved"], + onNoteExternalChange: ((callback) => subscribe("notes:external-change", callback)) as GeneratedRpcApi["onNoteExternalChange"], + onTagsChanged: ((callback) => subscribe("notes:tags-changed", callback)) as GeneratedRpcApi["onTagsChanged"], + onFolderConfigUpdated: ((callback) => subscribe("notes:folder-config-updated", callback)) as GeneratedRpcApi["onFolderConfigUpdated"], + onTaskCreated: ((callback) => subscribe("tasks:created", callback)) as GeneratedRpcApi["onTaskCreated"], + onTaskUpdated: ((callback) => subscribe("tasks:updated", callback)) as GeneratedRpcApi["onTaskUpdated"], + onTaskDeleted: ((callback) => subscribe("tasks:deleted", callback)) as GeneratedRpcApi["onTaskDeleted"], + onTaskCompleted: ((callback) => subscribe("tasks:completed", callback)) as GeneratedRpcApi["onTaskCompleted"], + onTaskMoved: ((callback) => subscribe("tasks:moved", callback)) as GeneratedRpcApi["onTaskMoved"], + onProjectCreated: ((callback) => subscribe("tasks:project-created", callback)) as GeneratedRpcApi["onProjectCreated"], + onProjectUpdated: ((callback) => subscribe("tasks:project-updated", callback)) as GeneratedRpcApi["onProjectUpdated"], + onProjectDeleted: ((callback) => subscribe("tasks:project-deleted", callback)) as GeneratedRpcApi["onProjectDeleted"], + onInboxCaptured: ((callback) => subscribe("inbox:captured", callback)) as GeneratedRpcApi["onInboxCaptured"], + onInboxUpdated: ((callback) => subscribe("inbox:updated", callback)) as GeneratedRpcApi["onInboxUpdated"], + onInboxArchived: ((callback) => subscribe("inbox:archived", callback)) as GeneratedRpcApi["onInboxArchived"], + onInboxFiled: ((callback) => subscribe("inbox:filed", callback)) as GeneratedRpcApi["onInboxFiled"], + onInboxSnoozed: ((callback) => subscribe("inbox:snoozed", callback)) as GeneratedRpcApi["onInboxSnoozed"], + onInboxSnoozeDue: ((callback) => subscribe("inbox:snooze-due", callback)) as GeneratedRpcApi["onInboxSnoozeDue"], + onInboxTranscriptionComplete: ((callback) => subscribe("inbox:transcription-complete", callback)) as GeneratedRpcApi["onInboxTranscriptionComplete"], + onInboxMetadataComplete: ((callback) => subscribe("inbox:metadata-complete", callback)) as GeneratedRpcApi["onInboxMetadataComplete"], + onInboxProcessingError: ((callback) => subscribe("inbox:processing-error", callback)) as GeneratedRpcApi["onInboxProcessingError"], + onSettingsChanged: ((callback) => subscribe("settings:changed", callback)) as GeneratedRpcApi["onSettingsChanged"], + onEmbeddingProgress: ((callback) => subscribe("settings:embeddingProgress", callback)) as GeneratedRpcApi["onEmbeddingProgress"], + onVoiceModelProgress: ((callback) => subscribe("settings:voiceModelProgress", callback)) as GeneratedRpcApi["onVoiceModelProgress"], + onSettingsOpenRequested: ((callback) => subscribe("settings:openSection", callback)) as GeneratedRpcApi["onSettingsOpenRequested"], } } diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 3b224d27d..9e7aa8e68 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -34,8 +34,11 @@ import { useNewNoteShortcut, useUndoKeyboardShortcut, useReminderNotifications, - useSearchShortcut + useSearchShortcut, + useHintActivation } from '@/hooks' +import { HintModeProvider } from '@/contexts/hint-mode' +import { HintOverlay, HintIndicator } from '@/components/hint-overlay' import { CommandPalette } from '@/components/search/command-palette' import { SettingsModalProvider, useSettingsModal } from '@/contexts/settings-modal-context' import { SettingsModal } from '@/components/settings-modal' @@ -141,6 +144,7 @@ const AppContent = (): React.JSX.Element => { useFolderViewEvents() // Global cache invalidation for folder-view tabs const toggleSearch = useCallback(() => setSearchOpen((prev) => !prev), []) useSearchShortcut(toggleSearch) + useHintActivation() useEffect(() => { const openSearch = () => setSearchOpen(true) @@ -189,6 +193,10 @@ const AppContent = (): React.JSX.Element => { {/* Chord Indicator */} + {/* Hint Mode Overlay + Indicator */} + + + {/* Keyboard Shortcuts Dialog */} - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src/renderer/src/components/hint-overlay/hint-badge.tsx b/apps/desktop/src/renderer/src/components/hint-overlay/hint-badge.tsx new file mode 100644 index 000000000..cbf905253 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/hint-overlay/hint-badge.tsx @@ -0,0 +1,41 @@ +import type { HintTarget } from '@/contexts/hint-mode' + +interface HintBadgeProps { + hint: HintTarget + typedChars: string +} + +export const HintBadge = ({ hint, typedChars }: HintBadgeProps): React.JSX.Element => { + const isMatching = hint.label.startsWith(typedChars) + const matchedLength = typedChars.length + + return ( + + {hint.label.split('').map((char, i) => ( + + {char} + + ))} + + ) +} diff --git a/apps/desktop/src/renderer/src/components/hint-overlay/hint-indicator.tsx b/apps/desktop/src/renderer/src/components/hint-overlay/hint-indicator.tsx new file mode 100644 index 000000000..02decd39a --- /dev/null +++ b/apps/desktop/src/renderer/src/components/hint-overlay/hint-indicator.tsx @@ -0,0 +1,26 @@ +import { useHintModeContext } from '@/contexts/hint-mode' +import { cn } from '@/lib/utils' + +export const HintIndicator = (): React.JSX.Element | null => { + const { state } = useHintModeContext() + + if (!state.isActive) return null + + return ( +
+ HINT + {state.typedChars && ( + {state.typedChars} + )} +
+ ) +} diff --git a/apps/desktop/src/renderer/src/components/hint-overlay/hint-overlay.tsx b/apps/desktop/src/renderer/src/components/hint-overlay/hint-overlay.tsx new file mode 100644 index 000000000..4b7014685 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/hint-overlay/hint-overlay.tsx @@ -0,0 +1,28 @@ +import { createPortal } from 'react-dom' +import { useHintModeContext } from '@/contexts/hint-mode' +import { HINT_OVERLAY_ID } from '@/lib/dom-scanner' +import { HintBadge } from './hint-badge' + +export const HintOverlay = (): React.JSX.Element | null => { + const { state } = useHintModeContext() + + if (!state.isActive) return null + + return createPortal( + , + document.body + ) +} diff --git a/apps/desktop/src/renderer/src/components/hint-overlay/index.ts b/apps/desktop/src/renderer/src/components/hint-overlay/index.ts new file mode 100644 index 000000000..ad35d613e --- /dev/null +++ b/apps/desktop/src/renderer/src/components/hint-overlay/index.ts @@ -0,0 +1,3 @@ +export { HintOverlay } from './hint-overlay' +export { HintBadge } from './hint-badge' +export { HintIndicator } from './hint-indicator' diff --git a/apps/desktop/src/renderer/src/contexts/hint-mode/context.tsx b/apps/desktop/src/renderer/src/contexts/hint-mode/context.tsx new file mode 100644 index 000000000..53eb2ab3d --- /dev/null +++ b/apps/desktop/src/renderer/src/contexts/hint-mode/context.tsx @@ -0,0 +1,91 @@ +import { + createContext, + useContext, + useState, + useCallback, + useRef, + useEffect, + type ReactNode +} from 'react' +import type { HintModeState, HintModeContextType } from './types' +import { scanClickableElements } from '@/lib/dom-scanner' +import { assignLabels } from '@/lib/label-assigner' + +export const hintModeActiveRef: { current: boolean } = { current: false } + +const INITIAL_STATE: HintModeState = { + isActive: false, + hints: [], + typedChars: '' +} + +const HintModeContext = createContext(null) + +export const HintModeProvider = ({ children }: { children: ReactNode }): React.JSX.Element => { + const [state, setState] = useState(INITIAL_STATE) + const stateRef = useRef(state) + stateRef.current = state + + const deactivate = useCallback(() => { + hintModeActiveRef.current = false + setState(INITIAL_STATE) + }, []) + + const activate = useCallback(() => { + if (stateRef.current.isActive) { + deactivate() + return + } + + const elements = scanClickableElements() + if (elements.length === 0) return + + const hints = assignLabels(elements) + hintModeActiveRef.current = true + setState({ isActive: true, hints, typedChars: '' }) + }, [deactivate]) + + const typeChar = useCallback( + (char: string) => { + const upper = char.toUpperCase() + const next = stateRef.current.typedChars + upper + const matching = stateRef.current.hints.filter((h) => h.label.startsWith(next)) + + if (matching.length === 0) return + + if (matching.length === 1 && matching[0].label === next) { + const target = matching[0].element + target.click() + if (document.contains(target)) target.focus() + deactivate() + return + } + + setState((prev) => ({ ...prev, typedChars: next })) + }, + [deactivate] + ) + + const backspace = useCallback(() => { + setState((prev) => ({ + ...prev, + typedChars: prev.typedChars.slice(0, -1) + })) + }, []) + + useEffect(() => { + return () => { + hintModeActiveRef.current = false + } + }, []) + + const value: HintModeContextType = { state, activate, deactivate, typeChar, backspace } + + return {children} +} + +export const useHintModeContext = (): HintModeContextType => { + const ctx = useContext(HintModeContext) + if (!ctx) throw new Error('useHintModeContext must be inside HintModeProvider') + return ctx +} diff --git a/apps/desktop/src/renderer/src/contexts/hint-mode/index.ts b/apps/desktop/src/renderer/src/contexts/hint-mode/index.ts new file mode 100644 index 000000000..cc9f0cca3 --- /dev/null +++ b/apps/desktop/src/renderer/src/contexts/hint-mode/index.ts @@ -0,0 +1,2 @@ +export { HintModeProvider, useHintModeContext, hintModeActiveRef } from './context' +export type { HintTarget, HintModeState, HintModeContextType } from './types' diff --git a/apps/desktop/src/renderer/src/contexts/hint-mode/types.ts b/apps/desktop/src/renderer/src/contexts/hint-mode/types.ts new file mode 100644 index 000000000..b5dfd90d6 --- /dev/null +++ b/apps/desktop/src/renderer/src/contexts/hint-mode/types.ts @@ -0,0 +1,20 @@ +export interface HintTarget { + element: HTMLElement + label: string + rect: DOMRect + text: string +} + +export interface HintModeState { + isActive: boolean + hints: HintTarget[] + typedChars: string +} + +export interface HintModeContextType { + state: HintModeState + activate: () => void + deactivate: () => void + typeChar: (char: string) => void + backspace: () => void +} diff --git a/apps/desktop/src/renderer/src/hooks/index.ts b/apps/desktop/src/renderer/src/hooks/index.ts index d61ca3b49..6cca5506d 100644 --- a/apps/desktop/src/renderer/src/hooks/index.ts +++ b/apps/desktop/src/renderer/src/hooks/index.ts @@ -54,3 +54,6 @@ export * from './use-sync-status' // Search export * from './use-search-shortcut' export * from './use-search' + +// Hint mode +export * from './use-hint-activation' diff --git a/apps/desktop/src/renderer/src/hooks/use-chord-shortcuts.ts b/apps/desktop/src/renderer/src/hooks/use-chord-shortcuts.ts index 8fef16847..49b8a870a 100644 --- a/apps/desktop/src/renderer/src/hooks/use-chord-shortcuts.ts +++ b/apps/desktop/src/renderer/src/hooks/use-chord-shortcuts.ts @@ -7,6 +7,7 @@ import { useState, useEffect, useCallback } from 'react' import { useTabs } from '@/contexts/tabs' import { isMac } from './use-keyboard-shortcuts-base' import { calculateGroupPositions, type GroupPosition } from './use-pane-navigation' +import { hintModeActiveRef } from '@/contexts/hint-mode' // ============================================================================= // TYPES @@ -204,6 +205,8 @@ export const useChordShortcuts = (): boolean => { // Event listener for chord keys useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { + if (hintModeActiveRef.current) return + const metaOrCtrl = isMac ? e.metaKey : e.ctrlKey // Ignore if typing in input diff --git a/apps/desktop/src/renderer/src/hooks/use-hint-activation.test.tsx b/apps/desktop/src/renderer/src/hooks/use-hint-activation.test.tsx new file mode 100644 index 000000000..e433c7430 --- /dev/null +++ b/apps/desktop/src/renderer/src/hooks/use-hint-activation.test.tsx @@ -0,0 +1,138 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { type ReactNode } from 'react' +import { HintModeProvider, useHintModeContext } from '@/contexts/hint-mode' + +const wrapper = ({ children }: { children: ReactNode }): React.JSX.Element => ( + {children} +) + +const mockRect: DOMRect = { + x: 10, + y: 10, + width: 100, + height: 30, + top: 10, + left: 10, + bottom: 40, + right: 110, + toJSON: () => ({}) +} as DOMRect + +const addButton = (text: string): HTMLButtonElement => { + const btn = document.createElement('button') + btn.textContent = text + btn.getBoundingClientRect = () => mockRect + Object.defineProperty(btn, 'offsetParent', { value: document.body, configurable: true }) + document.body.appendChild(btn) + return btn +} + +describe('HintModeProvider', () => { + beforeEach(() => { + document.body.innerHTML = '' + }) + + afterEach(() => { + document.body.innerHTML = '' + }) + + it('activate scans DOM and assigns labels', () => { + addButton('Inbox') + addButton('Journal') + + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + + expect(result.current.state.isActive).toBe(true) + expect(result.current.state.hints).toHaveLength(2) + expect(result.current.state.hints[0].label).toBe('I') + expect(result.current.state.hints[1].label).toBe('J') + }) + + it('deactivate resets state', () => { + addButton('Test') + + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + expect(result.current.state.isActive).toBe(true) + + act(() => result.current.deactivate()) + expect(result.current.state.isActive).toBe(false) + expect(result.current.state.hints).toHaveLength(0) + }) + + it('typeChar narrows matches', () => { + addButton('Tags') + addButton('Tasks') + + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + expect(result.current.state.hints[0].label).toBe('TA') + expect(result.current.state.hints[1].label).toBe('TS') + + act(() => result.current.typeChar('T')) + expect(result.current.state.typedChars).toBe('T') + }) + + it('typeChar triggers click on unique match', () => { + const btn = addButton('Inbox') + const clickSpy = vi.spyOn(btn, 'click') + + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + act(() => result.current.typeChar('I')) + + expect(clickSpy).toHaveBeenCalledOnce() + expect(result.current.state.isActive).toBe(false) + }) + + it('backspace removes last typed char', () => { + addButton('Tags') + addButton('Tasks') + + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + act(() => result.current.typeChar('T')) + expect(result.current.state.typedChars).toBe('T') + + act(() => result.current.backspace()) + expect(result.current.state.typedChars).toBe('') + }) + + it('double activate toggles off', () => { + addButton('Test') + + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + expect(result.current.state.isActive).toBe(true) + + act(() => result.current.activate()) + expect(result.current.state.isActive).toBe(false) + }) + + it('activate with no clickable elements is a no-op', () => { + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + expect(result.current.state.isActive).toBe(false) + }) + + it('ignores non-matching typeChar', () => { + addButton('Inbox') + + const { result } = renderHook(() => useHintModeContext(), { wrapper }) + + act(() => result.current.activate()) + act(() => result.current.typeChar('Z')) + + expect(result.current.state.typedChars).toBe('') + expect(result.current.state.isActive).toBe(true) + }) +}) diff --git a/apps/desktop/src/renderer/src/hooks/use-hint-activation.ts b/apps/desktop/src/renderer/src/hooks/use-hint-activation.ts new file mode 100644 index 000000000..f3f2187de --- /dev/null +++ b/apps/desktop/src/renderer/src/hooks/use-hint-activation.ts @@ -0,0 +1,61 @@ +import { useEffect } from 'react' +import { useHintModeContext } from '@/contexts/hint-mode' +import { isInputFocused } from '@/hooks/use-keyboard-shortcuts' + +export const useHintActivation = (): void => { + const { state, activate, deactivate, typeChar, backspace } = useHintModeContext() + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.isComposing || e.keyCode === 229) return + + if (state.isActive) { + if (e.key === 'Escape') { + e.preventDefault() + e.stopPropagation() + deactivate() + return + } + + if (e.key === 'Backspace') { + e.preventDefault() + e.stopPropagation() + backspace() + return + } + + if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) { + e.preventDefault() + e.stopPropagation() + typeChar(e.key) + return + } + + return + } + + if (e.code === 'KeyF' && e.altKey && !e.metaKey && !e.ctrlKey) { + e.preventDefault() + e.stopPropagation() + activate() + return + } + + if (e.code === 'KeyF' && !e.altKey && !e.metaKey && !e.ctrlKey && !e.shiftKey) { + if (!isInputFocused()) { + e.preventDefault() + e.stopPropagation() + activate() + return + } + } + + if (e.key === 'Escape' && isInputFocused()) { + ;(document.activeElement as HTMLElement)?.blur() + } + } + + window.addEventListener('keydown', handleKeyDown, { capture: true }) + return () => window.removeEventListener('keydown', handleKeyDown, { capture: true }) + }, [state.isActive, activate, deactivate, typeChar, backspace]) +} diff --git a/apps/desktop/src/renderer/src/hooks/use-keyboard-shortcuts-base.ts b/apps/desktop/src/renderer/src/hooks/use-keyboard-shortcuts-base.ts index d34adebb4..d7c6bedb6 100644 --- a/apps/desktop/src/renderer/src/hooks/use-keyboard-shortcuts-base.ts +++ b/apps/desktop/src/renderer/src/hooks/use-keyboard-shortcuts-base.ts @@ -4,6 +4,7 @@ */ import { useEffect, useCallback, useMemo } from 'react' +import { hintModeActiveRef } from '@/contexts/hint-mode' // ============================================================================= // TYPES @@ -71,6 +72,8 @@ export const useKeyboardShortcuts = (shortcuts: KeyboardShortcut[]): void => { const handleKeyDown = useCallback( (e: KeyboardEvent) => { + if (hintModeActiveRef.current) return + const target = e.target as HTMLElement // Check if typing in input/textarea diff --git a/apps/desktop/src/renderer/src/lib/dom-scanner.test.ts b/apps/desktop/src/renderer/src/lib/dom-scanner.test.ts new file mode 100644 index 000000000..71eac8ed0 --- /dev/null +++ b/apps/desktop/src/renderer/src/lib/dom-scanner.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { scanClickableElements } from './dom-scanner' + +const HINT_OVERLAY_ID = 'hint-mode-overlay' + +const mockRect = (el: HTMLElement, rect: Partial = {}): void => { + el.getBoundingClientRect = () => + ({ + x: 0, + y: 0, + width: 100, + height: 30, + top: 0, + left: 0, + bottom: 30, + right: 100, + toJSON: () => ({}), + ...rect + }) as DOMRect +} + +const mockOffsetParent = (el: HTMLElement, parent: Element | null = document.body): void => { + Object.defineProperty(el, 'offsetParent', { value: parent, configurable: true }) +} + +const createEl = (tag: string, attrs: Record = {}, text = ''): HTMLElement => { + const el = document.createElement(tag) + for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v) + if (text) el.textContent = text + document.body.appendChild(el) + mockRect(el) + mockOffsetParent(el) + return el +} + +describe('scanClickableElements', () => { + beforeEach(() => { + document.body.innerHTML = '' + }) + + afterEach(() => { + document.body.innerHTML = '' + }) + + it('finds buttons', () => { + createEl('button', {}, 'Click me') + const results = scanClickableElements() + expect(results).toHaveLength(1) + expect(results[0].tagName).toBe('BUTTON') + }) + + it('finds anchor links', () => { + createEl('a', { href: '/test' }, 'Link') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('finds role=button elements', () => { + createEl('div', { role: 'button' }, 'Fake button') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('finds role=tab elements', () => { + createEl('div', { role: 'tab' }, 'Tab') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('finds role=treeitem elements', () => { + createEl('div', { role: 'treeitem' }, 'Tree item') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('finds role=menuitem elements', () => { + createEl('div', { role: 'menuitem' }, 'Menu item') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('finds role=option elements', () => { + createEl('div', { role: 'option' }, 'Option') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('finds tabindex elements (not -1)', () => { + createEl('div', { tabindex: '0' }, 'Focusable') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('excludes tabindex=-1', () => { + createEl('div', { tabindex: '-1' }, 'Not focusable') + const results = scanClickableElements() + expect(results).toHaveLength(0) + }) + + it('finds data-hint elements', () => { + createEl('span', { 'data-hint': '' }, 'Custom hint') + const results = scanClickableElements() + expect(results).toHaveLength(1) + }) + + it('excludes disabled elements', () => { + createEl('button', { disabled: '' }, 'Disabled') + const results = scanClickableElements() + expect(results).toHaveLength(0) + }) + + it('excludes aria-disabled elements', () => { + createEl('button', { 'aria-disabled': 'true' }, 'Disabled') + const results = scanClickableElements() + expect(results).toHaveLength(0) + }) + + it('excludes elements inside hint overlay', () => { + const overlay = document.createElement('div') + overlay.id = HINT_OVERLAY_ID + const btn = document.createElement('button') + btn.textContent = 'Inside overlay' + mockRect(btn) + mockOffsetParent(btn, overlay) + overlay.appendChild(btn) + document.body.appendChild(overlay) + + const results = scanClickableElements() + expect(results).toHaveLength(0) + }) + + it('returns empty array when no clickable elements exist', () => { + createEl('div', {}, 'Plain text') + const results = scanClickableElements() + expect(results).toHaveLength(0) + }) +}) diff --git a/apps/desktop/src/renderer/src/lib/dom-scanner.ts b/apps/desktop/src/renderer/src/lib/dom-scanner.ts new file mode 100644 index 000000000..d58190e4f --- /dev/null +++ b/apps/desktop/src/renderer/src/lib/dom-scanner.ts @@ -0,0 +1,61 @@ +const CLICKABLE_SELECTOR = [ + 'button', + 'a[href]', + '[role="button"]', + '[role="tab"]', + '[role="treeitem"]', + '[role="menuitem"]', + '[role="option"]', + '[tabindex]:not([tabindex="-1"])', + '[data-hint]' +].join(', ') + +const HINT_OVERLAY_ID = 'hint-mode-overlay' + +const MIN_SIZE = 8 + +const isVisible = (el: HTMLElement): boolean => { + if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') return false + if (getComputedStyle(el).pointerEvents === 'none') return false + return true +} + +const isInViewport = (rect: DOMRect): boolean => { + return ( + rect.width >= MIN_SIZE && + rect.height >= MIN_SIZE && + rect.bottom > 0 && + rect.right > 0 && + rect.top < window.innerHeight && + rect.left < window.innerWidth + ) +} + +const isEnabled = (el: HTMLElement): boolean => { + if (el.hasAttribute('disabled')) return false + if (el.getAttribute('aria-disabled') === 'true') return false + return true +} + +const isInsideOverlay = (el: HTMLElement): boolean => { + const overlay = document.getElementById(HINT_OVERLAY_ID) + return overlay !== null && overlay.contains(el) +} + +export const scanClickableElements = (): HTMLElement[] => { + const candidates = document.querySelectorAll(CLICKABLE_SELECTOR) + const results: HTMLElement[] = [] + + for (const el of candidates) { + if (!isEnabled(el)) continue + if (isInsideOverlay(el)) continue + if (!isVisible(el)) continue + const rect = el.getBoundingClientRect() + if (!isInViewport(rect)) continue + results.push(el) + } + + return results +} + +export { HINT_OVERLAY_ID } diff --git a/apps/desktop/src/renderer/src/lib/label-assigner.test.ts b/apps/desktop/src/renderer/src/lib/label-assigner.test.ts new file mode 100644 index 000000000..3a5f8234b --- /dev/null +++ b/apps/desktop/src/renderer/src/lib/label-assigner.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect } from 'vitest' +import { assignLabels, extractElementText } from './label-assigner' + +const mockRect = (el: HTMLElement): void => { + el.getBoundingClientRect = () => + ({ + x: 0, + y: 0, + width: 100, + height: 30, + top: 0, + left: 0, + bottom: 30, + right: 100, + toJSON: () => ({}) + }) as DOMRect +} + +const mockElement = (text: string, ariaLabel?: string): HTMLElement => { + const el = document.createElement('button') + el.textContent = text + if (ariaLabel) el.setAttribute('aria-label', ariaLabel) + mockRect(el) + return el +} + +describe('extractElementText', () => { + it('returns textContent trimmed', () => { + const el = mockElement(' Hello World ') + expect(extractElementText(el)).toBe('Hello World') + }) + + it('prefers aria-label over textContent', () => { + const el = mockElement('X', 'Close dialog') + expect(extractElementText(el)).toBe('Close dialog') + }) + + it('falls back to title attribute', () => { + const el = document.createElement('button') + el.setAttribute('title', 'Settings') + expect(extractElementText(el)).toBe('Settings') + }) + + it('returns empty string when no text found', () => { + const el = document.createElement('button') + expect(extractElementText(el)).toBe('') + }) +}) + +describe('assignLabels', () => { + it('assigns single-char mnemonic for unique first letters', () => { + const elements = [mockElement('Inbox'), mockElement('Journal'), mockElement('Tasks')] + const hints = assignLabels(elements) + expect(hints.map((h) => h.label)).toEqual(['I', 'J', 'T']) + }) + + it('assigns two-char labels when first letters conflict', () => { + const elements = [mockElement('Tags'), mockElement('Tasks')] + const hints = assignLabels(elements) + expect(hints[0].label).toBe('TA') + expect(hints[1].label).toBe('TS') + }) + + it('falls back to sequential codes for three-way conflicts with same second letter', () => { + const elements = [mockElement('AA'), mockElement('AB'), mockElement('AA')] + const hints = assignLabels(elements) + const labels = hints.map((h) => h.label) + expect(labels).toHaveLength(3) + expect(new Set(labels).size).toBe(3) + }) + + it('assigns sequential codes for elements with no text', () => { + const el1 = document.createElement('button') + const el2 = document.createElement('button') + ;[el1, el2].forEach(mockRect) + const hints = assignLabels([el1, el2]) + expect(hints[0].label).toMatch(/^[A-Z]{2}$/) + expect(hints[1].label).toMatch(/^[A-Z]{2}$/) + expect(hints[0].label).not.toBe(hints[1].label) + }) + + it('avoids prefix collisions between single and two-char labels', () => { + const elements = [ + mockElement('Inbox'), + mockElement('Journal'), + mockElement(''), + mockElement('') + ] + const hints = assignLabels(elements) + const singleChars = hints.filter((h) => h.label.length === 1).map((h) => h.label) + const twoChars = hints.filter((h) => h.label.length === 2).map((h) => h.label) + + for (const sc of singleChars) { + for (const tc of twoChars) { + expect(tc.startsWith(sc)).toBe(false) + } + } + }) + + it('is case insensitive — labels are uppercase', () => { + const elements = [mockElement('inbox')] + const hints = assignLabels(elements) + expect(hints[0].label).toBe('I') + }) + + it('skips non-ASCII first letters and assigns sequential code', () => { + const elements = [mockElement('日本語')] + const hints = assignLabels(elements) + expect(hints[0].label).toMatch(/^[A-Z]{2}$/) + }) + + it('handles single element', () => { + const hints = assignLabels([mockElement('Only')]) + expect(hints).toHaveLength(1) + expect(hints[0].label).toBe('O') + }) + + it('handles empty input', () => { + const hints = assignLabels([]) + expect(hints).toHaveLength(0) + }) + + it('preserves element references in output', () => { + const el = mockElement('Test') + const hints = assignLabels([el]) + expect(hints[0].element).toBe(el) + }) + + it('includes rect in output', () => { + const hints = assignLabels([mockElement('Test')]) + expect(hints[0].rect).toBeDefined() + expect(hints[0].rect.width).toBe(100) + }) +}) diff --git a/apps/desktop/src/renderer/src/lib/label-assigner.ts b/apps/desktop/src/renderer/src/lib/label-assigner.ts new file mode 100644 index 000000000..78a311693 --- /dev/null +++ b/apps/desktop/src/renderer/src/lib/label-assigner.ts @@ -0,0 +1,127 @@ +import type { HintTarget } from '@/contexts/hint-mode/types' + +const ASCII_UPPER = /^[A-Z]$/ + +export const extractElementText = (el: HTMLElement): string => { + const ariaLabel = el.getAttribute('aria-label') + if (ariaLabel?.trim()) return ariaLabel.trim() + + const textContent = el.textContent?.trim() + if (textContent) return textContent + + const title = el.getAttribute('title') + if (title?.trim()) return title.trim() + + return '' +} + +const getFirstLetter = (text: string): string | null => { + const char = text.charAt(0).toUpperCase() + return ASCII_UPPER.test(char) ? char : null +} + +const asciiLettersAfter = (text: string): string[] => { + const letters: string[] = [] + const seen = new Set() + for (let i = 1; i < text.length; i++) { + const char = text.charAt(i).toUpperCase() + if (ASCII_UPPER.test(char) && !seen.has(char)) { + letters.push(char) + seen.add(char) + } + } + return letters +} + +const generateSequentialCode = ( + index: number, + usedLabels: Set, + singleCharLabels: Set +): string | null => { + const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + let seqIndex = 0 + for (let i = 0; i < 26; i++) { + for (let j = 0; j < 26; j++) { + const code = letters[i] + letters[j] + if (singleCharLabels.has(code[0])) continue + if (usedLabels.has(code)) continue + if (seqIndex === index) return code + seqIndex++ + } + } + return null +} + +export const assignLabels = (elements: HTMLElement[]): HintTarget[] => { + if (elements.length === 0) return [] + + const texts = elements.map((el) => extractElementText(el)) + const firstLetters = texts.map((t) => getFirstLetter(t)) + + const letterGroups = new Map() + const noLetterIndices: number[] = [] + + firstLetters.forEach((letter, i) => { + if (!letter) { + noLetterIndices.push(i) + return + } + const group = letterGroups.get(letter) ?? [] + group.push(i) + letterGroups.set(letter, group) + }) + + const labels = new Array(elements.length).fill('') + const singleCharLabels = new Set() + + for (const [letter, indices] of letterGroups) { + if (indices.length === 1) { + labels[indices[0]] = letter + singleCharLabels.add(letter) + } + } + + const usedLabels = new Set(singleCharLabels) + const needsSequential: number[] = [] + + for (const [letter, indices] of letterGroups) { + if (indices.length <= 1) continue + + for (const idx of indices) { + const candidates = asciiLettersAfter(texts[idx]) + let assigned = false + for (const char of candidates) { + const candidate = letter + char + if (!usedLabels.has(candidate)) { + labels[idx] = candidate + usedLabels.add(candidate) + assigned = true + break + } + } + if (!assigned) needsSequential.push(idx) + } + } + + const allNeedSequential = [...noLetterIndices, ...needsSequential] + let seqCounter = 0 + for (const idx of allNeedSequential) { + const code = generateSequentialCode(seqCounter, usedLabels, singleCharLabels) + if (code === null) break + labels[idx] = code + usedLabels.add(code) + seqCounter++ + } + + const targets: HintTarget[] = [] + for (let i = 0; i < elements.length; i++) { + if (labels[i] === '') continue + targets.push({ + element: elements[i], + label: labels[i], + rect: elements[i].getBoundingClientRect(), + text: texts[i] + }) + } + return targets +} diff --git a/apps/desktop/tests/e2e/hint-mode.e2e.ts b/apps/desktop/tests/e2e/hint-mode.e2e.ts new file mode 100644 index 000000000..2cb0c1034 --- /dev/null +++ b/apps/desktop/tests/e2e/hint-mode.e2e.ts @@ -0,0 +1,456 @@ +// @ts-nocheck - E2E tests inject globals (window.__hintClicks) for introspection. +/** + * Hint Mode E2E Tests + * + * Covers the Vim-style hint navigation feature end-to-end: + * - Activation shortcuts: Alt+F (universal), F when no editor focused + * - Editor suppression: F inside contenteditable / input / textarea types normally + * - Overlay + HINT status indicator rendering via portal into document.body + * - Hint label assignment (single-char mnemonic; two-char fallback on first-letter collision) + * - Badge opacity narrowing + indicator echoing typed chars + * - Backspace to undo narrowing + * - Unique-full-match auto-click + deactivation + * - Escape to deactivate + * - Non-matching chars silently ignored + * + * Strategy: inject controlled DOM (`data-hint` buttons, textarea) so tests don't + * depend on sidebar labels that shift as features are added. Match each badge to + * its source element by comparing screen rects (badges render at rect.top - 6, + * rect.left - 6), which survives any change to the label-assignment algorithm. + */ + +import { test, expect } from './fixtures' +import { waitForAppReady, waitForVaultReady } from './utils/electron-helpers' + +const OVERLAY_ID = 'hint-mode-overlay' +const INDICATOR_TEXT = 'HINT' +const TARGET_TESTID = 'hint-e2e-target' +const EDITOR_ID = 'hint-e2e-editor' +const HOST_BUTTON_ID = 'hint-e2e-host-button' + +type InjectedButton = { + label: string + top: number + left: number +} + +async function activate(page): Promise { + await page.keyboard.press('Alt+f') + await page.waitForSelector(`#${OVERLAY_ID}`, { timeout: 2000 }) +} + +async function isOverlayGone(page): Promise { + await expect(page.locator(`#${OVERLAY_ID}`)).toHaveCount(0) +} + +async function injectButton(page, button: InjectedButton, index = 0): Promise { + await page.evaluate( + ({ label, top, left, index, testid }) => { + const btn = document.createElement('button') + btn.setAttribute('data-hint', '') + btn.setAttribute('data-testid', `${testid}-${index}`) + btn.setAttribute('aria-label', label) + btn.dataset.hintE2eIndex = String(index) + btn.textContent = label + Object.assign(btn.style, { + position: 'fixed', + top: `${top}px`, + left: `${left}px`, + width: '140px', + height: '44px', + zIndex: '10000', + background: '#1e293b', + color: '#f8fafc', + border: '1px solid #334155' + }) + if (typeof (window as any).__hintClicks !== 'object') { + ;(window as any).__hintClicks = {} + } + ;(window as any).__hintClicks[String(index)] = 0 + btn.addEventListener('click', () => { + ;(window as any).__hintClicks[String(index)] += 1 + }) + document.body.appendChild(btn) + }, + { label: button.label, top: button.top, left: button.left, index, testid: TARGET_TESTID } + ) +} + +async function cleanupInjected(page): Promise { + await page.evaluate( + ({ testid, editorId, hostId }) => { + document + .querySelectorAll(`[data-testid^="${testid}"], #${editorId}, #${hostId}`) + .forEach((el) => el.remove()) + delete (window as any).__hintClicks + }, + { testid: TARGET_TESTID, editorId: EDITOR_ID, hostId: HOST_BUTTON_ID } + ) +} + +/** + * Returns every rendered hint badge's label, opacity, and bounding rect. + * Badges are the direct children of the #hint-mode-overlay portal. + */ +async function readBadges( + page +): Promise> { + return page.evaluate((id) => { + const overlay = document.getElementById(id) + if (!overlay) return [] + return Array.from(overlay.querySelectorAll(':scope > span')).map((badge) => { + const rect = (badge as HTMLElement).getBoundingClientRect() + return { + label: badge.textContent ?? '', + opacity: parseFloat(getComputedStyle(badge as HTMLElement).opacity), + top: rect.top, + left: rect.left + } + }) + }, OVERLAY_ID) +} + +/** + * Finds the label assigned to the injected test button at `index` by matching + * the badge's rendered position (rect.top - 6, rect.left - 6) to the button's + * bounding rect. Tolerance accounts for sub-pixel rounding. + */ +async function labelForInjectedButton(page, index: number): Promise { + return page.evaluate( + ({ id, selector }) => { + const overlay = document.getElementById(id) + if (!overlay) return null + const target = document.querySelector(selector) as HTMLElement | null + if (!target) return null + const tr = target.getBoundingClientRect() + const badges = Array.from(overlay.querySelectorAll(':scope > span')) as HTMLElement[] + for (const badge of badges) { + const br = badge.getBoundingClientRect() + if (Math.abs(br.top - (tr.top - 6)) < 3 && Math.abs(br.left - (tr.left - 6)) < 3) { + return badge.textContent + } + } + return null + }, + { id: OVERLAY_ID, selector: `[data-testid="${TARGET_TESTID}-${index}"]` } + ) +} + +async function clickCount(page, index: number): Promise { + return page.evaluate((i) => (window as any).__hintClicks?.[i] ?? 0, String(index)) +} + +test.describe('Hint Mode', () => { + test.beforeEach(async ({ page }) => { + await waitForAppReady(page) + await waitForVaultReady(page) + }) + + test.afterEach(async ({ page }) => { + // Ensure no overlay bleeds into the next test. Escape is a no-op if mode is off. + await page.keyboard.press('Escape').catch(() => {}) + await cleanupInjected(page) + }) + + test('Alt+F activates: overlay portal, HINT indicator, and badges all render', async ({ + page + }) => { + await injectButton(page, { label: 'ZEBRA', top: 180, left: 200 }) + + await activate(page) + + await expect(page.locator(`#${OVERLAY_ID}`)).toBeVisible() + await expect(page.getByText(INDICATOR_TEXT, { exact: true })).toBeVisible() + + const badges = await readBadges(page) + expect(badges.length).toBeGreaterThan(0) + + const injectedLabel = await labelForInjectedButton(page, 0) + expect(injectedLabel).toBeTruthy() + }) + + test('Escape deactivates and removes overlay + indicator', async ({ page }) => { + await injectButton(page, { label: 'ZEBRA', top: 180, left: 200 }) + await activate(page) + await expect(page.locator(`#${OVERLAY_ID}`)).toBeVisible() + + await page.keyboard.press('Escape') + + await isOverlayGone(page) + await expect(page.getByText(INDICATOR_TEXT, { exact: true })).toHaveCount(0) + }) + + test('typing a prefix narrows hints: non-matching badges dim, indicator echoes typed char', async ({ + page + }) => { + // Two buttons sharing first letter X → label assigner MUST produce two-char + // codes both beginning with X (e.g. XR, XE). Typing X keeps 2 hints active; + // no auto-click yet. + await injectButton(page, { label: 'XRAY', top: 180, left: 200 }, 0) + await injectButton(page, { label: 'XENON', top: 260, left: 200 }, 1) + + await activate(page) + const before = await readBadges(page) + const xPrefixedBefore = before.filter((b) => b.label.startsWith('X')) + expect(xPrefixedBefore.length).toBeGreaterThanOrEqual(2) + + await page.keyboard.press('x') + + // Indicator echoes typed char via . + await expect(page.locator('kbd', { hasText: 'X' })).toBeVisible() + + // HintBadge uses `transition: opacity 100ms ease`, so computed opacity + // interpolates. Poll until dimmed badges settle at their 0.3 target rather + // than hard-coding a sleep that may be too short on slow CI runners. + await expect + .poll( + async () => { + const after = await readBadges(page) + const dimmed = after.filter((b) => !b.label.startsWith('X')) + return dimmed.every((b) => b.opacity < 0.5) + }, + { timeout: 2000 } + ) + .toBe(true) + + const after = await readBadges(page) + const bright = after.filter((b) => b.label.startsWith('X')) + for (const b of bright) expect(b.opacity).toBeGreaterThan(0.9) + + // Overlay still active (2+ candidates, no unique full match yet). + await expect(page.locator(`#${OVERLAY_ID}`)).toBeVisible() + }) + + test('backspace removes the last typed char and restores full hint set', async ({ page }) => { + await injectButton(page, { label: 'XRAY', top: 180, left: 200 }, 0) + await injectButton(page, { label: 'XENON', top: 260, left: 200 }, 1) + + await activate(page) + await page.keyboard.press('x') + await expect(page.locator('kbd', { hasText: 'X' })).toBeVisible() + + await page.keyboard.press('Backspace') + + // When typedChars is empty, indicator renders no . + await expect(page.locator('kbd', { hasText: 'X' })).toHaveCount(0) + // HINT badge itself still shown. + await expect(page.getByText(INDICATOR_TEXT, { exact: true })).toBeVisible() + + // All badges back to full opacity — wait past the 100ms transition. + await expect + .poll( + async () => { + const badges = await readBadges(page) + return badges.every((b) => b.opacity > 0.9) + }, + { timeout: 2000 } + ) + .toBe(true) + }) + + test('typing a full unique label clicks the target element and deactivates', async ({ page }) => { + await injectButton(page, { label: 'QUEBEC', top: 180, left: 200 }, 0) + + await activate(page) + const label = await labelForInjectedButton(page, 0) + expect(label).toBeTruthy() + expect(label!.length).toBeGreaterThan(0) + + // Type each char of the assigned label. The last keystroke that completes + // the full label must trigger auto-click + deactivate (single exact match). + for (const ch of label!) { + await page.keyboard.press(ch.toLowerCase()) + } + + await isOverlayGone(page) + await expect(page.getByText(INDICATOR_TEXT, { exact: true })).toHaveCount(0) + + const clicks = await clickCount(page, 0) + expect(clicks).toBe(1) + }) + + test('non-matching character is silently ignored; overlay remains active with no typed chars', async ({ + page + }) => { + await injectButton(page, { label: 'ZEBRA', top: 180, left: 200 }) + await activate(page) + + // Digits can't be labels — scanClickableElements + label-assigner only emit ASCII letters. + await page.keyboard.press('9') + + await expect(page.locator(`#${OVERLAY_ID}`)).toBeVisible() + // Indicator must NOT show a typed-chars kbd because typeChar rejected the input. + await expect(page.locator('kbd', { hasText: '9' })).toHaveCount(0) + }) + + test('F alone activates when focus is on a non-editor element', async ({ page }) => { + // Inject a plain button (not in CLICKABLE_SELECTOR for label purposes, just a focus host), + // focus it, then press F. The activation hook's editor detector should pass. + await page.evaluate( + ({ hostId }) => { + const host = document.createElement('button') + host.id = hostId + host.textContent = 'Focus host' + Object.assign(host.style, { + position: 'fixed', + top: '20px', + left: '20px', + width: '140px', + height: '44px', + zIndex: '10000' + }) + document.body.appendChild(host) + host.focus() + }, + { hostId: HOST_BUTTON_ID } + ) + await injectButton(page, { label: 'ZEBRA', top: 180, left: 200 }) + + await page.keyboard.press('f') + + await expect(page.locator(`#${OVERLAY_ID}`)).toBeVisible() + await expect(page.getByText(INDICATOR_TEXT, { exact: true })).toBeVisible() + }) + + test('F inside a focused editor does NOT activate hint mode (passes through as typing)', async ({ + page + }) => { + await page.evaluate( + ({ editorId }) => { + const ta = document.createElement('textarea') + ta.id = editorId + Object.assign(ta.style, { + position: 'fixed', + top: '20px', + left: '20px', + width: '240px', + height: '80px', + zIndex: '10000' + }) + document.body.appendChild(ta) + ta.focus() + }, + { editorId: EDITOR_ID } + ) + + await page.keyboard.press('f') + + await isOverlayGone(page) + const value = await page.locator(`#${EDITOR_ID}`).inputValue() + expect(value).toBe('f') + }) + + test('Alt+F still activates even when editor has focus (editor-override shortcut)', async ({ + page + }) => { + await page.evaluate( + ({ editorId }) => { + const ta = document.createElement('textarea') + ta.id = editorId + Object.assign(ta.style, { + position: 'fixed', + top: '20px', + left: '20px', + width: '240px', + height: '80px', + zIndex: '10000' + }) + document.body.appendChild(ta) + ta.focus() + }, + { editorId: EDITOR_ID } + ) + await injectButton(page, { label: 'ZEBRA', top: 180, left: 240 }) + + await page.keyboard.press('Alt+f') + + await expect(page.locator(`#${OVERLAY_ID}`)).toBeVisible() + await expect(page.getByText(INDICATOR_TEXT, { exact: true })).toBeVisible() + + // And the textarea must not have received 'f' (hook called preventDefault). + const value = await page.locator(`#${EDITOR_ID}`).inputValue() + expect(value).toBe('') + }) + + test('disabled elements and elements outside viewport do not receive hint badges', async ({ + page + }) => { + // Visible + enabled → labeled. + await injectButton(page, { label: 'VISIBLE_ONE', top: 200, left: 200 }, 0) + + // Disabled button at a visible position → must be skipped by scanner. + await page.evaluate(() => { + const btn = document.createElement('button') + btn.setAttribute('data-hint', '') + btn.setAttribute('data-testid', 'hint-e2e-disabled') + btn.setAttribute('aria-label', 'DISABLED_ONE') + btn.disabled = true + btn.textContent = 'DISABLED_ONE' + Object.assign(btn.style, { + position: 'fixed', + top: '280px', + left: '200px', + width: '140px', + height: '44px', + zIndex: '10000' + }) + document.body.appendChild(btn) + }) + + // Off-screen button (negative coordinates) → must be skipped by isInViewport. + await page.evaluate(() => { + const btn = document.createElement('button') + btn.setAttribute('data-hint', '') + btn.setAttribute('data-testid', 'hint-e2e-offscreen') + btn.setAttribute('aria-label', 'OFFSCREEN_ONE') + btn.textContent = 'OFFSCREEN_ONE' + Object.assign(btn.style, { + position: 'fixed', + top: '-500px', + left: '-500px', + width: '140px', + height: '44px', + zIndex: '10000' + }) + document.body.appendChild(btn) + }) + + await activate(page) + + const badges = await readBadges(page) + const labels = badges.map((b) => b.label) + + // No label on disabled/off-screen buttons regardless of the codes chosen. + // Verify indirectly: the visible injected button has a badge, the others + // don't. We compare badge positions against each injected rect. + const positions = await page.evaluate(() => { + const pick = (sel: string) => { + const el = document.querySelector(sel) as HTMLElement | null + if (!el) return null + const r = el.getBoundingClientRect() + return { top: r.top - 6, left: r.left - 6 } + } + return { + visible: pick('[data-testid="hint-e2e-target-0"]'), + disabled: pick('[data-testid="hint-e2e-disabled"]'), + offscreen: pick('[data-testid="hint-e2e-offscreen"]') + } + }) + + const hasBadgeAt = (p: { top: number; left: number } | null) => + !!p && badges.some((b) => Math.abs(b.top - p.top) < 3 && Math.abs(b.left - p.left) < 3) + + expect(hasBadgeAt(positions.visible)).toBe(true) + expect(hasBadgeAt(positions.disabled)).toBe(false) + expect(hasBadgeAt(positions.offscreen)).toBe(false) + + // Cleanup the extra injected DOM. + await page.evaluate(() => { + document + .querySelectorAll('[data-testid="hint-e2e-disabled"], [data-testid="hint-e2e-offscreen"]') + .forEach((el) => el.remove()) + }) + // Leave nothing typed, sequence length of `labels` was just for debug context. + expect(labels.length).toBeGreaterThan(0) + }) +})