Tightens five smaller P2/P3 findings from the multi-agent review in
one batch:
- Slash menu: single-char keys with Ctrl/Cmd/Alt or during IME
composition (ev.isComposing) no longer extend the filter — fixes
Ctrl+S etc. silently accumulating in the filter and the IME
composition swallow. Enter on a zero-matches filter now closes
the menu and falls through to the editor instead of being eaten.
- Slash menu "New task" action: captures currentCtx at start, sets
slashActionInFlight to drop re-entrant triggers, and skips the
doc insert if the user switched contexts during the host
round-trip (the task is still saved in the host — only the chip
insert is deferred).
- taskRef / subTaskRef applyState now trusts task.isDone (the
host's source of truth) instead of `n.attrs.isDone || task.isDone`.
Previously a host-side clear would leave the chip visually stuck
"done" while the user was focused inside it.
- ensureSubtasksInJSON skips subTaskIds that aren't in the local
cache yet — they would otherwise render as empty "ghost" rows
with the is-missing style. They get inserted on the next
refreshTaskCache + insertSubtaskByParent cycle naturally.
- Slash-menu item rendering uses textContent for label and hint
(icon stays as innerHTML — the SVG path map is constant). Label
is constant today but pre-empts the XSS hazard if it ever sources
from a task title.
Closes the biggest gap in the move/drag UX surfaced by review #8:
moving a parent taskRef should bring its subtasks along, not strand
them under the previous sibling. Also closes#9 (drop indicator
stuck if the drag leaves the iframe).
Changes:
- New moveContentSliceToIndex replaces moveBlockToIndex. It accepts
a slice length so a single block (sliceLen=1) and a parent group
(sliceLen = 1 + trailing subTaskRefs) share one implementation.
Captures the slice's nodes and total size up front, applies a
single delete + sequential insert.
- sliceLenAt(idx) returns the atomic-move length: 1 for any block
except a taskRef, parent.nodeSize + sum of immediately-following
subTaskRefs for a parent.
- Move up / Move down (block menu) now uses the slice. subTaskRef
Move up/down refuses to cross the parent boundary; taskRef Move
jumps past the previous / next group as a whole, not the next
block.
- Drag flow: pendingDrag captures fromIdx + sliceLen + sourceType
at pointerdown. computeDropTarget snaps the target — subtask
source stays inside its parent's range; parent source advances
past any foreign subTaskRef run so the parent never lands between
a different parent and its first subtask. The .is-dragging dim is
applied to every block in the slice so the whole group lifts
visually.
- endBlockDrag now defends against drag-end never firing: window
blur and document.documentElement pointerleave (with no
relatedTarget, i.e. crossing the iframe boundary) abort any
pending drag so the drop indicator and dim state don't stick.
Addresses the P1 findings from the multi-agent branch review:
- setActiveContext is now guarded by a monotonic activeContextSeq:
concurrent context switches that resolve out-of-order bail after
each await instead of writing the previous editor doc under the
new context's id. Title write timers and pendingTitleWrites are
cleared on every switch so they can't mutate the next context's
cache.
- setContent catch path no longer silently overwrites a corrupt
blob: when parsing fails we set isDocCorrupt and gate scheduleSave
on it, so the empty seed shown in the editor is never persisted on
top of the original (possibly recoverable) JSON.
- findParentTaskIdBefore and validInsertRange now iterate
doc.childCount manually instead of doc.resolve(...).index(0).
Provably correct at every top-level gap; the resolve+index path
was flagged by review as mis-resolving for some positions.
- onAnyTaskUpdate only auto-appends a chip on a transition
absent → present in taskCache (tracked via lastSeenTaskIds).
Updates for existing tasks — including time-tracking ticks — no
longer re-insert chips the user has intentionally removed.
- Work-context header button re-registration is now replace, not
skip. Iframes that re-mount (work-view embed cycling with
skipCleanupOnDestroy: true) produce a new onClick closure pointing
at the new Window; the bridge would previously reject the new
entry as a duplicate and keep posting to the detached Window.
Spec updated to assert replacement instead of dedup-skip.
- Track lastWrittenTitles per task to seed a future, more robust
echo-vs-remote-change discrimination in refreshTaskRef (the
current 500 ms pendingTitleWrites window can still race with a
concurrent remote edit, but the deeper fix needs a follow-up).
Three issues hit while editing subtasks; all fixed in one pass:
- PluginAPI.log is declared on the type but never assigned in the
iframe's runtime PluginAPI (see plugin-iframe.util.ts). Every
PluginAPI.log.err call inside a .catch() callback then threw
"Cannot read properties of undefined (reading 'err')", which
surfaced as the user-visible crash on double-Enter on an empty
subtask. Replace all usages with a logErr helper that calls
PluginAPI.log?.err?.() and always falls back to console.error.
- deleteTask rejects with "Task data not found" when a stale chip
points at a task already removed in the host (sync removal, etc.).
The unhandled rejection cascaded with the log bug. Add
deleteTaskTolerant: skip if the task isn't in the local cache,
swallow "not found" responses, log anything else.
- After setParagraph() on an empty taskRef/subTaskRef, the selection
remained a NodeSelection on the new paragraph. A follow-up Enter
on a NodeSelection takes a different path in ProseMirror and could
resolve a bad position. Chain setTextSelection(nodePos + 1) so the
caret lands inside the paragraph as text.
- Subtask drag now stays inside the parent's group. Adds
validInsertRange(draggingPos): for a subTaskRef, returns the
[parentIdx+1, groupEnd] range. computeDropTarget clamps the
candidate insertion index and recomputes the indicator Y so the
drop line follows the clamped target. Top-level taskRef drag is
unchanged for now.
Two issues hit when the gutter "+" inserts a paragraph and then opens
the slash menu, and when clicks land on a hovered block that has
already been re-rendered:
- Slash menu landed in the top-left corner because the anchor used
getRangeAt(0).getBoundingClientRect(), which returns a zero rect at
(0, 0) for empty blocks (which is what we just inserted). Switch
the slash-menu anchor to editor.view.coordsAtPos(selection.from)
via a new caretRect() helper — ProseMirror always returns real
coords there. The same helper is now used by the slash-menu
keyboard handlers (arrow keys, backspace, typing filter) so the
menu stays anchored consistently.
- Uncaught "Position -1 out of range" came from
editor.view.posAtDOM(hoveredBlock, 0) returning -1 when the hovered
block was unmapped between hover and click (e.g. doc re-rendered,
block replaced). The gutter "+" and openBlockMenu both blindly
passed that value to doc.resolve, which threw. Bail when pos < 0,
and also when the resolved position has depth 0 (no enclosing
block, no nodePos to compute).
setActiveContext loads the per-context doc from storage and only fell
back to buildSeedDoc when no doc existed. Docs saved before subtask
support didn't contain subTaskRef blocks, so reopening such a context
showed parents with no children even though task.subTaskIds was
populated in the host.
Add ensureSubtasksInJSON: walks the loaded top-level content and, for
each taskRef, inserts any subTaskRefs from the host that aren't yet
present right after the parent's existing subtask group. Idempotent;
preserves existing order and never removes subtasks that have been
removed in the host (left to a future "sync remove" pass).
Adds first-class subtask rendering and editing in the document-mode
plugin, plus a couple of polish fixes:
- New `subTaskRef` node — same shape as taskRef (content: inline*,
taskId + isDone attrs, identical NodeView wiring) but rendered with
a `.sub-task-ref` class so CSS indents it under the parent and
draws a short connector line. Subtask Enter at end creates another
subtask under the same parent (resolved via findParentTaskIdBefore);
Enter on an empty subtask deletes the task and outdents.
- Seeding (buildSeedDoc) now emits each parent followed by its
subTaskRefs from task.subTaskIds, so opening a context shows the
full task tree. migrateStoredDoc handles both taskRef and
subTaskRef. collectKnownTaskIds, reconcileTitlesFromDoc,
refreshTaskRef and isTaskRefFocused all walk both node types.
- appendMissingTask now routes subtasks to insertSubtaskByParent
(which places the new subTaskRef after the parent's existing
subtask group), so an externally-added subtask appears under
its parent instead of at the doc tail.
- Enter at the end of a parent taskRef now uses
positionAfterParentGroup so the new sibling lands past any
subtasks — previously it would interleave with the parent's
children.
- Done-toggle is now a squircle (`<rect rx="5">` instead of
`<circle>`) to match the shape used in the actual app's task list.
- Replace the HTML5 drag-API grip with a pointer-event-driven drag.
The native API was unreliable because the grip lives outside the
ProseMirror view (in document.body), and the editor's contentEditable
region interfered with dragstart on a sibling button. Pointer events
also give us setPointerCapture for stable tracking and a clean
click-vs-drag split (with a justDragged guard so the synthetic click
doesn't open the block menu right after a drop).
- Slash and block menus now add window.scrollX/Y when positioning,
so they stay anchored to the caret when the iframe is scrolled.
Previously the popovers used viewport-relative coords with
position:absolute and drifted off when the document scrolled.
- Task chips now render the app's <done-toggle> visual instead of a
native checkbox: faint outline circle with an animated checkmark
that fades in once done. Mirrors src/app/ui/done-toggle styling
(stroke widths, opacities, transitions) so chips feel like the
rest of the app.
Adds three editor UX improvements in the document-mode plugin iframe:
- Move up / Move down entries in the block menu, plus full HTML5
drag-to-reorder via the grip handle (with a horizontal drop
indicator between blocks).
- Slash and block menus flip above the caret when there is no
room below, avoiding clipped popovers near the viewport bottom.
- Inline-SVG Material icon fallback so the editor renders
correctly offline (drops the Google Fonts dependency).
Two issues:
1. Enter/Backspace inside a task chip had no special handling. Enter
would split a taskRef into two with the same taskId (broken).
Backspace at start could merge the chip's content into the previous
block, detaching the title from its task entity.
2. The block hover gutter sat outside #editor-root. Moving the mouse
from a block toward the gutter crossed an unwatched gap, firing
mouseleave on the root and hiding the gutter before the mouse
reached it.
Fixes:
- TaskRefNode.addKeyboardShortcuts:
- Enter at end of chip → addTask + insert new chip below; cursor
lands inside it. Async; chain runs after refreshTaskCache.
- Enter on an empty chip → convert to paragraph + deleteTask.
- Enter in middle → swallow (no-op for POC).
- Backspace at start of empty chip → deleteTask + remove chip.
- Backspace at start of non-empty chip → swallow (no merge).
- #editor-root padding-left bumped to 60px so the gutter (positioned
52px left of each block) renders inside the root — the mouse stays
inside the root the whole time.
- .block-gutter adds padding-right so its hit area extends rightward
toward the block, and uses [style*='flex'] as the visibility cue
rather than the previous brittle hover chain.
Task chips were atom nodes that just displayed the title and floated as
cards, with extra block spacing between them. Now they're content-bearing
nodes whose inline content IS the task title — typing inside one edits
the linked task.
- taskRef switches from atom to content: 'inline*', exposes contentDOM
on the .title span so ProseMirror manages the editable content.
- isDone moves to an attr (instead of read-from-cache) so undo/redo
carries it; the checkbox dispatches a setNodeAttribute transaction.
- buildSeedDoc populates each taskRef with the task title from cache.
- migrateStoredDoc walks older atom-style stored docs and backfills
content from the cache so they load under the new schema.
- Title write-back: walking the doc on each onUpdate, per-task debounced
updateTask. pendingTitleWrites + isTaskRefFocused guard the
reconciler from clobbering an active edit with the echo of our own
write (or an unrelated ANY_TASK_UPDATE).
- ANY_TASK_UPDATE now refreshes only the affected node via a precise
replaceWith on the taskRef's inline range — preserves cursor on
surrounding blocks.
- CSS: zero margin between consecutive taskRefs, baseline-aligned
checkbox, no card background — the chip reads as a list item.
Layer Notion-like interaction on top of the TipTap editor:
- Block hover gutter on the left margin with `+` (insert below + open
slash menu) and `⋮⋮` (select node and open block menu) buttons.
- Bubble menu on text selection with Bold / Italic / Strike / Code
(via @tiptap/extension-bubble-menu).
- Slash menu shows icons next to labels, supports keyboard nav, and
now includes bullet list, numbered list, quote, code block, and
divider in addition to paragraph / headings / new task.
- Block menu (from ⋮⋮) offers turn-into for paragraph + H1-H3,
duplicate, and delete.
- Tighter CSS — Material Icons via Google Fonts, cleaner spacing
matching Notion-style density, selectednode highlight on taskRef.
Document mode is now remembered per context across sessions. Toggling
the header button writes the ctxId to enabledCtxIds in the shared blob;
on app start (and on every WORK_CONTEXT_CHANGE), the background script
auto-shows or -closes the embed based on whether the active context is
enabled.
The editor's debounced save now does a read-modify-write so it doesn't
clobber enabledCtxIds (or any other field future code adds to the blob).
Previously clicking the button only enabled the embed; user was stuck
in document mode with no way back to the task list. Track isShown in
memory and toggle showInWorkContext / closeWorkContextView. State is
not persisted — restart returns to the task list.
A bundled plugin that replaces the work-view task list with a TipTap
editor for projects and the TODAY tag. Task references are read-only
chips with a checkbox; everything else is normal ProseMirror content
(paragraphs, headings, dividers) plus a simple slash menu for inserts.
- Iframe plugin scaffolded under packages/plugin-dev/document-mode with
esbuild bundling. The editor.js bundle is inlined into index.html
because the blob-URL iframe can't resolve relative scripts.
- background.ts registers a work-context header button with
showFor: ['PROJECT', 'TODAY']; click calls showInWorkContext().
- editor.ts: TipTap (core + StarterKit + Placeholder) with a custom
taskRef atom node-view (checkbox + title from getTasks() cache).
Subscribes to WORK_CONTEXT_CHANGE for nav and ANY_TASK_UPDATE for
title/done-state refresh. Persists per-ctx ProseMirror JSON inside
the existing single plugin blob ({ docs: { [ctxId]: doc } }) with
5 s debounced save and a pagehide flush.
- Plugin auto-bundled by packages/plugin-dev/scripts/build-all.js and
registered in BUNDLED_PLUGIN_PATHS.
Out of scope for the POC (documented in the plan): inline-editable
task titles, keyed persistDataSynced API, removal of in-tree
document-mode, data migration, Electron beforeUnload hook.
The new theme upload button's matTooltip ("...never run code...") leaves its
text in the DOM via cdk-describedby, so case-insensitive getByText('never')
matched both the "Never" reminder option and the tooltip. Target the
mat-option by ARIA role and exact case, and scope the post-schedule
assertion to the dialog so only the mat-select trigger text qualifies.
Also prunes a stale nested vitest/esbuild tree from the caldav plugin's
package-lock.json.
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.
Bundle ical.js into the plugin and use it for the read path. The hand-rolled
parser stays for getById/updateIssue/deleteIssue. Anchor the sync window to
start-of-UTC-day so in-progress events stay visible, count only emitted
occurrences toward the per-event safety cap, and isolate per-VEVENT failures
so one malformed event can't drop the rest. Compound id uses '#occ=<ms>' so
a server-controlled href cannot collide with the occurrence delimiter.
Make explicit in the backlog-query help text that the default behavior
without a token imports every open issue, not just the user's, so anyone
enabling auto-import without credentials understands the scope.
The default backlog query 'sort:updated state:open assignee:@me' fails with
HTTP 422 ("The listed users cannot be searched...") when the provider has
no token configured, because GitHub's Search API can't resolve @me without
an authenticated request. This broke auto-import for public repos, which
the docs and UI advertise as not requiring a token.
Fall back to 'sort:updated state:open' when no token is set, and update
the help text to describe both defaults.
Closes#7381
Adds an advanced checkbox on the GitHub issue provider that, when enabled,
drops the hardcoded `is:issue` filter in both backlog auto-import and
in-provider search. Lets users sync PRs (assigned, authored, or
review-requested) into their backlog with a query like
`state:open is:pull-request (assignee:@me OR review-requested:@me)`.
Default is off, existing behavior is unchanged.
packages/vite-plugin/package.json was bumped to vite ^6.4.2 in
4b9bd23e56 but dependent plugin lockfiles still referenced ^6.0.0.
npm install during the e2e build brought them in sync and hoisted
vitest's nested esbuild binaries, dropping ~500 redundant entries.
encodeURIComponent leaves ( and ) intact per RFC 3986, but GitHub's
search API treats them as grouping operators that must be percent-encoded,
returning HTTP 422 on queries like "(author:@me OR assignee:@me)".
The in-repo provider was fixed in 960330dd56 but the fix didn't carry
over when GitHub moved to a plugin. Reapply via a small encodeGithubQuery
helper used at both call sites.
* fix(issue): prevent crash from orphan issueProviderId (#7135)
The Jira image-headers effect in task-detail-panel subscribed to
selectIssueProviderById without an error handler, so a task with an
issueProviderId pointing at a deleted provider (e.g. after sync
convergence where taskIdsToUnlink didn't cover all local tasks)
propagated the selector throw to Zone.js as a crash dialog. Wrap the
inner selector observable in catchError that logs and falls back to
of(null); the downstream jiraCfg?.isEnabled guard handles the fallback.
Also drop IssueLog.log(issueProviderKey, issueProvider) from the
throwing variant of the selector: providers may carry credentials
(host, token, apiKey) and IssueLog history is exportable.
* fix(focus-mode): sync tray countdown with in-app timer during breaks
Tray title was rebuilt from a cached currentFocusSessionTime that only
refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an
active current task, so during focus-mode breaks or task-less focus
sessions the cache froze while the in-app timer kept ticking.
Add the tick action to taskChangeElectron$ so the cache refreshes every
second whenever the focus timer is running.
Fixes#7278
* fix(ci): restore GitHub Actions SHA pins undone by 0e9218bd68
Commit 0e9218bd68 silently reverted PR #7212 (github-actions-minor group
bump) along with its stated sync/client-id work. This restores the 15
workflow files to their pre-revert state.
Actions restored to newer pinned SHAs:
- actions/upload-artifact v7.0.0 -> v7.0.1
- step-security/harden-runner v2.16.1 -> v2.17.0
- softprops/action-gh-release v2.6.1 -> v3.0.0
- signpath/github-action-submit-signing-request v2.0 -> v2.1
- anthropics/claude-code-action v1.0.89 -> v1.0.93
- docker/build-push-action v7.0.0 -> v7.1.0
- easingthemes/ssh-deploy v5.1.1 -> v6.0.3
* fix: restore i18n, UI, and docs work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following work alongside its stated
sync/client-id changes. Files where later master commits (fec7b25f23, etc.)
already re-applied the reverted work are intentionally left untouched.
Restored:
- #7232 docs/long-term-plans/location-based-reminders.md (513 lines)
- #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines)
- #7049 Polish translation improvements
- #7143 planner component styling (4 scss files)
- #7211 add-task-bar preserve time estimate when typing title
- #7208 task.reducer roll-up estimates for added subtasks
- #6767 focus-mode pomodoro reset button
- #7205 plugin-dev github-issue-provider TOKEN description
- #7231 mobile-bottom-nav FAB fix
- a4fe03272 iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown)
- 309670db3 ShortSyntaxEffects undefined guard
- 667a7986f Dropbox PKCE auth comment/behavior
* fix(electron): restore electron + e2e work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following electron/e2e work.
Files already re-fixed by later master commits are left as-is:
- e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976 + 191d129ff3)
Restored:
- e8a3e156eb fix(electron): Linux autostart IDB backing-store recovery
(re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring)
- 5ce78a5b63 fix(electron): macOS Cmd+Q / Dock > Quit hang
(setIsQuiting + before-quit delegate to close-handler)
- 46e0fa2d01 fix(sync): FILE_SYNC_LIST_FILES IPC contract
(electronAPI.d.ts + local-file-sync + preload + ipc-events)
- ea1ef16307 fix(android): session-only SAF permissions on OEM devices
- 8865dc0a50 test(e2e): supersync parallel-worker stampede guard
(SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop)
- af7c7687e2 test(e2e): block WS-triggered downloads in non-WS specs
- 265b44db5d test(e2e): premature waitForURL on daily-summary
(this is literally the fix the bad commit's message claimed to add)
Conflict resolutions:
- e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement
checks from d64014d086 (later than c558bcab5e) while restoring the
goto retry loop from 8865dc0a50.
- electron/start-app.ts: unioned imports (setIsQuiting +
clearStaleLevelDbLocks from theirs, fs from ours).
* fix(sync): restore sync-core work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted parts of several sync fixes. Most
sync-core reverts have already been re-addressed differently on master
by later commits (1f5184f6e7, 05cd875dd6, 09f5ced2c9, 7df43358ab,
d9158d6adb, 32dbc95ed9, 8c3b08e016, f89fe1ebc3) — those files are
intentionally left untouched to avoid reverting master's newer work.
This PR restores only the pieces that are genuinely still missing:
- e8a3e156eb fix(electron): IDB backing-store autoreload (in-app piece)
operation-log-hydrator.service.ts + .spec.ts (the electron/clear-
stale-idb-locks.ts piece was restored in the prior commit)
Plus three low-risk documentation/cleanup restorations:
- operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc
- dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb)
- dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform
comment explaining why manual code entry flow is used (667a7986fb)
- file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01)
Intentionally NOT restored (master's newer work covers or supersedes):
- sync-trigger.service.ts / sync.effects.ts (05cd875dd6)
- sync-wrapper.service.ts (1f5184f6e7)
- sync-errors.ts (1f5184f6e7 re-added LegacySyncFormatDetectedError)
- file-based-sync-adapter.service.ts + spec (1f5184f6e7 + d9158d6adb)
- file-based-sync.types.ts (1f5184f6e7)
- operation-log.const.ts (32dbc95ed9 bumped IDB_OPEN_RETRIES to 5)
- dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
* Implement task parsing with sub-tasks
Added a new function to parse tasks with sub-tasks from a text input, improving task management capabilities. Updated the task addition logic to utilize this new parsing method.
Should solve #7183
* Enhance task parsing and user information
Key Changes Summary:
✅ Critical Fix#1 - 4-space indentation: Detects minimum indent and normalizes relative to it
✅ Critical Fix#2 - Mixed input: Plain-text lines no longer silently dropped; warning shown
✅ Critical Fix#3 - Deeply-nested items: Now flattened to sub-task level with user notification
✅ Minor - Sub-task dueDay: Documented that it inherits from parent (not passed independently)
✅ Suggestion - UI/UX: Updated placeholder text to show expected format with examples
* fix indentation calculation and missing isBullet: true
* fix(brain-dump-plugin): keep sub-tasks when plain-text lines interrupt
Previously, a plain-text line between a bullet and its indented sub-task
caused the sub-task to be silently dropped: the outer loop advanced past
the plain-text entry but the inner look-ahead had already exited at
indent 0, so the following indented bullet was never attached to the
parent and was rejected by the outer loop's else branch.
The sub-task scan now skips over plain-text lines while still breaking
at the next top-level bullet, so indented bullets attach to the previous
parent regardless of interleaved plain text. The plain-text line itself
is still emitted as its own top-level task by the outer loop.
Also drops dead code:
- findMinimumIndent() was declared but never called.
- dueDay on sub-task payload was forwarded but silently discarded by the
plugin bridge (sub-tasks inherit the parent's date).
And de-duplicates the "Note: ... Note: ..." prefix in the deeply-nested
warning snack.
---------
Co-authored-by: Adnoh <git@rotzefull.de>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
* test(e2e): update Show/hide task panel button label
The TOGGLE_DETAIL_PANEL translation was renamed from "Show/Hide
additional info" to "Show/hide task panel" in dd789d2, but e2e
selectors still referenced the old string, causing every test that
opens the task detail panel to time out.
https://claude.ai/code/session_011mX4Pr24S42svmA5j7fRux
* 18.2.1
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper
- Handle corrupted remote data gracefully instead of throwing
- Add getOrGenerateClientId() as unified entry point, eliminating dual injection
of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption,
and sync-hydration services
- Use crypto.getRandomValues() instead of Math.random() for client ID generation
- Warn user when stored client ID is invalid and must be regenerated
- Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)
When a full-state op (SYNC_IMPORT/BACKUP_IMPORT) arrived from another
client, mergeRemoteOpClocks reset the local clock to a "minimal" form
but only preserved the current client's counter from the incoming op's
clock. If the current client had issued ops (e.g. GLOBAL_CONFIG) not
reflected in the incoming full-state op's clock, its counter was dropped,
causing subsequent ops to reuse the same counter value. Downstream clients
then saw these ops as EQUAL (duplicate) and skipped them silently.
Fix: take max(mergedClock[clientId], currentClock[clientId]) when
rebuilding the clock after a full-state op reset.
Also add __SP_E2E_BLOCK_WS_DOWNLOAD flag to WsTriggeredDownloadService
to allow E2E tests to block automatic WS-triggered downloads during
concurrent conflict scenarios.
Fix archive conflict test by blocking WS downloads on Client A during
the concurrent edit phase so it doesn't auto-receive B's rename via
WebSocket before archiving (restoring the intended conflict scenario).
Fix LWW singleton test to assert convergence rather than specific winner.
Fix renameTask helper to avoid Playwright/Angular re-render races.
Fix shepherd.js import paths that broke the Angular dev server build.
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.