Skip to content

Afterword: prompt templates feature - #55

Merged
yoonwaiyan merged 13 commits into
mainfrom
feature/afterword
Jul 4, 2026
Merged

Afterword: prompt templates feature#55
yoonwaiyan merged 13 commits into
mainfrom
feature/afterword

Conversation

@yoonwaiyan

@yoonwaiyan yoonwaiyan commented Jul 3, 2026

Copy link
Copy Markdown
Owner

User description

Summary

Incremental build-out of the Afterword (custom prompt templates) feature. Tickets landed so far on this branch, in order:

  • AUD-56 — Afterword data model, CRUD IPC handlers, and built-in presets
  • AUD-59 — Afterword list view in Settings — cards, badges, overflow menu, sidebar nav
  • AUD-57 — Wire Afterword engine into summarisation pipeline and add preview IPC
  • AUD-66 — Per-session Afterword selector dropdown in main recording view
  • AUD-62 — Afterword editor — name, system prompt, variable chips, output sections, action bar
  • AUD-65 — New Afterword creation modal — blank or from preset
  • AUD-63 — Output section drag-and-drop reordering in Afterword editor

This branch is a persistent feature branch and will accumulate further Afterword tickets before a single merge to main.

Test plan

  • npm run typecheck passes
  • npm run lint clean on all touched files (pre-existing warnings elsewhere untouched)
  • npm run build succeeds
  • Playwright e2e green: prompt-templates.spec.ts, prompt-template-editor.spec.ts, new-template-modal.spec.ts, output-section-reorder.spec.ts (pointer drag, keyboard reorder, single-section edge case)

🤖 Generated with Claude Code


PR Type

Enhancement, Tests, Bug fix, Documentation


Description

  • Adds full template editor workflow

  • Supports preset-based template creation

  • Renames active templates to default

  • Adds drag reordering and coverage


Diagram Walkthrough

flowchart LR
  list["Template list"] -- "create or duplicate" --> editor["Template editor"]
  editor -- "edit prompts and sections" --> api["Templates IPC"]
  api -- "persist default templates" --> store["Template store"]
  store -- "resolve template" --> summary["Summarisation/session detail"]
Loading

File Walkthrough

Relevant files
Enhancement
17 files
TemplateEditorPage.tsx
Implements full prompt template editor page                           
+293/-22
OutputSectionList.tsx
Adds sortable output sections list                                             
+217/-0 
OutputSectionRow.tsx
Adds editable draggable section rows                                         
+112/-0 
SystemPromptEditor.tsx
Adds prompt editor with variable chips                                     
+148/-0 
InlineEditableText.tsx
Adds reusable inline editable text control                             
+108/-0 
TemplateActionBar.tsx
Adds editor preview save actions                                                 
+85/-0   
NewTemplateModal.tsx
Implements blank and preset creation modal                             
+97/-15 
PresetCard.tsx
Adds preset option card component                                               
+31/-0   
DeleteTemplateDialog.tsx
Adds template deletion confirmation dialog                             
+61/-0   
DiscardChangesDialog.tsx
Adds unsaved changes confirmation dialog                                 
+52/-0   
TemplateListPage.tsx
Wires default actions and modal data                                         
+9/-5     
TemplateCard.tsx
Updates card badge to default terminology                               
+4/-4     
TemplateOverflowMenu.tsx
Renames active menu action to default                                       
+6/-6     
logic.ts
Renames active template logic to default                                 
+14/-14 
ipc.ts
Adds default and session resolution IPC                                   
+12/-4   
engine.ts
Resolves summaries through default template                           
+2/-2     
SessionDetail.tsx
Shows resolved summary template name                                         
+11/-0   
Bug fix
1 files
store.ts
Migrates legacy active template storage                                   
+17/-6   
Tests
1 files
output-section-reorder.spec.ts
Adds output section reordering tests                                         
+122/-0 
Documentation
1 files
CHANGELOG.md
Documents Afterword feature additions                                       
+22/-0   
Additional files
19 files
pr-agent.yml +5/-0     
new-template-modal.spec.ts +84/-0   
prompt-template-editor.spec.ts +86/-0   
prompt-templates.spec.ts +6/-5     
summarisation.spec.ts +4/-4     
package.json +3/-0     
builtins.ts +1/-1     
engine.test.ts +6/-6     
logic.test.ts +31/-31 
store.test.ts +5/-5     
types.ts +2/-2     
index.d.ts +3/-2     
index.ts +3/-1     
TemplateSelector.tsx +6/-6     
PillBadge.tsx +2/-2     
useTemplates.ts +1/-1     
useUnsavedChanges.ts +30/-0   
relativeTime.ts +26/-0   
VariableChip.tsx +27/-0   

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 0da318b)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Stale State

