Partials and preambles for directory templates — and the one-line fix that resurrected eleven hidden settings sections
Shared guidance for the four Obsidian profile templates (mermaid discipline, citation enforcement, image-placement, research-framing) used to be duplicated across templates or hardcoded in TypeScript; today it moved into vault-visible `partials/` and `preambles/` folders that templates pull in by `{{include: name}}` and that the runtime auto-attaches to every Perplexity request. While checking the UI shipped, we discovered the Perplexed settings tab only ever rendered its first section — Perplexity — because a stray `activeDocument.createEl('textarea')` was throwing in this Obsidian version and aborting `display()` before Claude, Perplexica/Vane, LM Studio, and eight other sections could mount. One-line fix: switch eight call sites to `containerEl.createEl`.
Partials, preambles, and a settings UI that finally shows all four providers
Why care?
If you author Obsidian notes with the Perplexed plugin, two things just got better at once:
Your generated diagrams won't crash anymore. The four directory templates (
concept,vocabulary,source,toolkit) all share one source of truth for mermaid syntax discipline, and the rule is a file in your vault you can edit. No more quoting parens twice — fix it inpartials/mermaid-discipline.mdand every future generation picks it up.Your settings page now shows every AI provider you configured. Perplexity, Claude, Perplexica/Vane, and LM Studio sections were all in the code but never rendered. They render now.
If you're extending the plugin: shared guidance is no longer hardcoded in TypeScript constants and no longer duplicated across templates. You can write a partial, drop it into zz-cf-lib/partials/, reference it from any template with {{include: <name>}}, and the next "Apply template" run picks it up live from your vault.
What's new?
Three peer folders under your templates root, three new behaviors:
zz-cf-lib/
├── templates/ (existing — your four profile templates)
├── partials/ (new — referenced from templates via {{include: name}})
│ └── mermaid-discipline.md
└── preambles/ (new — auto-attached to every Perplexity request)
├── inline-citation.md (was a hardcoded TS constant)
├── image-placement.md (was a hardcoded TS constant)
└── research-framing.md (was a TS function) {{include: name}}directive in templates resolves against the partials folder. Recursive, depth-limited (5), cycle-detected, missing-file surfaces as an inline[[include: name — file not found]]marker so typos stay visible.Preambles auto-load from your vault with the bundled defaults as fallback. Settings tab gained four new rows: Partials root, Preambles root, System preambles, User preambles. Per-template overrides live in the ```cft fence:
preambles: { system: [...], skip-user: [...], skip-all: true }.All four provider settings sections now render in the Perplexed settings tab (the bug that hid 11 of 12 sections is fixed).
How it works
A template's prompt assembly used to be: read the template, paste a hardcoded citation directive in front of the system prompt, wrap the skeleton with a hardcoded research-framing string, conditionally append a hardcoded image-placement directive. All three "hardcodes" lived in src/services/directoryTemplateService.ts. None were editable from Obsidian.
The new pipeline:
template file (from vault)
│
├─ expandIncludes(text, partialsRoot) ← splice in {{include: name}} bodies, recursive
│ (cycle + depth guards; missing → inline marker)
│
├─ interpolate(text, ctx) ← {{basename}}/{{title}}/{{frontmatter}} as before
│
└─ assemble final messages:
system: [systemPreambles.join("\n\n")] + templateSystem
user: [research-framing if any]
+ interpolatedSkeleton
+ [trailing preambles, e.g. image-placement when return-images] Each preamble itself runs through expandIncludes + interpolate, so a preamble can reference a partial and use {{title}} etc.
Per-template override
A template that wants different guidance just declares it in its own ```cft fence:
preambles:
system: ["inline-citation", "house-rules"] # replace defaults for THIS template
skip-user: ["research-framing"] # opt out of one user preamble
# or: skip-all: true # bypass every global preamble Falls through to the settings defaults when absent.
Seeding
templateSeederService.ts now seeds three folders instead of one through a shared seedFolder() helper. Same idempotent rule: README is always seeded if missing; content files only seeded into folders that are missing or empty. Existing user content is never clobbered.
The settings-UI bug we found en route
We wanted to verify the new "Directory templates" subsection in the settings tab looked right. Opened Settings → Perplexed, expecting to scroll past Perplexity → Claude → Perplexica/Vane → LM Studio → … → Directory templates. Got:
Perplexity (remote service)
Endpoint
API key
Header position
Request body template ← label visible, no textarea
← end of page That's it. No Claude. No Perplexica. No LM Studio. Eleven of twelve sections silently missing. Source said they should render. git blame showed they'd been there for months.
The culprit, eight call sites in main.ts:
const perplexityTextArea = activeDocument.createEl('textarea');
// ^^^^^^^^^^^^^^
// Obsidian global; undefined in this version,
// throws on .createEl access, kills display() activeDocument is an Obsidian-injected global that's been intermittent across releases. When it's undefined, the first textarea creation throws, display() aborts, and every section after it never mounts. The fix:
const perplexityTextArea = containerEl.createEl('textarea'); containerEl is the HTMLElement Obsidian hands display() — it's always defined, and createEl is the same Obsidian-augmented API. The textarea still gets appendChild'd into its setting row a few lines later, exactly as before. Pattern matches every other DOM operation in the same file.
Replaced all eight (lines 1342, 1449, 1512, 1683, 1703, 1725, 1747, 1767). Build green: eslint + tsc + esbuild all exit 0.
Under the hood — the things that earned their own design decisions
Bundled defaults, vault-authoritative at runtime. Same pattern as templates today: the plugin ships sensible defaults inside main.js via esbuild's text loader; the seeder writes them to the user's vault on first run; the runtime then reads live from the vault. User edits are sticky; first-run users get something working out of the box.
Missing-file asymmetry on purpose.
| Asset type | Missing behavior | Why |
| Partial | Inline [[include: name — file not found]] marker in output | User wrote the include explicitly; surface their typo |
| Preamble | Silent fallback to bundled default with console.warn | Preambles are infrastructure the user didn't explicitly invoke from this template |
Cycle and depth guards. expandIncludes tracks an Set<string> of in-flight names and refuses to recurse past depth 5. A partial that (transitively) includes itself errors at the cycle, not by stack overflow.
No Perplexity-side attachment. We checked. Perplexity's sonar endpoints are OpenAI-compatible /chat/completions — text-only messages, no file upload, no system-prompt attachment, no Claude-style documents array. Inlining text into the prompt is the only path. This work just moves the "where the text comes from" from TypeScript constants to vault files.
Files touched
plugin-modules/perplexed/
├── src/docs/
│ ├── partials/ (new)
│ │ ├── README.md
│ │ └── mermaid-discipline.md
│ ├── preambles/ (new)
│ │ ├── README.md
│ │ ├── inline-citation.md
│ │ ├── image-placement.md
│ │ └── research-framing.md
│ └── templates/ (four templates updated to use {{include: mermaid-discipline}})
│ ├── concept-profile.md
│ ├── vocabulary-profile.md
│ ├── source-profile.md
│ └── toolkit-profile.md
├── src/services/
│ ├── directoryTemplateService.ts (expandIncludes, loadPreamble, parsePreambleOverrides, applyTemplate refactor)
│ └── templateSeederService.ts (rewrite to seed three folders; exports BUNDLED_PREAMBLES)
└── main.ts
├── PerplexedPluginSettings + DEFAULT_SETTINGS extended with 4 new directoryTemplates* fields
├── buildDirectoryTemplateSettings() helper (replaces two duplicated dirSettings literals)
├── seedTemplatesIfMissing / reSeedMissingFiles calls updated
├── four new settings rows (Partials root, Preambles root, System preambles, User preambles)
└── eight activeDocument.createEl → containerEl.createEl fixes Sibling context capture: content-farm/context-v/issues/Partials-And-Preambles-For-Perplexed-Templates.md documents the architecture review and proposed design that this commit implements.
What's next
The vault-seeding caveat is still there. If your vault already has
zz-cf-lib/templates/*.mdfrom a previous seed, the "only seed empty folders" rule means the updated bundled templates won't auto-replace your existing ones. To pick up{{include: mermaid-discipline}}in your templates, either copy the bundle versions over manually or use the "Re-seed" button after deleting the vault copies. On next plugin load, the seeder will createzz-cf-lib/partials/andzz-cf-lib/preambles/with their bundled defaults.Gemini is the one provider we set out to support that still isn't wired — no
GeminiService, no settings section, no commands. Same pattern as Claude when you're ready: new service file, new settings fields, new section in the settings tab, new commands. Hours of work, not days.The mermaid-discipline partial is the first of probably several. The editorial "anti-incumbent" stance is currently duplicated across
concept-profile.mdandvocabulary-profile.md; extracting it intopartials/editorial-stance-anti-incumbent.mdis the next obvious move.