feat(templates): apply templates at all 4 notes-render call sites (T8.1, FR-NOTE-5)

Adds StartRecordingArgs.template_id + list_note_templates command;
renames built_in_template to summary_prompt_bias to disambiguate it
from the new NoteTemplate concept (same meeting-type identifiers,
different purpose). Also fixes refresh_notes_and_notify (the
rename/merge-speaker refresh path) to route through
Store::update_notes instead of a raw std::fs::write — it was bypassing
T8.2's FTS reindexing, a gap missed there because this call site uses
meeting_dir(meeting_id) without the & that my earlier grep matched on.
This commit is contained in:
iamdoubz
2026-07-02 11:25:48 -05:00
parent 6b5b359438
commit 2f56891d40
+54 -13
View File
@@ -36,6 +36,8 @@ pub struct StartRecordingArgs {
/// Retain audio as .wav? Defaults to false (ADR-0009). Controls retention, not capture.
#[serde(default)]
pub record: bool,
/// Note-template id (Phase 8, T8.1, FR-NOTE-5) — see `notes::built_in_note_templates`.
pub template_id: Option<String>,
}
// ---- File-backed settings (consent/default-retention/storage policy) ----
@@ -215,6 +217,7 @@ pub async fn start_recording(
.meeting_title
.unwrap_or_else(|| "Untitled meeting".to_string()),
calendar_event_id: args.calendar_event_id,
template_id: args.template_id,
})
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
@@ -477,7 +480,7 @@ pub async fn stop_recording(
// T2.10: persist transcript.json (via finalize_meeting) and notes.md
// *before* touching the working WAV, so a crash here still leaves a
// recoverable, regenerable meeting.
state
let template_id = state
.store
.finalize_meeting(
&meeting_id,
@@ -494,7 +497,11 @@ pub async fn stop_recording(
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
let notes_md = crate::notes::MarkdownNotes.to_markdown(&segments, &speakers, None);
let template = template_id
.as_deref()
.and_then(crate::notes::note_template_by_id);
let notes_md =
crate::notes::MarkdownNotes.to_markdown(&segments, &speakers, None, template.as_ref());
let _ = state.store.update_notes(&meeting_id, &notes_md).await;
let _ = app.emit(
@@ -613,9 +620,17 @@ async fn refresh_notes_and_notify(
.get_meeting(meeting_id)
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
let notes_md =
crate::notes::MarkdownNotes.to_markdown(&meeting.segments, &meeting.speakers, None);
let _ = std::fs::write(meeting_dir(meeting_id).join("notes.md"), notes_md);
let template = meeting
.template_id
.as_deref()
.and_then(crate::notes::note_template_by_id);
let notes_md = crate::notes::MarkdownNotes.to_markdown(
&meeting.segments,
&meeting.speakers,
None,
template.as_ref(),
);
let _ = state.store.update_notes(meeting_id, &notes_md).await;
let _ = app.emit(
"diarization://updated",
serde_json::json!({ "meetingId": meeting_id, "speakers": meeting.speakers }),
@@ -881,6 +896,10 @@ pub async fn reprocess_transcript(
.map(|s| (s.end_ms / 1000) as i64)
.unwrap_or(meeting.duration_secs.unwrap_or(0));
let template = meeting
.template_id
.as_deref()
.and_then(crate::notes::note_template_by_id);
state
.store
.finalize_meeting(
@@ -898,7 +917,12 @@ pub async fn reprocess_transcript(
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
let notes_md = crate::notes::MarkdownNotes.to_markdown(&segments, &meeting.speakers, None);
let notes_md = crate::notes::MarkdownNotes.to_markdown(
&segments,
&meeting.speakers,
None,
template.as_ref(),
);
let _ = state.store.update_notes(&meeting_id, &notes_md).await;
let _ = app.emit(
@@ -961,7 +985,7 @@ pub async fn resume_transcription(
.map(|s| (s.end_ms / 1000) as i64)
.unwrap_or(0);
state
let template_id = state
.store
.finalize_meeting(
&meeting_id,
@@ -980,7 +1004,11 @@ pub async fn resume_transcription(
.await
.map_err(|e| WaError::new("storage", e.to_string()))?;
let notes_md = crate::notes::MarkdownNotes.to_markdown(&segments, &speakers, None);
let template = template_id
.as_deref()
.and_then(crate::notes::note_template_by_id);
let notes_md =
crate::notes::MarkdownNotes.to_markdown(&segments, &speakers, None, template.as_ref());
let _ = state.store.update_notes(&meeting_id, &notes_md).await;
let _ = app.emit(
@@ -1050,6 +1078,13 @@ pub async fn search(state: State<'_, AppState>, query: String) -> WaResult<Vec<S
.map_err(|e| WaError::new("storage", e.to_string()))
}
/// Built-in note templates for a picker at recording-start time (Phase 8,
/// T8.1, FR-NOTE-5).
#[tauri::command]
pub async fn list_note_templates() -> WaResult<Vec<crate::notes::NoteTemplate>> {
Ok(crate::notes::built_in_note_templates())
}
#[tauri::command]
pub async fn get_meeting(state: State<'_, AppState>, meeting_id: MeetingId) -> WaResult<Meeting> {
state
@@ -1261,10 +1296,13 @@ pub async fn pull_ollama_model(app: AppHandle, model: String) -> WaResult<()> {
.map_err(|e| WaError::new("llm", e.to_string()))
}
/// Built-in prompt biases for common meeting shapes (T5.4). User-authored
/// templates are Phase 8's FR-NOTE-5 — an unrecognized id is ignored rather
/// than erroring, so a stale/removed template id never blocks a summary.
fn built_in_template(id: &str) -> Option<&'static str> {
/// Built-in LLM-prompt biases for common meeting shapes (T5.4) — distinct
/// from `notes::NoteTemplate` (Phase 8, T8.1/FR-NOTE-5), which structures
/// notes.md itself. Both use the same meeting-type identifiers
/// ("standup"/"retro"/"one-on-one") since they describe the same kinds of
/// meetings, but are two independent lookups. An unrecognized id is ignored
/// rather than erroring, so a stale/removed template id never blocks a summary.
fn summary_prompt_bias(id: &str) -> Option<&'static str> {
match id {
"standup" => Some(
"This is a daily standup. Focus the summary on what each person did, what's \
@@ -1304,7 +1342,9 @@ fn build_prompt(meeting: &Meeting, template_id: Option<&str>) -> crate::llm::Pro
crate::llm::Prompt {
transcript: meeting.notes_markdown.clone(),
metadata,
template: template_id.and_then(built_in_template).map(str::to_string),
template: template_id
.and_then(summary_prompt_bias)
.map(str::to_string),
}
}
@@ -1779,6 +1819,7 @@ mod tests {
summary: None,
calendar_event_id: None,
tags: Vec::new(),
template_id: None,
}
}