Commit graph

26 commits

Author SHA1 Message Date
Johannes Millan
eeaa745042 chore(document-mode): mark plugin title as Alpha 2026-05-22 16:49:28 +02:00
Johannes Millan
3c1b446c00 feat(document-mode): coherent block-menu task ops; fix create race
- block-menu Delete on a task chip now deletes the host task (and a
  parent's whole subtask group) so chips no longer resurrect on reload
- add "Turn into task": promote a paragraph/heading into a task chip
- hide turn-into / Duplicate for task chips — converting or cloning a
  chip orphaned the task or produced a duplicate taskId
- seed the task cache on create instead of awaiting a full getTasks
  round-trip, so fast typing into a new chip isn't dropped
2026-05-22 15:35:40 +02:00
Johannes Millan
a27009b698 feat(document-mode): improve accessibility, theming and error states
- fix .task-ref:hover using an un-injected var (invisible in dark mode)
- make the done-toggle keyboard-operable with state-aware aria-labels
- add :focus-visible rings, ARIA roles and prefers-reduced-motion
- show a banner for corrupt/future-version docs and a connect-failure
  message instead of a silently blank panel
- populate slash-menu shortcut hints; scroll the active item into view
- fix muted-text contrast; replace brittle gutter-visibility selector
2026-05-22 15:30:20 +02:00
Johannes Millan
d0e098dde1 feat(plugins): add selectTask API and document-mode detail-panel button
New PluginAPI.selectTask(taskId): the host validates the task exists,
then calls TaskService.setSelectedId, opening the task's detail panel in
the right-hand panel — which is independent of the work-view body, so it
works while a plugin embed occupies it.

Document Mode adds a third block-gutter button, shown only when hovering
a task chip, that calls selectTask for that chip's task. The gutter now
self-measures its width so the 2- and 3-button layouts both align.

Wired across all three packages: the PluginAPI interface, the per-plugin
API wrapper, the iframe API forwarder, and the host bridge.
2026-05-22 15:11:51 +02:00
Johannes Millan
5a56b5d8b0 feat(document-mode): persist chip reorder; modularize editor
Bundled because all three changes share editor.ts and the pre-existing
working-tree state could not be split per-hunk:

- feat: a chip drag / block-menu move now writes the new order back via
  PluginAPI.reorderTasks (top-level order for PROJECT contexts, subtask
  order for any context), guarded permutation-only so a missing chip can
  never drop a task from project.taskIds / task.subTaskIds.
- refactor: split editor.ts (2238 -> 1846 lines) into icons.ts, the pure
  doc-nav.ts navigation helpers, and a createTaskRefNode factory that
  replaces the ~95% duplicated TaskRefNode / SubTaskRefNode.
- fix: onAnyTaskUpdate fast-paths in-place single-task updates from the
  hook payload, skipping a full getTasks() round-trip.
- test: doc-nav.spec.ts covers the drag/move helpers.
2026-05-22 13:42:30 +02:00
Johannes Millan
73e3715dd0 refactor(document-mode): add pure doc-transform module and test runner
Extract the load-time document pipeline (schema migration, ctx
reconciliation, subtask backfill, chip refresh) into a DOM-free
doc-transform module, and add a node:test runner (scripts/test.js) so
the plugin's pure logic can be unit tested without a browser harness.
2026-05-22 13:42:21 +02:00
Johannes Millan
cc3d9eaaec fix(document-mode): rebuild top-level chips in ctx.taskIds order
Diagnostic showed: in TODAY the chip *set* matched ctx.taskIds, but the
*order* didn't — saved-doc order won, while the regular TODAY view uses
ctx.taskIds (TODAY_TAG.taskIds). Plus the stored doc had duplicate
subtask rows under several parents (a residue of an earlier bug).

Rewrite reconcileTopLevelTaskRefs:

  - Pass 1 walks the stored content. For each parent taskRef it
    collects the parent + its trailing subTaskRef run as a "group",
    deduping subtasks within the group by taskId. The first stored
    group for a given parent wins; later duplicate groups for the
    same parent are dropped. Non-chip blocks (paragraphs, dividers,
    extra headings) are stashed; the first heading is preserved as
    the doc title at the top. Orphan subTaskRefs are dropped.

  - Pass 2 rebuilds the doc content. The title heading stays first.
    Chip groups follow ctx.taskIds order strictly — stored group if
    we have one, fresh from taskRefWithSubtasksJSON if the parent
    is new to this context. Custom non-chip blocks land at the end
    in their original relative order so they aren't lost.

Trade-off (documented in ADR #5 Open Limitations): paragraphs the
user had interleaved between chips lose their original anchor and
end up at the tail. The previous behaviour preserved their position
but at the cost of stale ordering and visible duplicates, which is
worse.

Also removed the temporary diagnostic console.logs.
2026-05-21 22:51:32 +02:00
Johannes Millan
6e86908320 fix(document-mode): reconcile stored doc against ctx.taskIds on load
TODAY in doc-mode didn't match the regular TODAY view because the
persisted blob is keyed by ctx.id and TODAY's membership changes daily:

  - Tasks that left TODAY since the last save stayed in the doc.
  - Tasks that newly joined TODAY weren't injected — ensureSubtasksInJSON
    backfills subtasks only.

Same drift hits TAG contexts on tag membership changes.

Add reconcileTopLevelTaskRefs as a middle step in prepareStoredDoc
(now a three-step pipeline: migrate → reconcile → ensureSubtasks):

  - Drops top-level taskRef chips whose taskId isn't in ctx.taskIds,
    along with the contiguous subTaskRef run that belongs to that
    parent group.
  - Drops orphan subTaskRefs left behind by an older save where the
    parent was already gone.
  - Appends any ctx.taskIds entries that aren't yet in the doc, in
    ctx order, before the trailing paragraph (so the empty line
    stays at the doc tail).
  - Non-chip blocks (headings, paragraphs, dividers, etc.) are
    preserved in place so the user's custom content survives.

Also restored the JSDoc on migrateStoredDoc / ensureSubtasksInJSON
that drifted during earlier edits.
2026-05-21 22:27:05 +02:00
Johannes Millan
7470c0d16d fix(document-mode): track in-context, not cache-global, for TODAY etc.
The recent appendMissingTask fix (#6) compared payload.taskId against
lastSeenTaskIds set to *every* id in taskCache. That treated "first
time we see this task in this user's data" as the transition signal,
which broke TODAY:

  - A task that already lives in another project is "known" globally
    the moment the cache loads. Later, when the user gives it the
    TODAY tag or sets a dueDay, ANY_TASK_UPDATE fires — but the id
    is still in lastSeenTaskIds, so wasKnown is true and the chip
    never appears in the TODAY doc. Same regression for TAG views.

Scope lastSeenTaskIds to the *current context's* tasks instead:

  - PROJECT: tasks whose projectId matches the ctx
  - TODAY:   tasks with the TODAY tag OR a dueDay OR a dueWithTime
  - TAG:     tasks whose tagIds include the ctx id
  - Subtasks (task.parentId set) are excluded — they flow through
    insertSubtaskByParent, not through the top-level append path.

Now a transition out-of-context → in-context triggers append while
in-context updates (time-tracking ticks etc.) still don't replay the
chip. The previous "globally new" check is replaced by a per-context
set diff, computed via snapshotInContextTaskIds(ctx).
2026-05-21 22:22:50 +02:00
Johannes Millan
8966a1df00 chore(plugins): polish bundle — JSDoc, busy-poll cap, hooks dedup
Small follow-ups from the multi-agent review:

- WORK_CONTEXT_CHANGE / ActiveWorkContext JSDoc spells out that
  taskIds is a snapshot at emit time; plugins should re-read via
  getActiveWorkContext or getTasks if they need the current value.
  (#15)

- waitForPluginAPI is now bounded — 250 attempts at 20 ms each, so
  if the host never injects PluginAPI the iframe logs and bails
  instead of busy-polling forever. (#23)

- Iframe-side Hooks enum is generated from the real PluginHooks
  source via JSON.stringify({...PluginHooks}). The previous
  hand-mirrored copy was an easy drift hazard when adding a new
  hook. (#24)

- Named pipeline: prepareStoredDoc wraps
  ensureSubtasksInJSON(migrateStoredDoc(...)) so the order
  invariant (migrate first, then backfill) is explicit at the
  callsite. (#20)
2026-05-21 21:39:46 +02:00
Johannes Millan
670a48c77e fix(document-mode): mount guard, bubble cross-block, version forward-compat
Three smaller lifecycle / correctness fixes from the review:

- mount() is now idempotent (isMounted flag). The iframe is normally
  rebuilt from scratch on embed open, but HMR or unusual host
  re-init flows could call mount() twice — every body-appended
  element and every document-level listener would then duplicate.
  One-mount-per-iframe is enforced.

- BubbleMenu.shouldShow now walks the full selection range with
  nodesBetween instead of only checking the start position's node.
  A selection that crosses an atom (e.g. paragraph → divider →
  paragraph) no longer shows the inline-mark menu — toggling bold
  across the divider wouldn't have done anything useful anyway.

- readBlob: future-version guard. If the stored blob's version is
  ahead of STORAGE_VERSION (user synced from a newer build), refuse
  to load and set isStorageUnreadable. scheduleSave is now gated on
  it too, so a downgrade can't overwrite the user's newer blob with
  our empty fallback. Recovery is automatic on update to a build
  that understands the newer schema.
2026-05-21 20:48:27 +02:00
Johannes Millan
9db74ba1d0 fix(document-mode): slash-menu polish + isDone trust + safer DOM
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.
2026-05-21 20:32:24 +02:00
Johannes Millan
d0e5373ddf feat(document-mode): parent + subtasks move as one group
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.
2026-05-21 20:08:00 +02:00
Johannes Millan
fb306e34b8 fix(document-mode): P1 sweep — context-switch races, flood, re-reg
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).
2026-05-21 19:21:12 +02:00
Johannes Millan
b7140027d9 fix(document-mode): tolerant delete, safe logging, subtask drag clamp
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.
2026-05-21 18:58:02 +02:00
Johannes Millan
17c6eae37c fix(document-mode): correct slash-menu anchor + guard posAtDOM -1
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).
2026-05-21 18:47:14 +02:00
Johannes Millan
a37ae1e150 fix(document-mode): backfill missing subtasks into previously-saved docs
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).
2026-05-21 18:41:42 +02:00
Johannes Millan
e428e20700 feat(document-mode): subtask support, squircle done-toggle
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.
2026-05-21 18:39:29 +02:00
Johannes Millan
1931455ac1 fix(document-mode): reliable grip drag, app-style task chips
- 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.
2026-05-21 18:29:24 +02:00
Johannes Millan
aa7b27c547 feat(document-mode): block reorder, smart popovers, inline-SVG icons
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).
2026-05-21 18:10:01 +02:00
Johannes Millan
aa7b2d1e9d feat(document-mode): keyboard handling for task chips, reachable gutter
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.
2026-05-21 17:47:54 +02:00
Johannes Millan
cae32b6db7 feat(document-mode): editable task titles, tight inline list layout
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.
2026-05-21 17:40:53 +02:00
Johannes Millan
73487e5c17 feat(document-mode): Notion-style UX (gutter, bubble menu, block menu)
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.
2026-05-21 17:34:13 +02:00
Johannes Millan
f695460bf8 feat(document-mode): persist per-context enable state
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).
2026-05-21 17:29:35 +02:00
Johannes Millan
47b7346742 fix(document-mode): make header button toggle on/off
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.
2026-05-21 17:24:06 +02:00
Johannes Millan
d429900665 feat(document-mode): add TipTap-based document-mode plugin (POC)
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.
2026-05-21 17:22:31 +02:00