Skip to content

T-0BFS-md-engine-attachments

State: closed/done · Impact: high · Complexity: medium

Pasting or dropping an image into the editor does nothing today; the provider contract has no write path for binary attachments. This task adds provider.saveAttachment, the attachments plugin with paste/drop handlers and pending markers, and insertAttachments for the chrome toolbar.

LocationRole today
packages/ts/obsidian-editor/src/types.ts#EditorIndexProvidersearchNotes, searchTags, resolveLink, assetUrl; read-only — no attachment write path
packages/ts/obsidian-editor/src/extensions.ts#obsidianEditorExtensionsNo paste or drop handling, no dropCursor()
solutions/determined/experiences/notes/app/composables/useEditorProvider.ts#useEditorProviderThe notes provider; implements only the read contract
packages/ts/obsidian-editor/test/helpers.ts#stubProviderStub provider the plugin tests extend with a saveAttachment spy

The plugin roster comes from T-4TE2-md-engine-plugin-contract; the catalogue and command payloads from T-EXQ7-md-engine-command-catalogue.

EditorIndexProvider gains saveAttachment?(file: File, notePath?: string): Promise<string>, resolving to the embed target name to write (e.g. pasted-2026-09-01.png).

packages/ts/obsidian-editor/src/plugins/attachments.ts, a defineEditorPlugin with extensions: dropCursor():

  • Paste and drop handlers claim the event only when the payload carries files and the provider has saveAttachment; otherwise the event falls through and text paste behaves exactly as today.
  • Each accepted file inserts a pending marker — a zero-length widget (cm-attachment-pending) whose position lives in a StateField and maps through every change. When saveAttachment resolves, the marker is replaced by name at the mapped position; when it rejects, the marker is removed and the document is untouched.
  • insertAttachments(view, files: File[]): Promise<void> runs the same path at the cursor — the export the chrome image button calls.
  • Catalogue addition insert.attachment (group insert, slash: false): run takes a File[] payload and calls insertAttachments; isEnabled is false when the provider lacks saveAttachment.

useObsidianEditor returns insertAttachments(files) bound to the view. The notes provider implements saveAttachment over the vault asset write path so wave 1 exercises the seam.

  1. Write packages/ts/obsidian-editor/test/plugins/attachments.plugin.test.ts first (happy-dom paste/drop events, saveAttachment spy with a controllable promise): accepted paste inserts a pending marker at the cursor; resolution with 'img.png' replaces it with img.png and no other byte changes; rejection restores the document byte-identically; typing between paste and resolution still lands the embed at the mapped position; without saveAttachment the handler does not claim the event.
  2. Add the insertAttachments case: two files insert two embeds in call order.
  3. Implement the plugin: handlers, marker StateField, resolution dispatch, dropCursor().
  4. Add insert.attachment to the catalogue with the payload run and isEnabled; extend the catalogue test.
  5. Add saveAttachment to the types; extend useObsidianEditor and the Vue test.
  6. Implement saveAttachment in the notes provider (write the blob through the vault asset path, return the vault-relative name); moon run product-workbench:check.
  7. bun test, tsc --noEmit, moon run obsidian-editor:bench.
LocationKindChange
packages/ts/obsidian-editor/src/plugins/attachments.tsnewattachments plugin: handlers, pending markers, insertAttachments
packages/ts/obsidian-editor/src/plugins/index.tsnewCreated by T-4TE2-md-engine-plugin-contract; attachments joins BUILTIN_PLUGINS
packages/ts/obsidian-editor/src/commands/catalogue.tsnewCreated by T-EXQ7-md-engine-command-catalogue; gains insert.attachment
packages/ts/obsidian-editor/src/types.tsmodifysaveAttachment? on the provider
packages/ts/obsidian-editor/src/vue/index.tsmodifyinsertAttachments(files) in the return
packages/ts/obsidian-editor/src/index.tsmodifyExport insertAttachments
packages/ts/obsidian-editor/test/plugins/attachments.plugin.test.tsnewPaste, drop, marker mapping, rejection and fall-through tests
packages/ts/obsidian-editor/test/vue.test.tsmodifyinsertAttachments returned
solutions/determined/experiences/notes/app/composables/useEditorProvider.tsmodifyImplement saveAttachment over the vault asset write path
  • AC-1: bun test packages/ts/obsidian-editor passes, including the new suite.
  • AC-2: a paste whose saveAttachment resolves 'img.png' yields img.png at the paste position with every other byte of the document unchanged.
  • AC-3: a rejected saveAttachment leaves the document byte-identical to before the paste and no pending marker remains.
  • AC-4: an insert typed before the promise resolves shifts the landing position — the embed appears at the mapped position, proven by the interleaved-edit test.
  • AC-5: without saveAttachment on the provider, the paste handler returns false and a text paste behaves as before this task (existing paste behavior asserted unchanged).
  • AC-6: insert.attachment exists in the catalogue; commandStates reports it enabled: false under the stub provider without saveAttachment and true with it.
  • AC-7: moon run product-workbench:check passes with the notes provider implementing saveAttachment.
  • AC-8: moon run obsidian-editor:bench stays within 1.5× of the recorded baseline.
  • The chrome “Attachment…” menu item and file input (T-98EU-md-ui-toolbar-insert-frontmatter).
  • Rendering the pasted image; the existing image-embed widget picks name up through provider.assetUrl unchanged.
  • Naming policy and collision handling for saved files; both live in the host’s saveAttachment.

Member of milestone M-931L; engine step E11 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.

T-EXQ7-md-engine-command-catalogue


← Back to Tasks