When the route id changes, the editor keeps rendering the previous template and form until the new get request resolves. Because React Router can reuse this component for /prefs/templates/:id changes, users can briefly see or act on the wrong template; resetting template/form to loading state at the start of the effect avoids accidental edits against the new id.

useEffect(() => {
  if (!id) return
  let cancelled = false
  window.api.templates.get(id).then((t) => {
    if (cancelled) return
    setTemplate(t)
    if (t) {
      const fresh = toFormState(t)
      setForm(fresh)
      markSaved(fresh)
    }
  })
  return () => {
    cancelled = true
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id])
Stale Preview

Preview generation uses window.api.templates.preview(id), so it previews the last saved template rather than the current editor form. If a user changes the system prompt or output sections and clicks Preview before saving, the preview can show outdated content, making the editor feedback misleading.

const handlePreview = async (): Promise<void> => {
  if (!id) return
  const token = ++previewTokenRef.current
  setPreviewState({ status: 'loading' })
  try {
    const { markdown } = await window.api.templates.preview(id)
    if (previewTokenRef.current !== token) return // closed (or re-opened) before this resolved

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 0da318b

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Validate session directory paths

Validate sessionDir before passing it to resolveTemplateForSession. As written, any
renderer that can call window.api.templates.resolveForSession can cause the main
process to read session.json from an arbitrary directory.

src/main/templates/ipc.ts [74-80]

 ipcMain.handle(
   'audist:templates:resolveForSession',
   (_, { sessionDir }: { sessionDir: string }): { id: string; name: string } => {
-    const template = resolveTemplateForSession(sessionDir)
+    const saveRoot = resolve(getSaveDirectory())
+    const requestedDir = resolve(sessionDir)
+    const relativePath = relative(saveRoot, requestedDir)
+
+    if (relativePath.startsWith('..') || relativePath === '' || relativePath.startsWith('/')) {
+      throw new Error('Invalid session directory')
+    }
+
+    const template = resolveTemplateForSession(requestedDir)
     return { id: template.id, name: template.name }
   }
 )
Suggestion importance[1-10]: 7

__

Why: The suggestion identifies a real trust-boundary issue: sessionDir from the renderer is passed to main-process filesystem-reading logic without validation. The proposed fix is conceptually sound, though the snippet would also need resolve/relative imports and more robust path containment checks.

Medium
General
Prevent stale template previews

window.api.templates.preview(id) renders the persisted template, so previewing with
unsaved edits shows stale output. Block preview until the draft is saved, or extend
the preview API to accept the current form state.

src/renderer/src/pages/prefs/templates/TemplateEditorPage.tsx [134-149]

 const handlePreview = async (): Promise<void> => {
   if (!id) return
+
+  if (hasUnsavedChanges && !isBuiltIn) {
+    setPreviewState({
+      status: 'error',
+      message: 'Save your changes before generating a preview.'
+    })
+    return
+  }
+
   const token = ++previewTokenRef.current
   setPreviewState({ status: 'loading' })
   try {
     const { markdown } = await window.api.templates.preview(id)
     if (previewTokenRef.current !== token) return // closed (or re-opened) before this resolved
     setPreviewState({ status: 'done', markdown })
   } catch (err) {
     if (previewTokenRef.current !== token) return
     setPreviewState({
       status: 'error',
       message: err instanceof Error ? err.message : 'Failed to generate preview'
     })
   }
 }
Suggestion importance[1-10]: 6

__

Why: This correctly notes that window.api.templates.preview(id) previews the persisted template, so unsaved edits in form are not reflected. Blocking preview until save is a reasonable UX safeguard, though accepting draft state in the preview API would be a fuller solution.

Low
Possible issue
Stabilize drag drop ordering

Avoid relying on dropTarget state inside handleDragEnd, because React state can be
stale if the user drops immediately after onDragOver. Compute the target from the
final active and over ids so pointer and keyboard reorders are deterministic.

src/renderer/src/pages/prefs/templates/OutputSectionList.tsx [129-146]

 const handleDragEnd = (event: DragEndEvent): void => {
   const { active, over } = event
   setActiveId(null)
-  const target = dropTarget
   setDropTarget(null)
-  if (!over || active.id === over.id || !target) return
+  if (!over || active.id === over.id) return
 
   const draggedIndex = sections.findIndex((s) => s.id === active.id)
-  if (draggedIndex === -1) return
+  const overIndex = sections.findIndex((s) => s.id === over.id)
+  if (draggedIndex === -1 || overIndex === -1) return
+
   const dragged = sections[draggedIndex]
   const rest = sections.filter((s) => s.id !== active.id)
-  const overIndexInRest = rest.findIndex((s) => s.id === target.overId)
+  const overIndexInRest = rest.findIndex((s) => s.id === over.id)
   if (overIndexInRest === -1) return
-  const insertAt = target.position === 'before' ? overIndexInRest : overIndexInRest + 1
 
+  const insertAt = draggedIndex < overIndex ? overIndexInRest + 1 : overIndexInRest
   const reordered = [...rest.slice(0, insertAt), dragged, ...rest.slice(insertAt)]
   onChange(reordered.map((s, index) => ({ ...s, order: index })))
 }
Suggestion importance[1-10]: 5

__

Why: Avoiding reliance on dropTarget state is a plausible reliability improvement because drag state updates can be asynchronous. However, the proposed replacement loses the current before/after drop-position behavior and may not preserve precise pointer drop intent.

Low

Previous suggestions

Suggestions up to commit 6792909
CategorySuggestion                                                                                                                                    Impact
Security
Validate session directory input

Validate sessionDir before passing it to resolveTemplateForSession. This IPC
endpoint currently lets the renderer ask the main process to read session.json from
arbitrary filesystem locations.

src/main/templates/ipc.ts [2-80]

-import { join } from 'path'
+import { isAbsolute, join, relative, resolve } from 'path'
 ...
   ipcMain.handle(
     'audist:templates:resolveForSession',
     (_, { sessionDir }: { sessionDir: string }): { id: string; name: string } => {
-      const template = resolveTemplateForSession(sessionDir)
+      const saveDir = resolve(getSaveDirectory())
+      const requestedDir = resolve(sessionDir)
+      const relativePath = relative(saveDir, requestedDir)
+
+      if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
+        throw new Error('Invalid session directory')
+      }
+
+      const template = resolveTemplateForSession(requestedDir)
       return { id: template.id, name: template.name }
     }
   )
Suggestion importance[1-10]: 7

__

Why: The new IPC handler accepts a renderer-provided sessionDir and passes it into filesystem-reading logic, so constraining it to the configured save directory is a sound security hardening. The practical exposure appears limited because only template metadata is returned, but validating main-process file access is still important.

Medium
Possible issue
Protect default template deletion

Guard deletion using store.defaultTemplateId as well as existing.isDefault. If
migrated or stale data has mismatched flags, the current default template can
otherwise be deleted, leaving defaultTemplateId pointing at a missing template.

src/main/templates/logic.ts [136-141]

-if (existing.isDefault) throw new TemplateOperationError('Cannot delete the default template')
+if (store.defaultTemplateId === id || existing.isDefault) {
+  throw new TemplateOperationError('Cannot delete the default template')
+}
 
 return {
   store: { ...store, templates: store.templates.filter((t) => t.id !== id) },
   success: true
 }
Suggestion importance[1-10]: 6

__

Why: This is a valid defensive data-integrity improvement because resolveTemplateForSession uses store.defaultTemplateId, while deletion currently only checks existing.isDefault. It prevents stale or migrated stores with mismatched flags from leaving defaultTemplateId pointing to a deleted template.

Low
Prevent accidental edit loss

useBlocker only covers router navigation, but the preferences window appears to have
a global Escape-to-close handler. Intercept Escape while hasUnsavedChanges is true
so users do not accidentally close the window and lose edits without seeing the
discard confirmation.

src/renderer/src/pages/prefs/templates/TemplateEditorPage.tsx [67-73]

 const blocker = useBlocker(
   useCallback(
     ({ currentLocation, nextLocation }) =>
       hasUnsavedChanges && currentLocation.pathname !== nextLocation.pathname,
     [hasUnsavedChanges]
   )
 )
 
+useEffect(() => {
+  if (!hasUnsavedChanges) return
+
+  const handleEscape = (e: KeyboardEvent): void => {
+    if (e.key !== 'Escape') return
+    e.preventDefault()
+    e.stopPropagation()
+    setPendingBack(true)
+  }
+
+  window.addEventListener('keydown', handleEscape, true)
+  return () => window.removeEventListener('keydown', handleEscape, true)
+}, [hasUnsavedChanges])
+
Suggestion importance[1-10]: 5

__

Why: The concern is plausible because useBlocker only handles router navigation and other PR code references a global Escape-to-close handler in PrefsLayout. However, the proposed capture-phase Escape listener may interfere with child controls like InlineEditableText that already use Escape to cancel editing, so the implementation needs care.

Low
Suggestions up to commit 99dec5a
CategorySuggestion                                                                                                                                    Impact
Security
Validate renderer-provided paths

The new resolveForSession IPC handler accepts an arbitrary sessionDir from the
renderer and passes it to code that reads session.json. Validate that the resolved
path stays inside the configured save directory before reading from it to avoid
exposing main-process filesystem reads to untrusted renderer input.

src/main/templates/ipc.ts [2-80]

-import { join } from 'path'
+import { join, relative, resolve } from 'path'
 ...
 ipcMain.handle(
   'audist:templates:resolveForSession',
   (_, { sessionDir }: { sessionDir: string }): { id: string; name: string } => {
-    const template = resolveTemplateForSession(sessionDir)
+    const root = resolve(getSaveDirectory())
+    const target = resolve(sessionDir)
+    const rel = relative(root, target)
+
+    if (rel === '..' || rel.startsWith(`..${join('/', '').replace('/', '')}`) || resolve(rel) === rel) {
+      throw new Error('Invalid session directory')
+    }
+
+    const template = resolveTemplateForSession(target)
     return { id: template.id, name: template.name }
   }
 )
Suggestion importance[1-10]: 7

__

Why: The resolveForSession IPC handler does trust a renderer-provided sessionDir, so constraining it to getSaveDirectory() is a meaningful hardening improvement. The proposed path check is somewhat awkward and could be simplified, but the underlying issue is relevant and security-oriented.

Medium
Possible issue
Bypass blocker after deletion

Deleting a template with unsaved edits can trigger the route blocker after the
delete succeeds, leaving the user on an editor page for a template that no longer
exists. Add an explicit navigation bypass for destructive actions that intentionally
leave the page.

src/renderer/src/pages/prefs/templates/TemplateEditorPage.tsx [67-126]

+const allowNavigationRef = useRef(false)
+
 const blocker = useBlocker(
   useCallback(
     ({ currentLocation, nextLocation }) =>
-      hasUnsavedChanges && currentLocation.pathname !== nextLocation.pathname,
+      !allowNavigationRef.current &&
+      hasUnsavedChanges &&
+      currentLocation.pathname !== nextLocation.pathname,
     [hasUnsavedChanges]
   )
 )
 ...
 const handleDelete = async (): Promise<void> => {
   if (!id) return
   await window.api.templates.delete(id)
-  navigate('/prefs/templates')
+  allowNavigationRef.current = true
+  navigate('/prefs/templates', { replace: true })
 }
Suggestion importance[1-10]: 7

__

Why: This correctly identifies that useBlocker can intercept the post-delete navigation when hasUnsavedChanges is true, potentially leaving the user on a deleted template editor. A bypass flag for intentional destructive navigation is a practical fix with moderate functional impact.

Medium
Suggestions up to commit e70194e
CategorySuggestion                                                                                                                                    Impact
Security
Validate session directory paths

The new IPC handler trusts renderer-provided sessionDir, allowing a compromised
renderer to make the main process read session.json from arbitrary filesystem
locations. Normalize the path and reject anything outside the configured save
directory before calling resolveTemplateForSession.

src/main/templates/ipc.ts [74-80]

 ipcMain.handle(
   'audist:templates:resolveForSession',
   (_, { sessionDir }: { sessionDir: string }): { id: string; name: string } => {
-    const template = resolveTemplateForSession(sessionDir)
+    const saveRoot = resolve(getSaveDirectory())
+    const requestedDir = resolve(sessionDir)
+    const relativePath = relative(saveRoot, requestedDir)
+
+    if (
+      relativePath === '..' ||
+      relativePath.startsWith(`..${sep}`) ||
+      isAbsolute(relativePath)
+    ) {
+      throw new Error('Invalid session directory')
+    }
+
+    const template = resolveTemplateForSession(requestedDir)
     return { id: template.id, name: template.name }
   }
 )
Suggestion importance[1-10]: 8

__

Why: This is a valid security concern because resolveForSession reads from a renderer-provided sessionDir, which could let a compromised renderer probe arbitrary paths. The suggested path validation is relevant, though the shown patch would also need corresponding path imports.

Medium
General
Prevent stale async updates

These async reads can resolve after the user navigates to a different session,
causing stale summary/template data to be rendered. Add a cancellation guard and
clear summaryTemplateName when the session changes.

src/renderer/src/pages/SessionDetail.tsx [116-121]

 useEffect(() => {
   if (!session) return
-  window.api.summary.read(session.dir).then(setSummary)
-  window.api.transcription.read(session.dir).then(setTranscript)
-  window.api.templates.resolveForSession(session.dir).then((t) => setSummaryTemplateName(t.name))
+
+  let cancelled = false
+  setSummaryTemplateName(null)
+
+  window.api.summary.read(session.dir).then((value) => {
+    if (!cancelled) setSummary(value)
+  })
+  window.api.transcription.read(session.dir).then((value) => {
+    if (!cancelled) setTranscript(value)
+  })
+  window.api.templates.resolveForSession(session.dir).then((t) => {
+    if (!cancelled) setSummaryTemplateName(t.name)
+  })
+
+  return () => {
+    cancelled = true
+  }
 }, [session?.dir])
Suggestion importance[1-10]: 6

__

Why: Adding a cancellation guard is a sound fix for async reads resolving after session changes. The suggestion is useful, but it only clears summaryTemplateName, so existing summary or transcript can still remain visible until replacement reads complete.

Low
Avoid stale preview output

preview(id) renders the persisted template, so pressing Preview after editing but
before saving shows stale output that does not match the visible form. Either send
the draft form to the preview IPC or block preview until changes are saved.

src/renderer/src/pages/prefs/templates/TemplateEditorPage.tsx [134-149]

 const handlePreview = async (): Promise<void> => {
   if (!id) return
+
+  if (hasUnsavedChanges) {
+    setPreviewState({
+      status: 'error',
+      message: 'Save changes before previewing this template.'
+    })
+    return
+  }
+
   const token = ++previewTokenRef.current
   setPreviewState({ status: 'loading' })
   try {
     const { markdown } = await window.api.templates.preview(id)
     if (previewTokenRef.current !== token) return // closed (or re-opened) before this resolved
     setPreviewState({ status: 'done', markdown })
   } catch (err) {
     if (previewTokenRef.current !== token) return
     setPreviewState({
       status: 'error',
       message: err instanceof Error ? err.message : 'Failed to generate preview'
     })
   }
 }
Suggestion importance[1-10]: 6

__

Why: This correctly identifies that window.api.templates.preview(id) previews the persisted template, not the unsaved form state. Blocking preview while hasUnsavedChanges is a reasonable UX/correctness fix, though sending the draft would be a more complete solution.

Low

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 99dec5a

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 6792909

@yoonwaiyan
yoonwaiyan force-pushed the feature/afterword branch from 6792909 to 0da318b Compare July 4, 2026 06:42
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 0da318b

yoonwaiyan and others added 12 commits July 4, 2026 15:04
…ation

A workflow run stalled mid-implementation of AUD-62 (Afterword editor).
Preserving the in-progress files rather than discarding them — this is
NOT a finished ticket, a future run must verify against AUD-62's full
acceptance criteria before treating it as done.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
The prior WIP commit's SystemPromptEditor relied on document.activeElement
to detect a live cursor, but clicking a chip button blurs the textarea
first, so insertion always fell back to appending at the end. Track the
last-known caret position in a ref instead so chip clicks insert at the
cursor as specced.

Also fixes TemplateListPage's overflow-menu "Duplicate & Customise" to
navigate to the new editable copy's editor, matching the editor's own
Duplicate & Customise action and the AUD-62 acceptance criteria.

Verified: typecheck, lint, and e2e (prompt-template-editor.spec.ts,
prompt-templates.spec.ts) all pass. The rest of the AUD-62 editor
(inline-editable name, monospace resizable textarea, highlighted variable
tokens, output section list, action bar, built-in read-only state, delete/
discard dialogs, unsaved-changes tracking) was already implemented by the
earlier stalled run and is confirmed working against the ticket's
acceptance criteria.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Replace the NewTemplateModal placeholder with the full "Create a new
template" dialog: a "Blank template" option plus a 2x2 grid of built-in
presets (PresetCard). Blank creates an empty template via
templates.create({}); each preset duplicates the matching built-in via
templates.duplicate(id). Both close the modal and navigate straight to
the new copy's editor. Closing via the x button, outside click, or
Escape creates nothing.

Adds e2e/new-template-modal.spec.ts covering all three paths.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Add @dnd-kit/core + @dnd-kit/sortable and wire drag-and-drop into
OutputSectionList: the six-dot handle (not the whole row) drives the
drag via useSortable, so inline-editable heading/instruction text stays
clickable. Reordering only happens on drop — sibling rows don't shift
during drag — and a blue drop-zone indicator line renders on whichever
row edge the dragged item is currently over, tracked via onDragOver
without mutating form state. The original slot renders as a dashed
placeholder, and a DragOverlay shows the floating row (shadow, slight
rotation, accent-coloured handle) following the pointer. New order is
only committed to local form state in onDragEnd, keeping it an unsaved
change per the existing Save Changes flow. Keyboard reordering works via
dnd-kit's KeyboardSensor + sortableKeyboardCoordinates.

Adds e2e/output-section-reorder.spec.ts covering pointer drag, keyboard
reorder, and the single-section (no drop indicator) case.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
- Preview close button appeared broken: closing while the LLM preview
  call was still in flight let the resolved promise reopen the modal
  afterward. A request token now invalidates stale resolutions.
- The action bar (Preview / Save / Duplicate & Customise) used
  `sticky bottom-0` inside a padded scroll container, which let it
  overlap trailing content instead of pinning to the window bottom.
  Switched to `fixed` anchored past the sidebar, with matching bottom
  padding on the editor page so content isn't hidden behind it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
The dropdown was anchored left-0 with a 220px min-width wider than the
trigger button, so it grew rightward past the button's bounds. Anchoring
right-0 instead so it grows leftward and stays flush with the button.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
All templates are always usable — the isActive flag never gated which
templates could be selected, it only marked which one a session falls
back to when the per-session selector isn't touched. "Default" names
that behavior accurately; "Active" implied an exclusivity the feature
never had. Renamed end-to-end: data model (isActive -> isDefault,
activeTemplateId -> defaultTemplateId), IPC channel and preload API
(setActive -> setDefault), and all UI copy/props (ACTIVE badge ->
DEFAULT, "Set as Active" -> "Set as Default"). Added a read-time
migration in store.ts so existing local templates.json files (old
field names) still resolve correctly without user action.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Neither the summary view nor the regenerate button gave any indication
of which template produced (or will produce) a session's summary,
making it unclear what "Refresh" would actually do. Adds a resolveForSession
IPC endpoint that surfaces the same resolution summariseSession already
uses, and displays it as "Using template: X" above the summary content.

Follow-up filed as AUD-119: the resolved template isn't currently pinned
to the session, so regenerating an unpinned session after changing the
global default silently switches templates.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Per AUD-104's changelog workflow: per-ticket entries under [Unreleased],
compiled into a version block at release time.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
PR-Agent's own default pr_actions allow-list excludes "synchronize",
so it silently no-ops on every new commit pushed to an existing PR
even though our workflow trigger includes that event. Explicitly
opting synchronize into GITHUB_ACTION_CONFIG.PR_ACTIONS.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
getByText('Meeting Notes') matched two elements once the "Using
template: Default Meeting Notes" label landed in SessionDetail —
both the markdown summary heading and the template name span contain
that substring. Passed locally because the label's async fetch didn't
always resolve before the assertion ran, so it only flaked in CI.
Scoped to getByRole('heading', ...) to target the actual markdown
heading unambiguously.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
PrefsLayout has a blanket "Escape closes the window" listener with no
way to know a more local component (a modal, an inline text field)
already wants to consume that keystroke. NewTemplateModal and
InlineEditableText's own Escape handlers didn't stop propagation, so
closing the modal via Escape also hid the entire Preferences window.

This is what caused the new-template-modal.spec.ts CI failure on
Ubuntu/Windows: the Escape sub-scenario hid the window, and the next
scenario's click on a now-hidden window hung until timeout — something
macOS's Electron/CDP integration tolerates but Windows/Linux do not.
Passed locally on macOS for the same reason, so this was never caught
until it hit the real CI matrix.

Also bumped output-section-reorder.spec.ts's fixed inter-keystroke
waits (50-100ms) to more generous values (200-300ms) — dnd-kit's
keyboard sensor state settling apparently doesn't reliably complete
within the tighter windows on CI hardware, causing an intermittent
"reorder didn't happen" failure on Ubuntu.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@yoonwaiyan

Copy link
Copy Markdown
Owner Author

/review

Leftover from rebasing an older synchronize-trigger fix on top of
main's already-merged rewrite to comment-triggered-only. The
pull_request trigger this env var existed to fix no longer exists on
this workflow, so it was dead, confusing config. File now matches
main's version exactly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 0da318b

@yoonwaiyan
yoonwaiyan force-pushed the feature/afterword branch from 0da318b to b4430f5 Compare July 4, 2026 07:07
@yoonwaiyan
yoonwaiyan merged commit a199d2e into main Jul 4, 2026
6 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant