---
author: architect
responsible: architect
phase: 37
phase_type: implementation
status: Ready for execution
hard_bar: false
security_relevant: false
design_fork: false
wave: 1
depends_on: []
files_modified:
  - web/src/components/LivePreviewEditor.tsx
  - web/src/routes/PageEditor.tsx
  - web/src/api/client.ts
requirements: [EP-03]
autonomous: true
---

# Phase 37: Insert-at-Cursor Templates - Plan

**Goal:** Insert a `Templates/` snippet at the cursor inside the open `LivePreviewEditor`
via a normal CM6 transaction.

**Requirements:** EP-03

## must_haves

```yaml
truths:
  - id: EP-03-1
    statement: "An editor affordance lists templates via the existing listTemplates endpoint and inserts the chosen template's substituted body ({{title}}/{{date}}/{{time}}) at the current cursor position"
    verification: "grep 'listTemplates\|insertTemplate\|template' web/src/routes/PageEditor.tsx web/src/components/LivePreviewEditor.tsx"
  - id: EP-03-2
    statement: "Insertion dispatches a normal CM6 transaction on the live EditorView (onChange fires verbatim on docChanged); the externalSeed reset annotation is NOT used"
    verification: "grep 'replaceSelection\|dispatch' web/src/routes/PageEditor.tsx; grep -c 'externalSeed' web/src/routes/PageEditor.tsx returns 0 (not used for insert)"
  - id: EP-03-3
    statement: "Insertion respects the safe-path resolver for the template read; no placeholder executes code; inserting into a non-empty document does not corrupt surrounding text"
    verification: "grep 'getPage' web/src/routes/PageEditor.tsx (template body read via the existing safe-path endpoint); cd web && npx tsc --noEmit"
```

## Tasks

### Task 1: Frontend — template picker + insert-at-cursor (Wave 1)

<read_first>
- web/src/components/LivePreviewEditor.tsx (the CM6 editor; EditorView exposed via CmEditorEl.cmView.view at :50; onChange/docChanged at :24; externalSeed at :56)
- web/src/routes/PageEditor.tsx (the page editor host — the insert affordance lives here)
- web/src/api/client.ts (listTemplates, getPage, TemplateInfo type)
- web/src/components/Dialog.tsx (the existing dialog component pattern)
</read_first>

<action>
**1. Add an "Insert template" affordance** in `PageEditor.tsx` (or in a toolbar above/beside the `LivePreviewEditor`):
- A button "Insert template" that, on click, opens a dropdown/dialog listing templates (fetched via `listTemplates()`).
- The list shows each template's name (from `TemplateInfo.name`). Optional: a preview of the first line.
- Selecting a template:
  1. Fetches the template body via `getPage(template.path)` → the `Page` response includes `body` (and `frontmatter`).
  2. Substitutes placeholders client-side: `{{title}}` → the current page title, `{{date}}` → today's date (YYYY-MM-DD), `{{time}}` → current time (HH:MM). (Same substitution as the create-from-template flow.)
  3. Inserts the substituted body at the cursor via a CM6 transaction on the live EditorView.

**2. Access the EditorView** — the `LivePreviewEditor` exposes it via `CmEditorEl.cmView.view` (`:50`). Wire this via:
- A `ref` on the `LivePreviewEditor` host element, OR
- A callback prop `onEditorViewReady={(view) => { editorViewRef.current = view; }}` that hands the `EditorView` to `PageEditor`.
- The planner picks the wiring (recommend a `ref` on the editor host element — `editorRef.current.cmView?.view`).

**3. Insert via a normal CM6 transaction:**
```typescript
const view = editorRef.current?.cmView?.view;
if (!view) return;
const substituted = substitutePlaceholders(templateBody, pageTitle);
view.dispatch(view.state.replaceSelection(substituted));
```
- `replaceSelection` replaces the current selection (or inserts at the cursor if no selection) — standard CM6.
- `onChange` fires verbatim on `docChanged` (the updateListener ships `doc.toString()`).
- The `externalSeed` annotation is NOT used — this is a user edit, not a prop-driven reset.

**4. Add `substitutePlaceholders` helper** — a small function replacing `{{title}}`, `{{date}}`, `{{time}}` in the template body. (Same logic as the create-from-template client-side substitution.)

**5. Add a test** — verify the insert dispatches a transaction (unit test on the substitution helper; integration test if feasible that verifies the editor content changes after insert).

**6. Add CSS** for the template picker dropdown if needed.
</action>

<acceptance_criteria>
- `grep 'listTemplates\|insertTemplate\|Insert template' web/src/routes/PageEditor.tsx` returns a match (the affordance exists)
- `grep 'replaceSelection\|dispatch' web/src/routes/PageEditor.tsx` returns a match (CM6 transaction)
- `grep 'externalSeed' web/src/routes/PageEditor.tsx` returns NO match (the reset annotation is not used for insert)
- `grep 'substitutePlaceholders\|{{title}}\|{{date}}\|{{time}}' web/src/routes/PageEditor.tsx` returns a match (substitution exists)
- `cd web && npx tsc --noEmit` exits 0
- `cd web && npx vite build` exits 0
</acceptance_criteria>

## Verification

- **Goal-backward (qa):** An editor affordance lists templates, inserts the chosen template's substituted body at the cursor via a normal CM6 transaction (onChange fires), the externalSeed annotation is NOT used, and inserting into a non-empty document doesn't corrupt surrounding text.
- **Byte-stability:** the insertion is a normal CM6 transaction — the doc IS the Markdown, so the round-trip is preserved by construction.
- **Safe-path:** the template body is read via `getPage` (the existing SEC-01-resolver endpoint); no path escape.