* feat(plugins): fire PERSISTED_DATA_CHANGED hook on persisted-data changes
Wires the dead PluginHooks.PERSISTED_DATA_UPDATE enum into a fired hook,
renamed PERSISTED_DATA_CHANGED. Plugins are notified when their persisted
data changes for any reason after the host's initial boot load — local
writes, remote incremental sync via bulkApplyOperations, and post-boot
wholesale loadAllData paths (SYNC_IMPORT / BACKUP_IMPORT / validation
repair / recovery).
Selector-based effect on selectPluginUserDataFeatureState, gated on
SyncTriggerService.afterInitialSyncDoneAndDataLoadedInitially$ so the
boot-time state seeds the pairwise baseline. Differ compares prev/next
by === on the encoded data blob (never decoded). Stage A composite
entityIds (pluginId:key) are normalized to owner pluginId and deduped
so a plugin with N keyed entries changing in one emission fires exactly
once. Per-pluginId dispatch via new PluginHooksService.dispatchHookToPlugin.
Effect is { dispatch: false } and creates no ops, so no sync-window
guard is needed — and adding one (skipDuringSyncWindow) would silently
suppress the very remote-sync deliveries the hook is designed to catch.
Closes#7754. Follow-up: doc-mode adoption tracked in #7752.
* test(plugins): tighten PERSISTED_DATA_CHANGED spec + docs
Multi-review pass 2 surfaced that the 5s timeout spec asserted only that
the dispatcher resolved — it would have silently passed if the timeout
race were removed entirely. Spy on PluginLog.err and assert the timeout
message actually reached the catch branch.
Also:
- Add `:` guard to PluginHooksService.registerHookHandler so the
persistence-key grammar is enforced at both the persistence and
hooks-registry endpoints (defense-in-depth; composeId already throws
at the bridge).
- Add async-rejected-promise spec to cover the Promise.race branch that
sync-throw didn't exercise.
- Carry the PERSISTED_DATA_CHANGED contract paragraph into
docs/plugin-development.md and docs/wiki/3.01-API.md (previously only
in packages/plugin-api/README.md).
- Clarify loadSyncedData(key?) in the README for keyed plugins.
* feat(plugins): keyed persistence API for per-context LWW (Stage A Phase 1+3)
Add an optional `key` argument to `persistDataSynced` / `loadSyncedData`,
composed at the bridge transport boundary into `pluginId:key` entity ids.
Distinct keys now produce distinct ops that LWW-resolve per-entity,
enabling document-mode-style plugins to avoid cross-context blob
overwrites without changing existing keyless callers.
Phase 3: `removePluginUserData(pluginId)` now sweeps the full prefix
(legacy entry + every keyed entry), dispatching one delete per match
with the rule-6 setTimeout(0) trailer so remote replicas don't keep
keyed entries after uninstall. The reducer-only "smart prefix match"
shortcut is wrong (one op for the prefix only, remote keyed entries
leak) — see docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md
Phase 3.
Phase 4 (document-mode plugin-side migration of the legacy single-blob
entry) is left as a separate follow-up so the host change can be
reviewed in isolation.
Issue #7749
* feat(document-mode): migrate to keyed persistence (Stage A Phase 4)
Move from one synced blob under the bare plugin id to per-entity keyed
entries:
- meta — { enabledCtxIds: string[] }, owned by background.ts
- doc:${ctxId} — one entry per context, owned by the editor iframe
- __meta__ — migration stamp
Each entry has its own LWW timestamp on the host, so a concurrent edit
in project A on Device 1 and project B on Device 2 no longer
whole-blob-collide.
The migration runs idempotently from both background.ts and editor.ts
(stamp-guarded), splits the legacy single blob into keyed entries, then
tombstones the legacy entry with an empty payload — giving LWW a
winning side against any offline device that still writes the old
shape.
flushSave / flushSaveSync no longer need to read+merge sibling state,
since each context's entry stands alone. The future-version blob guard
(isStorageUnreadable) is dropped — it referenced the wrapping blob's
version, which no longer exists; per-doc corruption still falls back
via isDocCorrupt.
Issue #7749
* fix(plugins): tighten Stage A keyspace at the boundaries
Multi-review surfaced three small gaps in the keyed-persistence rollout:
- The synchronous composeId throw covers the bridge's iframe and
direct-API entry points, but three in-process callers
(plugin-config.service, plugin.service, plugin-config-dialog) bypass
the bridge and route directly into the persistence service. A
user-installed plugin with `id: "evil:plugin"` passed manifest
validation and would have collided with the legitimate `evil`
plugin's keyed namespace — `removePluginUserData('evil')` would have
over-matched the sweep. Reject the colon at install time in
`validatePluginManifest`; keep the bridge throw as defense-in-depth.
- The new `key` arg at the bridge was typia-asserted on `data` but
unchecked itself. A compromised iframe could pass a multi-megabyte
string or a non-string value via postMessage. `data` is capped at
1 MB, but the entity id composed from `key` would be stored verbatim
in NgRx state, IndexedDB, the op-log, and on the sync wire — bypassing
the data cap. Add `assertPluginPersistenceKey` with a 256-char cap.
- `_loadPersistedData` silently returned `null` when composeId threw,
while `_persistDataSynced` rethrew. The asymmetry made a malformed
pluginId look like "no data yet" on the load side, indistinguishable
from a fresh install. Hoist composeId + key validation out of the
load try/catch so it throws symmetrically.
Issue #7749
* fix(plugins): lower per-write cap to 256 KB
The pre-Stage-A 1 MB cap was sized for the old single-blob shape, where
one entry held every context's data. With the keyed split, each entity
gets its own write budget — 1 MB per write is wildly over-provisioned
for the realistic upper bound of plugin payloads (heavy document-mode
docs ~30–100 KB, configs and automations KB-scale).
256 KB keeps 2–5× headroom over realistic payloads while bounding the
per-plugin storage growth more tightly.
Document-mode's migration loop now skips oversized legacy docs instead
of aborting the whole run: a user whose legacy blob holds one ~500 KB
doc (legal under the old cap) keeps the other contexts migrated and
the original bytes preserved in the legacy entry. The success stamp
stays at migrated:0 in that case so a future build (or pruning of the
doc) can complete the migration without data loss.
Issue #7749
* test(plugins): e2e migration of legacy single-blob to keyed entries
The migration logic in document-mode is unit-tested against a mock
PluginAPI, which can't catch real-iframe quirks (postMessage handling
of undefined second args, commit-chain timing under the host's
per-entity rate limiter, hydration ordering against the op-log). Add
two end-to-end scenarios:
- Fresh install: enable the plugin, verify the __meta__ stamp lands
at migrated:1 (the migration's final write — observing it implies
every earlier step completed).
- Legacy blob: seed a pre-Stage-A single-blob entry via the e2e helper
store, enable the plugin, verify the legacy entry is tombstoned,
meta carries the enabledCtxIds, and each doc landed under its own
doc:${ctxId} key.
Issue #7749
* chore(plugins): drop dead code and review-driven polish
Four small follow-ups from the multi-review pass:
- Don't log the plugin-supplied `key` value. Plugins may use user
content (search queries, doc titles) as keys; the log history is
exportable. Log `keyLen` instead, per CLAUDE.md rule 9.
- Delete `detectStaleLegacyWrite` and its 3 specs. Exported and
fully tested, but zero non-test callers — banner UI is forbidden
by project convention for transient-only messaging. If the need
resurfaces, the implementation is four lines.
- Drop the `attemptedAt` field from `MigrationStamp`. It was written
but never read; the success stamp is the only re-entry gate, and
the resume path is just "re-run the loop" — re-writes are content-
idempotent. Saves one rate-limited write per fresh migration.
- Update `docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md`
with an implementation-status table referencing the shipping
commits, so future readers don't have to dig through git.
Issue #7749
Add the host-side plugin API for work-context-scoped UI:
- registerWorkContextHeaderButton — context-filtered (PROJECT/TAG/TODAY)
header buttons, with toggled-state support on the active button
- a work-view embed slot so a plugin can replace the task list with its
own UI for the active context (showInWorkContext / closeWorkContextView)
- the WORK_CONTEXT_CHANGE hook and getActiveWorkContext() pull API,
sharing one toActiveWorkContext() projection helper as the single
source of truth
- selectTask API plus the plugin-bridge / plugin-hooks wiring and specs
Squashed from the feat/doc-mode-v4 work. The full per-step history —
including the earlier in-tree (non-plugin) Angular document-mode feature
that was prototyped and then removed on the same branch — is preserved
in the archive/doc-mode-v4-full-history branch and the
doc-mode-v4-pre-squash tag.
* feat(plugin-api): add tag ids to plugin field mapping
* feat(plugin-sync): handle tag ids in plugin sync adapter
* feat(plugin-sync): handle tag ids in plugin issue provider adapter
* feat(plugin-sync): add tags to two way sync
* fix(plugin-sync): add deep equality comparison for arrays
* refactor(plugin-sync): extract sortTagLabels utility and refactor sync adapters
* fix(two-way-sync): trigger sync on addTagToTask action
* fix(plugin-sync): harden tag synchronization
* fix(plugin-sync): preserve provider-owned baselines
---------
Co-authored-by: johannesjo <johannes.millan@gmail.com>
* feat(plugins): add onReady() API with IPC ping + fix consent write delay #7326
- Remove setTimeout(5000) from _getNodeExecutionConsent; write consent immediately
- Add plugin.onReady(fn) to PluginAPI — fires after plugin.js evaluation and IPC bridge confirmation
- Add _pingNodeBridge() in plugin.service.ts with 3-attempt retry (1s, 2s delays)
- Add triggerReady() and pingNodeBridge() to PluginRunner
- Show snack + set error state if IPC bridge unavailable after retries
- Add NODE_EXECUTION_BRIDGE_UNAVAILABLE translation key
- Add focused tests for onReady, triggerReady, pingNodeBridge, consent persistence
- Update plugin-development.md with onReady usage and nodeExecution guidance
* test(plugins): fix unused variable lint errors in spec files
* fix(plugins): guard triggerReady on instance.loaded; fix doc numbering
* fix(plugins): remove _triggerReady from public API, route ping via bridge, add retry tests
* fix(electron): add paths for @sp/sync-providers subpath exports (node moduleResolution compat)
* fix(plugins): centralize onReady, tear down runtime on activation error, add iframe onReady
Address review on #7578:
- All plugin load paths (startup, upload, reload, lazy) now go through _fireOnReady,
ensuring the IPC ping + onReady fire on every successful load — not just lazy.
- activatePlugin error path now unloads the plugin runtime (hooks, buttons, side
effects) before setting status='error', preventing partially-running plugins.
- Iframe PluginAPI now exposes onReady (fires on next microtask after plugin.js
evaluates), matching the host-side contract for typed iframe plugins.
* fix(plugins): clean up half-loaded plugins on onReady error, test real retry util
Self-review followups:
- _fireOnReadyWithCleanup wraps the 3 non-activatePlugin load paths and tears
down the plugin (unloadPlugin + remove from list + status='error' + snack)
if the IPC ping or onReady callback throws. Previously, those paths only
logged and rethrew, leaving partially-running plugins.
- Extracted retry loop into pure pingWithRetry utility; spec now exercises
production code instead of an inline-replicated stub. Removed the old
plugin-ping-node-bridge.spec.ts which was just testing its own copy of
the logic.
- Documented iframe onReady semantic (fires on microtask, no ping) in both
the source comment and docs/plugin-development.md, since cold-boot is not
a concern for iframe plugins (rendered on demand).
* ci(plugins): use npm i for root install to tolerate override drift
The root lockfile pins app-builder-lib's transitive minimatch via the
`overrides` field. npm 10.9.7 (bundled with Node 22 in setup-node@v6)
flags this as drift and fails `npm ci`, while npm 11 accepts it.
ci.yml's main test job uses `npm i`, which tolerates the drift without
mutating the lockfile on disk.
Plugin-Tests has been red on every PR since 2026-05-08 for this reason.
The inner `npm ci` for plugin-specific deps stays strict.
* fix(plugins): make onReady optional, assert callback isolation in spec
- packages/plugin-api/src/types.ts: mark onReady? optional on the public
PluginAPI interface so existing plugin TypeScript typings (and any
third-party PluginAPI implementations) remain assignable after upgrade.
The host runtime already treats onReady as optional (no-op if no
registration callback is provided), so this aligns the type with the
actual contract.
- src/app/plugins/plugin-runner.spec.ts: the previous isolation test only
asserted that triggerReady() resolved for both plugins; it would still
pass if triggerReady fired every registered callback. The updated test
wires per-plugin Jasmine spies through globalThis (the same context the
plugin code's `new Function` runs in) and asserts call counts before
and after each triggerReady, actually proving isolation.
* refactor(plugins): test real consent logic; scope startup snacks; tighten ping timeout
Address review feedback on PR #7578:
- Extract consent decision into pure `decideNodeExecutionConsent` util so the
spec exercises real code instead of a reimplemented stub. Delete the
stub-based plugin-consent.spec.ts and plugin-fire-on-ready.spec.ts (the
latter was orchestration glue already covered by plugin-runner.spec.ts and
ping-with-retry.util.spec.ts).
- Reduce per-ping timeout 5000ms -> 1500ms. Worst-case cold-boot bridge-down
detection drops from ~17s to ~7.5s; in-process vm script returning true
doesn't need 5s.
- Add PLUGIN_LOAD_FAILED translation wrapping plugin name + error. Strip the
now-redundant pluginName from NODE_EXECUTION_BRIDGE_UNAVAILABLE.
- Scope activation-failure snack to manual activations only — startup
auto-activation failures stay silent (plugin tile shows error state).
_handleReadyFailure still snacks unconditionally since onReady failure
leaves a partially-loaded runtime that the user needs to see.
---------
Co-authored-by: Benjamin <1159333+benjaminburzan@users.noreply.github.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
The web build at app.super-productivity.com was inheriting the desktop
OAuth client whose accepted redirect URIs are loopback-only, so Google
rejected the https callback with redirect_uri_mismatch.
Add webClientId / webClientSecret to OAuthFlowConfig and route the
browser branch through them. Google's "Web application" client type is
provisioned as confidential, so PKCE alone is not enough — the secret
ships in client JS like the desktop one. Electron and native mobile
flows are unchanged.
* feat: add plugin data reinit api
* test(plugins): stub data init in bridge spec
* test(plugins): stub data init in counter bridge spec
---------
Co-authored-by: Aamer Akhter <aamer_akhter@users.noreply.bitbucket.org>
Add a CalDAV Calendar plugin that syncs tasks with calendar events via
the CalDAV/WebDAV protocol, supporting self-hosted servers like Nextcloud.
Plugin features:
- CalDAV PROPFIND/REPORT for calendar discovery and event fetching
- iCal parsing and serialization with RFC 5545 compliance
- Two-way sync with field mappings (title, notes, dates, duration)
- Time-block integration for auto-creating calendar events
- Multi-calendar support with compound IDs
Host-side changes:
- Add generic request() method to PluginHttp for WebDAV methods
- Add allowPrivateNetwork manifest flag (bundled plugins only)
- Add dynamic loadOptions support for config field dropdowns
- Add backfill effect for existing scheduled tasks
- Generify translation keys (LOAD_OPTIONS instead of LOAD_CALENDARS)
Security:
- SSRF protection via origin validation in resolveHref
- allowPrivateNetwork gated by _isPluginBundled check
- XML parse error detection in CalDAV response parsing
- Description rendered as plain text (not markdown)
Correctness:
- TZID conversion uses formatToParts (no local timezone contamination)
- modifyICalEvent scoped to VEVENT block (preserves VTIMEZONE)
- responseType: 'text' on all CalDAV PUT/DELETE calls
- CR characters handled in iCal text escaping
- Trailing CRLF added to modifyICalEvent output per RFC 5545
- Fix taskIdToGcalEventId collision by hex-encoding nanoid bytes
- Add timeBlock API to plugin interface, move Google Calendar-specific
logic into the plugin, make TimeBlockSyncEffects provider-agnostic
- Use isPluginIssueProvider() in task selectors
- Replace console.error with Log.err, add i18n for time-block errors
- Use 1-hour default when rescheduling all-day event to timed slot
- Apply config migration in dialog for legacy single-calendar configs
- Remove planTaskForDay/moveBeforeTask from deleteOnUnschedule$
- Rename icalEvents$ to calendarEvents$
- Make issueProviderKey required on CalendarIntegrationEvent
- Parameterize loadAllCalendars/loadWritableCalendars
- Replace deprecated unescape() with TextEncoder in iCal util
- Fix btoa() non-ASCII throw in getIssueLink
- Fix timezone bug in all-day reschedule date parsing
- Deep-clone pluginConfig before mutating in migration dialog
- Add showIf property to PluginFormField so fields can depend on
another config value being truthy
- Show timeBlockCalendarId only when isAutoTimeBlock is enabled
- Improve wording for auto time blocking and calendar field descriptions
Add Google Calendar as a plugin-based calendar provider with OAuth 2.0
authentication, multi-calendar support, and event management.
- Plugin fetches events from selected read calendars, merged into the
existing calendar integration pipeline
- Context menus on planner and schedule views for calendar events:
open link, reschedule, create as task, hide forever, delete
- Reschedule opens date/time picker and updates event via plugin API,
handling both timed and all-day events correctly
- Delete with confirmation dialog
- CalendarEventActionsService extracts shared event action logic
- HiddenCalendarEventsService for permanent event hiding
- triggerRefresh() for immediate UI updates after mutations
- Multi-select config fields for choosing calendars to display
- Fix change detection for plugin select fields in provider dialog
- Electron-safe URL opening with scheme validation
Google rejects custom scheme redirect URIs that don't match the
platform's app identifier. Use the Android applicationId
(com.superproductivity.superproductivity) on Android and the iOS
bundle ID (com.super-productivity.app) on iOS, with single-slash
URI format per Google's documentation.
- Add iosClientId to OAuthFlowConfig and plugin API types
- Add iOS OAuth client ID to Google Calendar plugin
- Select client ID per platform (Android/iOS/Desktop) in bridge service
- Add Android package name intent filter in AndroidManifest
- Accept both URI schemes in OAuth callback handler
- Fix redirect handler to use IS_NATIVE_PLATFORM consistently
Google rejects Desktop OAuth client IDs when the redirect URI is a
custom scheme (as used on mobile via Capacitor), returning a 400 error.
Add mobileClientId to OAuthFlowConfig so plugins can specify a separate
Android/iOS client ID that authenticates via app signing instead of a
client secret. On native platforms, the bridge service automatically
uses the mobile client ID with PKCE only.
Also fix getRedirectUri() to return the custom scheme for all native
platforms (not just Android WebView), and align inline types in the
dialog component and plugin declaration with OAuthFlowConfig.
- Replace drag handle with SVG done-toggle circle with checkmark draw-on animation
- Improve mobile UX: cdkDragStartDelay for touch, bottom-sheet context menu
- Move issue/repeat indicators from drag handle to tag-list as chips
- Add isFinishDayEnabled config toggle
- Reorder context menu: add sub task before duplicate, deadline as own entry
- Fix context menu not opening on swipe left
- Fix race condition in done toggle animation timeout
Add OAuth support for plugins with PKCE, token persistence in local-only
IndexedDB, and Electron/web redirect handling. Extend two-way sync with
dueDay, dueWithTime, and timeEstimate field mappings including mutually
exclusive field clearing. Support remote issue deletion on task delete
and remote deletion detection during polling.
Add Google Calendar plugin (disabled from bundled builds pending legal
review) as a development reference for the plugin OAuth and two-way
sync APIs.
Additional fixes:
- Restore accidentally deleted focus-mode translation keys
- Remove full Task[] from deleteTasks action to prevent op-log bloat
- Add dueDay/deadlineDay format validation guards (#6908)
- Fix Android reminder alarm cancel intent matching
- Validate dueDay format to prevent false overdue from corrupted data
- Fix PKCE test expectation after utility consolidation
Move the voice reminder (domina mode) feature from a built-in Angular
feature into a self-contained plugin with its own manifest, i18n, and
config dialog. Add registerConfigHandler plugin API for settings button
on plugin cards. Include migration logic to auto-enable the plugin and
transfer config for users who had domina mode enabled.
- Fix currentTaskChange hook to dispatch full Task object instead of ID
- Extract shared BUNDLED_PLUGIN_PATHS constant to eliminate duplication
- Use firstValueFrom instead of deprecated .toPromise()
- Add voice name fallback matching for migrated configs
- Mark DominaModeConfig and selector as @deprecated
- Add unit tests for onCurrentTaskChange and config handler
Add a bundled plugin that opens a dialog with a textarea where each
line becomes a task. Includes project selector (defaults to inbox),
due date picker (defaults to today), draft auto-save via
persistDataSynced, and project theme color indicator.
Also extends the plugin API with dueDay support on PluginCreateTaskData,
raised button option on DialogButtonCfg, and default form element
theming in the plugin dialog component.
Add a plugin-based issue provider architecture that allows plugins to
register as issue providers alongside the existing built-in providers.
- Plugin API: types for search, display, comments, two-way sync, and
field mappings in @super-productivity/plugin-api
- Registry service to manage plugin provider lifecycle
- HTTP proxy with SSRF protection for plugin network requests
- Adapter service implementing IssueServiceInterface for plugins
- Sync adapter for plugin-based two-way sync
- Plugin config UI in the issue provider edit dialog
- Plugin providers shown in setup overview and issue panel
Migrate GitHub from built-in to plugin as first proof:
- Bundled github-issue-provider plugin with full feature parity
- Reducer migration converts old GitHub data to plugin format while
preserving legacy fields for cross-version compatibility
- Auto-enable plugin when existing GitHub providers are detected
- Plugin registers under 'GITHUB' key via MigratedIssueProviderKey
- GitHub translations moved to plugin i18n bundles
Mark Two-Way Sync as experimental in the UI.
* fix(e2e): stabilize undo task delete sync test
Two flakiness sources fixed:
- Click on task element could activate title inline editor, causing
Backspace to edit text instead of triggering delete. Now clicks the
drag handle which calls focusSelf() without entering edit mode.
- Replaced deleteTask helper with inline sequence to avoid wasting
2s of the 5s undo snackbar window on dialog-detection timeout.
* refactor: address code review findings from 2026-02-03
- Extract getBreakCycle helper to replace error-prone `cycle - 1 || 1`
pattern at 3 call sites
- Add clarifying comment on intentionally broad 'timed out' match
- Reduce Pomodoro E2E test from 9 to 5 sessions (sufficient coverage)
- Remove dead _isTransientNetworkError wrapper from DropboxApi
- Extract stubWindowConfirm helper in task reducer tests
* fix(sync): prevent Formly from clearing provider config on show (#6345)
resetOnHide: true caused Formly to reset field values when provider
fieldGroups transitioned from hidden to visible, discarding user input
if sync was enabled before selecting a provider.
* fix(tasks): fix huge space between emoji and text in tag/project menus
Use matMenuItemIcon attribute on emoji spans so they project into the
icon slot of mat-menu-item instead of the text slot. Update emoji icon
sizing to 24x24px to match mat-icon and add overflow: hidden.
Closes#5977
* fix(tasks): guard against undefined task entities in selectors and archive (#6359)
Prevent TypeError crashes (reading 'dueWithTime', 'dueDay', 'issueProviderId') caused
by orphaned IDs in NgRx state. Fix archive merge to deduplicate IDs, filter orphans,
and use correct entity precedence (young over old). Add defensive null guards to
selectors and archive/task service methods.
* fix(tasks): guard against undefined task in mainListTasksInProject$ (#6360)
* fix(tasks): detect and sanitize orphaned task IDs to prevent startup crashes (#6359, #6360)
Orphaned task IDs (entries in task.ids without matching entities) caused
TypeError on app startup. Fix addresses three layers: validation now
flags orphaned IDs instead of silently skipping them, loadAllData
sanitizes IDs on load as a safety net, and data repair no longer crashes
when encountering orphaned IDs it's trying to fix.
* fix(sync): prevent recurring task duplication across clients
Remove SuperSync special-case that bypassed initial sync wait, causing
repeatable task effects to fire before sync completed. Add post-sync
cleanup effect that detects and removes stale duplicate repeat instances
when multiple active instances exist for the same repeat config.
* fix(sync): restore WebDAV provider compatibility warning text
* feat(sync): mark WebDAV and LocalFile sync options as experimental
* feat(plugins): add UI Kit with inject-first CSS strategy for iframe plugins
Introduce a lightweight CSS reset (UI Kit) that auto-styles basic HTML
elements in plugin iframes to match the host app theme. Injected after
<head> so plugin styles always win by source order.
UI Kit provides: element resets (body, headings, buttons, inputs, tables,
links, code, lists, hr), .btn-primary/.btn-outline button variants, and
.card/.card-clickable components.
All bundled plugins updated to use UI Kit classes, removing redundant
custom CSS (-542 lines net). Pico CSS removed from automations plugin.
sync-md converted from hardcoded colors to host theme variables.
* feat(plugins): extract shared CSS utilities into UI Kit
Move .text-muted, .text-primary, .page-fade and @keyframes fadeIn from
plugin CSS into the UI Kit so all iframe plugins get them automatically.
Add box-shadow focus ring to input:focus for better accessibility.
Remove per-plugin focus overrides now covered by the UI Kit.
- Add i18n field to PluginManifest type in plugin-api package
- Create PluginI18nService for translation management
- Load translations from file paths or cached content
- Nested key lookup with dot notation (e.g., BUTTONS.SAVE)
- Smart fallback: current language → English → key
- Parameter interpolation with {{param}} syntax
- Language switching via signals
- Memory cleanup for unloaded plugins
- Add type validation for cached translation content
- Document language sync integration points
Part of plugin internationalization system implementation.
Tests will be added in Phase 8.
The reminderId field was deprecated in favor of remindAt. After migration
4.6, tasks store reminder timestamps directly in remindAt instead of
referencing a separate Reminder entity.
Changes:
- Remove reminderId from TaskCopy interface and plugin-api Task type
- Update all code checking reminderId to use remindAt instead
- Update data repair to clear legacy reminderId values
- Remove obsolete reminderId validation (reminders array is now empty)
- Update migration files to handle legacy types
- Update tests to reflect new behavior
- Add unit tests for isPersistentAction type guard.
- Fix compilation errors in task scheduling components caused by removed reminderId/removeReminderFromTask.
- Fix type error in create-sorted-blocker-blocks.spec.ts.
- Fix lint errors in various files.
Upgrade Electron from 37.7.0 to 39.2.5. Since Electron 38+ defaults to
Wayland via --ozone-platform=auto, force X11 on Linux to ensure reliable
idle detection (#1443) and global shortcuts. Users can opt-in to Wayland
with --ozone-platform=wayland or --force-wayland flags.
- Add log property to PluginAPI interface and implementation
- Extend PluginBridgeService to provide Log.withContext for each plugin
- Create simplified logger helper for sync-md plugin with fallback
- Replace console.log statements in sync-md with centralized logging
- All plugin logs now integrate with main app's Log class and history