Wiki update

This commit is contained in:
WiseGuru 2026-06-18 08:52:38 -07:00
parent 0773f9ff02
commit 50eb661611
8 changed files with 76 additions and 239 deletions

View file

@ -16,7 +16,7 @@ Update CLAUDE.md with every behavioral change. When modifying code that this doc
Some subsystems are documented in depth in linked `docs/` files (currently [docs/DIARIZATION.md](docs/DIARIZATION.md), [docs/SECRETS.md](docs/SECRETS.md), [docs/WHISPER_HOST.md](docs/WHISPER_HOST.md)) with only a summary + pointer left here. The same rule applies: when you change behavior covered by one of those files, update the linked doc in the same change AND keep the CLAUDE.md summary accurate. Each linked doc restates this at its top.
There is a second, user-facing doc to keep in sync: the **template guide** (`DEFAULT_TEMPLATE_GUIDE` in [src/template-guide.ts](src/template-guide.ts)), seeded into the vault by Populate. Any change to the template structure — a new/changed `NoteTemplate` field or frontmatter property, the prompt-assembly order, the default-template set or count, or what the shared core carries — must update the guide in the same change: add the property to its frontmatter table AND give the behavior its own `##` section. See the Templates section for the full rule.
The user-facing guide to the template format is the wiki page **`wiki/Creating-Templates.md`** (no longer a file seeded into the vault; the old in-vault `Template guide.md` and its `DEFAULT_TEMPLATE_GUIDE` constant were removed in favor of the wiki). Any change to the template structure (a new/changed `NoteTemplate` field or frontmatter property, the prompt-assembly order, the default-template set or count, or what the shared core carries) must update that wiki page in the same change: add the property to its frontmatter table AND give the behavior its own `##` section. See the Templates section for the full rule.
The user-facing reference docs live in the **wiki** (the [`wiki/` folder](wiki/), mirrored to the GitHub Wiki; see the Wiki section below) and in the slimmed root [README.md](README.md). Both are in the in-sync set: any behavioral change a user can observe (a settings field, a command, a provider, a self-hosting step, a limit, a default-template change) must update the relevant wiki page AND the README quick-start/links if affected, in the same change. The `wiki/` source is canonical; never hand-edit the GitHub Wiki, which is a generated mirror.
@ -33,7 +33,7 @@ Page map (keep `_Sidebar.md` and `Home.md`'s contents list in sync when adding/r
- `Quick-Start.md`: install, Populate, first note (Daily note example). Overlaps with the README quick-start by design.
- `Settings-Reference.md`: every settings section (driven by [src/settings/tab.ts](src/settings/tab.ts)).
- `Commands-and-Menus.md`: command palette, ribbon, editor/file menus, Quick Record UI ([src/main.ts](src/main.ts)).
- `Creating-Templates.md`: template file format + authoring guide; mirrors the in-vault `Template guide.md` (note that as canonical).
- `Creating-Templates.md`: template file format + authoring guide. This is the canonical user-facing template guide (it replaced the old in-vault `Template guide.md`); keep it in sync with the `NoteTemplate` schema.
- `Providers.md`: provider tables, model selection, base-URL conventions, diarization, context hint, known nouns, recording limits.
- `Self-Hosting-Whisper.md`: local whisper.cpp host (mirrors user-facing parts of [docs/WHISPER_HOST.md](docs/WHISPER_HOST.md)).
- `Self-Hosting-LLMs.md`: Ollama/llama.cpp local and remote, plus low-spec model recommendations (refresh these as models age).
@ -85,7 +85,7 @@ src/
├── insert.ts # cursor/newFile/append + {{date}}/{{time}} expansion
├── whisper-host.ts # Spawns/stops a user-supplied whisper-server child process (desktop only)
├── templates-folder.ts # Load templates from a vault folder + populate it with the 10 defaults
├── template-guide.ts # Populate a human-facing "Template guide.md" next to the templates folder (never loaded by the plugin)
├── template-guide.ts # Writes the "Template update report.md" (the Update button's worklist) next to the templates folder; never loaded by the plugin. (The in-vault user guide was removed; the template guide now lives in the wiki.)
├── shared-core.ts # Load the shared cleanup preface from a vault Markdown file + populate default (prepended to every template prompt)
├── assistant-prompt.ts # Load the ad-hoc-instructions assistant prompt from a vault Markdown file + populate default
├── known-nouns.ts # Load a vault Markdown file of known nouns + populate default + build the system-prompt section
@ -189,7 +189,7 @@ The 10 defaults ([src/settings/default-templates.ts](src/settings/default-templa
`NoteTemplate.disableSharedCore?: boolean` (frontmatter `disableSharedCore: true`) opts a single template out of the shared-core prepend. `renderTemplateFile` ALWAYS emits the key so the knob is discoverable: empty (`disableSharedCore:`, parses to null = not disabled) by default, `disableSharedCore: true` when set. `parseTemplateFile` treats it as disabled only when the value is boolean `true` or the string `"true"` (case-insensitive) — the string form is tolerated because Obsidian's Properties UI may store an edited value as text; any other value (null/empty/false) means not disabled. The three opt-in boolean flags `enableContextHint` (see Context hint), `diarize` (see Speaker diarization), and `titleFromContent` (see Note title) follow the exact same render/parse convention (always-emitted empty key, boolean-or-`"true"` tolerance); their polarity is positive (set to turn ON), the reverse of `disableSharedCore`.
The Templates "Populate" button also seeds SharedCore.md when missing (non-destructive), because the shared core is load-bearing for the default templates' quality (it carries their guardrail + output discipline). Populating templates without it would yield prompts with no guardrail. It additionally writes a human-facing "Template guide.md" via `populateTemplateGuide` ([src/template-guide.ts](src/template-guide.ts)): a Markdown doc explaining the frontmatter properties, how the shared core combines with a template at run time, and how to word prompts. The guide is placed in the PARENT of the templates folder (the `ReWrite` root by default; vault root if the templates folder has no parent), NOT inside the templates folder, so `loadTemplatesFromFolder` never tries to parse it as a template. The plugin never reads or caches the guide and never sends it to a provider; it has no `loadX`/`isPathX`/cache/refresh, only `populateTemplateGuide` (non-destructive, skipped when the file exists). **Keep `DEFAULT_TEMPLATE_GUIDE` in sync with the template structure.** Whenever you add or change a `NoteTemplate` field / frontmatter property, change the prompt-assembly order, alter what the shared core carries, or change the default-template set or count, update the guide in the SAME change: add the property to its frontmatter table AND give the behavior its own `##` section (the guide already has dedicated sections for the shared core, Context hint, Speaker identification, and Updating defaults). The guide is the user-facing contract for the template format; treat updating it as part of the task, not a follow-up.
The Templates "Populate" button also seeds SharedCore.md when missing (non-destructive), because the shared core is load-bearing for the default templates' quality (it carries their guardrail + output discipline). Populating templates without it would yield prompts with no guardrail. (Populate no longer seeds an in-vault help file; the user-facing template guide lives in the wiki.) **Keep `wiki/Creating-Templates.md` in sync with the template structure.** Whenever you add or change a `NoteTemplate` field / frontmatter property, change the prompt-assembly order, alter what the shared core carries, or change the default-template set or count, update that wiki page in the SAME change: add the property to its frontmatter table AND give the behavior its own `##` section. The settings tab links users to it from the Templates section ("Creating templates"); it is the user-facing contract for the template format, so treat updating it as part of the task, not a follow-up.
### Updating defaults
@ -199,11 +199,11 @@ The Templates section ships three buttons sharing one `Setting` row: **Populate*
**Per-field 3-way merge.** `mergeTemplate(onDisk, def, priors)` (pure, synchronous; `priors` = `priorVersionsForId(id)`) treats a field as *pristine* (user never touched it) when its on-disk value equals the current default OR any prior shipped default; a pristine value is brought forward to the current default, a genuine edit is kept. The **prompt body** is the only field whose kept edit becomes a report `body` conflict — an unedited old body is silently updated (recorded in `changes`). Scalars + flags adopt-or-keep silently (a real adoption is noted in `changes`). `noteProperties` union by name: missing defaults appended; an instruction matching a prior default is brought forward; a genuinely edited instruction is kept + `changedInstruction`; a property the default dropped is always kept + `removedProperty` (with `wasShippedDefault` set when the user's value matches a prior default, so the report can say it is safe to delete). Because `renderTemplateFile` always emits the four flag stubs and emits `noteProperties` only when non-empty, re-rendering a pristine current default is byte-identical to Populate's output, so an already-current folder is a clean no-op.
`updateDefaultTemplates` walks the folder, matches files to defaults by frontmatter `id` (via `readTemplateId`, also now backing `collectExistingIds`; non-default-derived files skipped), merges, and `vault.modify`s in place only when the render differs (CRLF-normalized compare; never renames, even with an ordering prefix). It then **recreates any default missing from disk entirely** (superset top-up, same name + path-collision skip as Populate) but does NOT seed SharedCore.md or the guide (Populate's job). Counters are mutually exclusive: `created` + `updated` + `unchanged` + `conflicts` + `parseFailed` = files seen. A default-derived file that fails to parse is left untouched and reported as `parseFailed`.
`updateDefaultTemplates` walks the folder, matches files to defaults by frontmatter `id` (via `readTemplateId`, also now backing `collectExistingIds`; non-default-derived files skipped), merges, and `vault.modify`s in place only when the render differs (CRLF-normalized compare; never renames, even with an ordering prefix). It then **recreates any default missing from disk entirely** (superset top-up, same name + path-collision skip as Populate) but does NOT seed SharedCore.md (Populate's job). Counters are mutually exclusive: `created` + `updated` + `unchanged` + `conflicts` + `parseFailed` = files seen. A default-derived file that fails to parse is left untouched and reported as `parseFailed`.
**Load prior versions** writes each `allPriorVersions()` snapshot as a standalone template with a distinct id (`<id>@<version>`) and versioned name (`<name> <version>`), so it appears in the picker for comparison, never collides with the live template's identity, and is left untouched by Update (its id is not a current default) and Populate. Non-destructive (skips by id/path); reports `available` so the button can say "no prior versions yet" when the registry is empty.
Anything Update cannot auto-merge is written to **`Template update report.md`** via `writeTemplateUpdateReport` ([src/template-guide.ts](src/template-guide.ts), `TEMPLATE_UPDATE_REPORT_FILENAME`), placed next to the guide (parent of the templates folder, OUTSIDE it so `loadTemplatesFromFolder` never parses it) and **overwritten every run**. It lists, per non-`unchanged` template: changes applied automatically, a `body` conflict (user file kept, default shown beside it in `~~~text` fences), `removedProperty` / `changedInstruction` / `parseFailed`. The report types (`TemplateUpdateConflict` / `TemplateUpdateEntry` / `UpdateResult`) live in [src/templates-folder.ts](src/templates-folder.ts); `template-guide.ts` imports them type-only, so `templates-folder.ts → template-guide.ts` is the one runtime edge (acyclic). Caveat (documented in the guide and the report footer): a `vault.modify` re-serializes the frontmatter via `stringifyYaml`, which **drops YAML comments**; the prompt body is passed through untouched, and an already-current file is never rewritten.
Anything Update cannot auto-merge is written to **`Template update report.md`** via `writeTemplateUpdateReport` ([src/template-guide.ts](src/template-guide.ts), `TEMPLATE_UPDATE_REPORT_FILENAME`), placed in the parent of the templates folder (OUTSIDE it so `loadTemplatesFromFolder` never parses it) and **overwritten every run**. It lists, per non-`unchanged` template: changes applied automatically, a `body` conflict (user file kept, default shown beside it in `~~~text` fences), `removedProperty` / `changedInstruction` / `parseFailed`. The report types (`TemplateUpdateConflict` / `TemplateUpdateEntry` / `UpdateResult`) live in [src/templates-folder.ts](src/templates-folder.ts); `template-guide.ts` imports them type-only, so `templates-folder.ts → template-guide.ts` is the one runtime edge (acyclic). Caveat (documented in the report footer): a `vault.modify` re-serializes the frontmatter via `stringifyYaml`, which **drops YAML comments**; the prompt body is passed through untouched, and an already-current file is never rewritten.
## Shared core

View file

@ -26,7 +26,7 @@ Open Settings, ReWrite (Voice Notes). Configure the profile active on this devic
### 3. Populate templates
In Settings, Templates, click **Populate**. This seeds `ReWrite/Templates/` with 10 starter templates, plus `SharedCore.md`, `AssistantPrompt.md`, `KnownNouns.md`, and a `Template guide.md`. It is non-destructive.
In Settings, Templates, click **Populate**. This seeds `ReWrite/Templates/` with 10 starter templates, plus `SharedCore.md`, `AssistantPrompt.md`, and `KnownNouns.md`. It is non-destructive. The template format is documented in [Creating templates](https://github.com/WiseGuru/ReWrite-Voice-Notes/wiki/Creating-Templates).
### 4. Create your first note

View file

@ -39,7 +39,7 @@ The tag name must equal `manifest.json`'s `version` exactly. `.npmrc` already pi
1. `npm run build` passes (this is `tsc -noEmit` then esbuild production; a type error here is a release blocker).
2. `npm run lint` passes with zero warnings. The local `eslint-plugin-obsidianmd` is looser than the official review bot, so also eyeball the conflict checklist below.
3. Manual smoke test in a real vault for anything you touched. At minimum for a code change: record + Quick Record, run a template insert (cursor / new file / append), and on desktop start the local whisper.cpp server. Install by copying `main.js` / `manifest.json` / `styles.css` into `<Vault>/.obsidian/plugins/rewrite-voice-notes/` (folder name must match the plugin `id`) and reloading.
4. Update docs for any behavioral change ([CLAUDE.md](../CLAUDE.md) and the user-facing template guide), per the doc-maintenance rules in CLAUDE.md.
4. Update docs for any behavioral change ([CLAUDE.md](../CLAUDE.md), the user-facing [`wiki/`](../wiki/) pages, and the [README](../README.md)), per the doc-maintenance rules in CLAUDE.md.
## Guideline-conflict checklist (what the review bot flags)

View file

@ -17,7 +17,6 @@ import { createLLMProvider } from '../llm';
import { formatWhisperStatus } from '../whisper-host';
import { loadPriorTemplateVersions, populateDefaultTemplates, updateDefaultTemplates } from '../templates-folder';
import { populateDefaultSharedCore } from '../shared-core';
import { populateTemplateGuide } from '../template-guide';
import { populateDefaultAssistantPrompt } from '../assistant-prompt';
import { populateDefaultKnownNouns } from '../known-nouns';
import { changeEncryptionMode, EncryptionMode, lockSecrets, resetSecrets } from '../secrets';
@ -27,6 +26,30 @@ import { PassphraseModal } from '../ui/passphrase-modal';
// Sentinel value for the "Custom..." entry in a model dropdown; never written to config.model.
const CUSTOM_MODEL_OPTION = '__rewrite_custom__';
// Base URL of the project wiki. Per-section "Learn more" links point at its
// pages so the in-app docs stay short and the long-form guidance lives in one
// place (the wiki), rather than a help file seeded into the user's vault.
const WIKI_BASE = 'https://github.com/WiseGuru/ReWrite-Voice-Notes/wiki';
// Append an external anchor to the project wiki (a specific page, or the wiki
// root when page is omitted). Opened in a new tab with a safe rel.
function wikiAnchor(parent: HTMLElement, label: string, page = ''): HTMLAnchorElement {
const href = page ? `${WIKI_BASE}/${page}` : WIKI_BASE;
const a = parent.createEl('a', { text: label, href });
a.target = '_blank';
a.rel = 'noopener noreferrer';
return a;
}
// A standalone "Learn more" paragraph linking one wiki page, styled like the
// other section descriptions.
function wikiLinkParagraph(parent: HTMLElement, leadText: string, label: string, page: string): void {
const p = parent.createEl('p', { cls: 'rewrite-section-desc' });
p.appendText(leadText);
wikiAnchor(p, label, page);
p.appendText('.');
}
// Probe the locations scripts/build-whisper-linux.sh installs whisper-server to,
// plus the common system/Homebrew paths, and return the first that exists.
// Desktop-only (lazy-requires fs/os via the same window.require pattern as whisper-host.ts);
@ -117,6 +140,13 @@ export class ReWriteSettingTab extends PluginSettingTab {
containerEl.empty();
containerEl.addClass('rewrite-settings');
const intro = containerEl.createEl('p', { cls: 'rewrite-section-desc' });
intro.appendText('New to ReWrite? See the ');
wikiAnchor(intro, 'Quick start', 'Quick-Start');
intro.appendText(', or browse the full ');
wikiAnchor(intro, 'documentation wiki', '');
intro.appendText('.');
this.renderEncryption(containerEl);
this.renderActiveProfile(containerEl);
this.renderProfile(containerEl, 'desktop');
@ -221,6 +251,7 @@ export class ReWriteSettingTab extends PluginSettingTab {
text: 'Your API keys are saved in a file named secrets.json.nosync in the plugin folder. This setting picks how that file is locked.',
cls: 'rewrite-section-desc',
});
wikiLinkParagraph(parent, 'How keys are encrypted and how to keep the key file off device sync: ', 'Secrets and sync', 'Secrets-and-Sync');
new Setting(parent)
.setName('Encryption mode')
@ -402,6 +433,8 @@ export class ReWriteSettingTab extends PluginSettingTab {
body = details;
}
wikiLinkParagraph(body, 'Choosing transcription and LLM providers, models, and base URLs: ', 'Providers', 'Providers');
new Setting(body)
.setName('Profile label')
.setDesc('Display name for this profile.')
@ -745,6 +778,7 @@ export class ReWriteSettingTab extends PluginSettingTab {
text: 'Run your own whisper-server program so your speech stays on this computer. The plugin only uses the file paths you give it. It never downloads or looks for programs on its own.',
cls: 'rewrite-section-desc',
});
wikiLinkParagraph(parent, 'Getting a binary, building it, the FUTO models, and troubleshooting: ', 'Self-hosting: whisper.cpp', 'Self-Hosting-Whisper');
const cfg = this.plugin.settings.localWhisper;
@ -865,6 +899,7 @@ export class ReWriteSettingTab extends PluginSettingTab {
text: 'Each template is a Markdown file in a vault folder. The text in the file is the prompt. The frontmatter holds its settings. Files show up in name order, so put a number in front of a name to set the order.',
cls: 'rewrite-section-desc',
});
wikiLinkParagraph(parent, 'Full guide to the template format, every frontmatter field, and writing prompts: ', 'Creating templates', 'Creating-Templates');
const s = this.plugin.settings;
@ -882,7 +917,7 @@ export class ReWriteSettingTab extends PluginSettingTab {
new Setting(parent)
.setName('Populate or update default templates')
.setDesc('Populate writes the built-in templates, shared core, and guide if they are missing, skipping anything that already exists. Update reconciles your built-in-derived templates with the current defaults: it fills in new fields and properties, brings unedited prompts forward, restores any you deleted, keeps your edits, and writes a report for anything it cannot safely merge. Load prior versions drops earlier shipped versions of the prompts in as separate templates so you can compare them.')
.setDesc('Populate writes the built-in templates and shared core if they are missing, skipping anything that already exists. Update reconciles your built-in-derived templates with the current defaults: it fills in new fields and properties, brings unedited prompts forward, restores any you deleted, keeps your edits, and writes a report for anything it cannot safely merge. Load prior versions drops earlier shipped versions of the prompts in as separate templates so you can compare them.')
.addButton((b) => {
b.setButtonText('Populate').setCta().onClick(() => void this.runGuardedButton(b, async () => {
try {
@ -892,12 +927,8 @@ export class ReWriteSettingTab extends PluginSettingTab {
// (it carries the guardrail + output discipline), so seed it alongside.
const coreCreated = await populateDefaultSharedCore(this.app, s.sharedCorePath);
await this.plugin.refreshSharedCore();
// Seed the human-facing template guide next to the templates folder.
// The plugin never reads it; it just teaches the template format.
const guideCreated = await populateTemplateGuide(this.app, s.templatesFolderPath);
const coreNote = coreCreated ? ` Created ${s.sharedCorePath}.` : '';
const guideNote = guideCreated ? ' Added the template guide.' : '';
new Notice(`ReWrite: populated ${result.folder}. Created ${result.created}, skipped ${result.skipped}.${coreNote}${guideNote}`);
new Notice(`ReWrite: populated ${result.folder}. Created ${result.created}, skipped ${result.skipped}.${coreNote}`);
this.display();
} catch (e) {
console.error('ReWrite: populate templates failed', e);

View file

@ -1,205 +1,37 @@
import { App, moment, normalizePath, TFile } from 'obsidian';
import type { TemplateUpdateConflict, TemplateUpdateEntry, UpdateResult } from './templates-folder';
// A human-facing help document seeded next to the templates folder by the
// settings "Populate" button. The plugin never reads this file and never sends
// it to any provider; it exists purely so users can learn the template format,
// how the shared core combines with a template, and how to word a good prompt.
export const TEMPLATE_GUIDE_FILENAME = 'Template guide.md';
// The Update button's worklist. Lives next to the templates folder (in its
// parent, the "ReWrite" root by default), so loadTemplatesFromFolder never
// parses it. Overwritten on every Update; the plugin never reads it back and
// never sends it to a provider. User-facing help about the template format now
// lives in the project wiki (Creating templates), not a seeded vault file.
export const TEMPLATE_UPDATE_REPORT_FILENAME = 'Template update report.md';
export const DEFAULT_TEMPLATE_GUIDE = `---
guidance: |
This is a help document for you, the human. The ReWrite plugin never reads
this file and never sends it to any provider. Edit or delete it freely.
---
# ReWrite: how to write a template
A template tells ReWrite how to turn a transcript (or pasted / selected text) into a finished note. This guide covers every property a template can set, how the shared core combines with your template at run time, and how to word a prompt that produces clean output.
## A template is one Markdown file
Each template is a single \`.md\` file in your templates folder (default \`ReWrite/Templates\`). The file has two parts:
- **Frontmatter** (the \`---\` block at the top): the template's settings.
- **Body** (everything after the frontmatter): the prompt sent to the language model.
Templates are listed in the picker sorted by file name, so prefix names with numbers (\`01 General.md\`, \`02 Daily note.md\`) if you want a specific order. Identity comes from the \`id\` property, not the file name, so you can rename a file without breaking which template is your default or last used.
The **Populate** button in settings writes the ten built-in templates here (plus the shared core and this guide). It is non-destructive: it skips any template whose \`id\` already exists, so you can run it again to top up after deleting one, and your own edits are never overwritten.
## Keeping templates up to date
Populate only ever *adds* missing files; it never touches one you already have. So when a new version of ReWrite improves a built-in template (a new frontmatter field, a new property, reworded instructions), Populate cannot push that into the copy in your vault. The **Update** button does.
Update reconciles your built-in-derived templates (matched by \`id\`) with the current defaults:
- It **fills in new frontmatter fields and missing \`noteProperties\`** automatically.
- It **recreates any built-in template you deleted** entirely.
- It **brings unedited content forward.** ReWrite remembers what each built-in looked like in past versions. If your copy of a prompt (or a setting, or a property instruction) still matches an older shipped version, Update knows you never changed it and safely upgrades it to the new default for you.
- It **never overwrites your edits**: anything you actually changed is kept exactly as you wrote it. A **prompt you edited** is left untouched and instead written to the report below for you to merge by hand.
Anything it cannot safely merge by itself, it writes to a **\`Template update report.md\`** file next to this guide (not inside the templates folder). That report lists, per template: settings and properties it upgraded or added for you, a property the default dropped (kept in your file, flagged so you can remove it), a property instruction you changed (your wording kept), and a **prompt body that you edited** (your file untouched, the new default shown beside it so you can merge by hand). The report is regenerated (overwritten) on every Update.
One caveat: when Update changes a file, it re-saves the file's frontmatter, which **discards any YAML comments** you added in the frontmatter. Your prompt body is never altered unless it was an unedited older version. A template that is already current is never rewritten, so its comments are only ever lost if the file genuinely needed a change.
### Comparing prompt versions
The **Load prior versions** button drops earlier shipped versions of the built-in prompts into your templates folder as their own templates, named with the version they came from (e.g. \`Meeting notes 0.1.1\`). They appear in the picker alongside the current one, so you can run the same recording through an old prompt and the new prompt and compare the results. They are ordinary templates with their own \`id\`, so Update and Populate leave them alone; delete them when you are done. (Until a built-in prompt has actually changed across versions, there is nothing prior to load.)
## Frontmatter properties
| Property | What it does |
| --- | --- |
| \`id\` | Stable identifier. Must be unique. This is how ReWrite remembers your default and last-used template, so do not reuse an id or change it casually. |
| \`name\` | Display name shown in the modal and pickers. Falls back to the file name if blank. |
| \`insertMode\` | Where the result goes: \`cursor\`, \`newFile\`, or \`append\`. See below. |
| \`newFileFolder\` | For \`insertMode: newFile\`, the folder the new note is created in. Blank means the vault root. |
| \`newFileNameTemplate\` | For \`insertMode: newFile\`, the new note's file name. Supports \`{{date}}\`, \`{{time}}\`, and \`{{title}}\`. |
| \`disableSharedCore\` | Set to \`true\` to skip the shared core for this one template. Leave blank otherwise. See "The shared core". |
| \`enableContextHint\` | Set to \`true\` to show an optional "Context" field for this template. Leave blank otherwise. See "Context hint". |
| \`diarize\` | Set to \`true\` to force speaker identification on for this template. Leave blank otherwise. See "Speaker identification". |
| \`titleFromContent\` | Set to \`true\` to have the model name the new note from the content (\`newFile\` only). Leave blank otherwise. See "Note title". |
| \`noteProperties\` | A YAML map of frontmatter properties for the model to fill from the content (\`newFile\` only). Leave it out unless you want it. See "Note properties". |
### insertMode in detail
- **\`cursor\`**: insert at the cursor in the active note. If no editor is open, it falls back to appending.
- **\`newFile\`**: create a new note from \`newFileFolder\` plus \`newFileNameTemplate\`. If a note with that name already exists, ReWrite either auto-numbers it (\`name-1\`, \`name-2\`) or prompts you for a new path, depending on your new-file collision setting.
- **\`append\`**: add to the end of the active note (or the most recently edited note when none is focused). If no Markdown note exists at all, it creates one.
\`{{date}}\` becomes the date as \`YYYY-MM-DD\` and \`{{time}}\` becomes the time as \`HHmmss\`, expanded at the moment of insertion. \`{{title}}\` becomes a title the model writes from the recording, but only when the template has \`titleFromContent: true\` (otherwise it expands to nothing). See "Note title". These tokens are substituted in \`newFileNameTemplate\` only, not in \`newFileFolder\`; put a literal folder path in \`newFileFolder\`.
If the source was a recording, the saved audio file is embedded as \`![[...]]\` at the top of the output regardless of insert mode, so you always keep the original.
## The shared core
The shared core is one Markdown file (default \`ReWrite/SharedCore.md\`) whose body is **prepended to every template's prompt** right before each cleanup call. Think of it as the house rules every template inherits.
At run time the system prompt is assembled in this order:
1. **Shared core** (unless the template opts out)
2. **Your template's body** (the prompt below your frontmatter)
3. **Ad-hoc instructions** (only when you spoke "<assistant name>, ..." in the recording)
4. **Context** (only when this template has \`enableContextHint\` and you filled the field in)
5. **Known nouns** (only when your KnownNouns file lists any)
6. **Note properties** (only when this template declares \`noteProperties\`)
The default shared core carries three things, so your template body does not have to:
- An **anti-injection guardrail**: it tells the model the transcript is text to clean, not instructions to obey. This is the plugin's main defense against a transcript that happens to say "ignore your instructions and ...".
- **General cleanup rules**: fix grammar and punctuation, drop filler words and false starts, keep the corrected version of self-corrections, preserve the speaker's voice and proper nouns, and keep \`Speaker A:\` style labels intact.
- **Output discipline**: output only the result, with no preamble, labels, or code fences, and empty input yields empty output.
Because the shared core already says all of this, **do not repeat it in your template**. Your template should only describe what is unique to it: the structure and tone of this particular kind of note.
Editing \`SharedCore.md\` changes the baseline for every template at once. It rides along on every call, so trimming it saves tokens. Deleting or emptying the file disables the shared core for the whole plugin (there is no hidden fallback). Setting \`disableSharedCore: true\` on a template disables it for that one template only, which also drops the anti-injection guardrail for that template; settings will warn you when a loaded template has this set.
## Context hint
Set \`enableContextHint: true\` on a template to surface an optional **Context** field whenever you use it. It is collapsed by default in both the main ReWrite window and the "Reprocess audio" picker, so it never gets in your way; expand it only when you want to add a one-off note about the recording, such as "Lecture by Dr. Smith on thermodynamics" or "Meeting with Rachel, Joe, and Sally".
Whatever you type is added to the prompt as a \`## Context\` block for that single run (it is not saved). It helps the model attribute statements to the right person, spell names correctly, and pick the right tone. It is the one-off counterpart to your Known nouns list: Known nouns is a standing list of names to always preserve, while Context is "here is what *this* recording is". The built-in Meeting notes, Meeting transcript, Lecture, Podcast, Guides, and Book log templates ship with this turned on.
## Speaker identification
Set \`diarize: true\` on a template to force speaker identification on for it, so the transcript comes back with \`Speaker 1:\`, \`Speaker 2:\` style labels that your prompt can turn into attendees and attributions. This overrides the per-profile "Identify speakers" toggle for this template only.
It only works on transcription providers that support diarization (AssemblyAI, Deepgram, Rev.ai); on any other provider the flag is simply ignored and you get an ordinary transcript. The built-in Meeting transcript template uses this.
## Note title
Set \`titleFromContent: true\` to have the model name the new note from what was said (and any Context you provided), instead of a fixed date/time name. A meeting becomes its subject, a book log becomes the book's title, and so on.
This works with \`insertMode: newFile\` only (other modes write into a note you already have, so there is nothing to name). Two ways to place the generated title:
- Put a \`{{title}}\` token in \`newFileNameTemplate\` to compose it with other pieces, e.g. \`Meeting {{date}} {{title}}\` becomes \`Meeting 2026-06-12 Q3 planning sync\`.
- Leave the token out and the generated title becomes the **whole** file name, e.g. set \`newFileNameTemplate: {{title}}\` (or any name) and a book log is filed as \`Bram Stoker's Dracula\`.
Good to know:
- **It names the file, it is not a frontmatter property.** If you also want a title *in* the note's properties, add it to \`noteProperties\` separately.
- **Illegal filename characters are cleaned up automatically** (\`/\`, \`:\`, \`?\`, and friends become \`-\`), and very long titles are trimmed.
- **If the model cannot produce a title** (or you have the LLM set to "none"), the note falls back to the normal date/time name, so you never get an empty file name.
- **Same titles collide more often** than dated names. ReWrite handles it the same way as any name clash: auto-numbering (\`Dracula-1\`) or a rename prompt, per your new-file collision setting.
The built-in Meeting notes, Meeting transcript, Lecture, Podcast, Guides, and Book log templates ship with this on. Daily note deliberately leaves it off (a date is the right name for a daily note).
## Note properties
A template can ask the model to fill in the new note's **frontmatter properties** from what was said. Add a \`noteProperties\` map to the frontmatter, where each key is a property name and its value is a short instruction telling the model what to put there:
\`\`\`
noteProperties:
title: The book title.
author: The author's full name.
series: The series name, or leave blank if standalone.
\`\`\`
When you run such a template, the model writes a small YAML block at the very top of its answer, and ReWrite turns it into the new note's frontmatter, then writes the note body below it. So a book log comes out with \`title\`, \`author\`, and \`series\` already set in the Properties panel.
A few things to know:
- **\`newFile\` only.** Properties are written only when the template creates a new note. They are ignored for \`cursor\` and \`append\`, which write into a note you already have open, so ReWrite never rewrites an existing note's frontmatter behind your back.
- **Every property always appears.** If the model could not work out a value, the property is still added, just left empty, so the note always has the full scaffold for you to fill in.
- **Only your declared properties are used.** The model is told to fill exactly the keys you listed and nothing else.
Several built-in templates ship with this: Meeting notes and Meeting transcript fill \`subject\` / \`participants\` / \`date\`, Lecture fills \`subject\` / \`lecturer\` / \`course\`, Podcast fills \`podcast\` / \`episode\` / \`host\` / \`guests\`, Guides fills \`topic\` / \`tool\`, and Book log fills \`title\` / \`author\` / \`series\`.
## Writing a good prompt
The body of the file is the prompt. A few principles produce reliable results:
1. **Describe the shape of the output, not the cleanup.** The shared core already handles grammar, filler, and formatting discipline. Spend your prompt on structure: which \`##\` sections to produce and in what order.
2. **Name your sections and say when to omit them.** For example: "Use these sections in order: \`## Summary\`, then \`## Action items\` as a \`- [ ] \` checkbox list, omitting the heading when there are none."
3. **Be concrete about format.** Say "as bullet points" or "as a checkbox list (\`- [ ] \`)" rather than leaving it open.
4. **Extract, do not invent.** When a section pulls items out of what was said (tasks, dates, decisions), say so explicitly: "extracted from what the speaker actually said; do not invent items."
5. **Keep it short and declarative.** Imperative sentences ("Lay the transcript into...", "Group related points under...") work better than long explanations.
6. **One template, one job.** If you are describing two unrelated output shapes, split them into two templates.
### Example
\`\`\`
---
id: my-meeting-notes
name: Meeting notes
insertMode: newFile
newFileFolder: Meetings
newFileNameTemplate: "{{date}} meeting"
disableSharedCore:
---
Turn the transcript into meeting notes with these sections in order:
## Summary
Two or three sentences on what the meeting was about.
## Decisions
Bullet points of decisions that were actually made. Omit the heading if none.
## Action items
A checkbox list ("- [ ] ") of concrete next steps, with an owner in brackets when one was named. Omit the heading if none.
Decisions and action items are extracted from what was said; do not invent them.
\`\`\`
Notice the body never mentions fixing grammar, removing "um", or avoiding preambles. The shared core covers all of that. To try a new template without recording, open ReWrite, pick the template, and use the Paste tab.
`;
export async function populateTemplateGuide(app: App, templatesFolderPath: string): Promise<boolean> {
const folder = guideFolder(templatesFolderPath);
export async function writeTemplateUpdateReport(
app: App,
templatesFolderPath: string,
result: UpdateResult,
): Promise<string> {
const folder = reportFolder(templatesFolderPath);
const path = folder
? normalizePath(`${folder}/${TEMPLATE_GUIDE_FILENAME}`)
: normalizePath(TEMPLATE_GUIDE_FILENAME);
if (app.vault.getAbstractFileByPath(path)) return false;
if (folder) await ensureFolder(app, folder);
await app.vault.create(path, DEFAULT_TEMPLATE_GUIDE);
return true;
? normalizePath(`${folder}/${TEMPLATE_UPDATE_REPORT_FILENAME}`)
: normalizePath(TEMPLATE_UPDATE_REPORT_FILENAME);
const content = renderUpdateReport(result);
const existing = app.vault.getAbstractFileByPath(path);
if (existing instanceof TFile) {
await app.vault.process(existing, () => content);
} else {
if (folder) await ensureFolder(app, folder);
await app.vault.create(path, content);
}
return path;
}
// The guide lives in the parent of the templates folder (the "ReWrite" root by
// The report lives in the parent of the templates folder (the "ReWrite" root by
// default), not inside the templates folder itself, so it is never parsed as a
// template. When the templates folder sits at the vault root, the guide does too.
function guideFolder(templatesFolderPath: string): string {
// template. When the templates folder sits at the vault root, the report does too.
function reportFolder(templatesFolderPath: string): string {
const normalized = normalizeFolderPath(templatesFolderPath);
if (!normalized) return '';
const idx = normalized.lastIndexOf('/');
@ -229,31 +61,6 @@ async function ensureFolder(app: App, folder: string): Promise<void> {
}
}
// The Update button's worklist. Lives next to the template guide (the parent of
// the templates folder), so loadTemplatesFromFolder never parses it. Overwritten
// on every Update; the plugin never reads it and never sends it to a provider.
export const TEMPLATE_UPDATE_REPORT_FILENAME = 'Template update report.md';
export async function writeTemplateUpdateReport(
app: App,
templatesFolderPath: string,
result: UpdateResult,
): Promise<string> {
const folder = guideFolder(templatesFolderPath);
const path = folder
? normalizePath(`${folder}/${TEMPLATE_UPDATE_REPORT_FILENAME}`)
: normalizePath(TEMPLATE_UPDATE_REPORT_FILENAME);
const content = renderUpdateReport(result);
const existing = app.vault.getAbstractFileByPath(path);
if (existing instanceof TFile) {
await app.vault.process(existing, () => content);
} else {
if (folder) await ensureFolder(app, folder);
await app.vault.create(path, content);
}
return path;
}
// Fenced block in the report. Tilde fences so a prompt containing ``` does not
// break it; an indented fence inside the user content is harmless either way.
function reportBlock(label: string, value: string): string[] {
@ -335,7 +142,7 @@ function renderEntry(entry: TemplateUpdateEntry): string[] {
const lines: string[] = [];
lines.push(`## ${entry.name} (\`${entry.id}\`)`);
lines.push('');
lines.push(`Status: **${entry.status}** ${entry.path}`);
lines.push(`Status: **${entry.status}** - ${entry.path}`);
lines.push('');
if (entry.changes.length > 0) {
lines.push('Applied automatically:');

View file

@ -2,7 +2,7 @@
Templates control how a transcript is cleaned and structured, and where the result goes. They are plain Markdown files in your vault, so you edit, rename, reorder, and create them like any other note.
The plugin also seeds a copy of this guide into your vault as `ReWrite/Template guide.md` when you Populate. That in-vault copy is the canonical, always-current reference (it ships with the exact version of the plugin you have installed). This page mirrors it for easy browsing; if the two ever differ, trust the in-vault guide.
This page is the canonical guide to the template format. The settings tab links here from its Templates section.
## Anatomy of a template

View file

@ -43,9 +43,8 @@ This creates, under `ReWrite/` in your vault:
- `ReWrite/Templates/` with 10 starter templates (General cleanup, Todo list, Daily note, Meeting notes, Meeting transcript, Idea capture, Lecture, Podcast, Guides, Book log).
- `ReWrite/SharedCore.md`, the cleanup ground rules prepended to every template.
- `ReWrite/AssistantPrompt.md` and `ReWrite/KnownNouns.md`, optional helpers.
- `ReWrite/Template guide.md`, a human-facing explanation of the format (never sent to an LLM).
Populate is non-destructive: re-running it only adds what is missing, so your edits are safe. To pull in changed defaults later, use **Update** instead (see [Creating templates](Creating-Templates)).
Populate is non-destructive: re-running it only adds what is missing, so your edits are safe. To pull in changed defaults later, use **Update** instead. The full template format is documented in [Creating templates](Creating-Templates).
## 4. Create your first note (Daily note)

View file

@ -42,7 +42,7 @@ Shown on desktop only. Manages a whisper-server binary you supply for fully on-d
- **Templates folder**: where the template Markdown files live (default `ReWrite/Templates`). Changing it reloads templates from the new location.
- **Populate or update default templates**: three buttons sharing one row:
- **Populate**: creates any missing default templates plus `SharedCore.md` and the template guide. Non-destructive; skips anything that already exists.
- **Populate**: creates any missing default templates plus `SharedCore.md`. Non-destructive; skips anything that already exists. (The template format is documented in [Creating templates](Creating-Templates), not a seeded vault file.)
- **Update**: reconciles your default-derived templates against the current built-ins with a per-field 3-way merge, and writes a `Template update report.md` for anything it cannot safely auto-merge. Recreates any default you deleted.
- **Load prior versions**: drops earlier shipped versions of the defaults into the folder as separate, selectable templates so you can compare wording.
- **Default template**: the template pre-selected when you open the modal.