Skip to content

Add prompt library with multiple LLM results per transcription#53

Merged
moona3k merged 73 commits intomainfrom
feature/prompt-library-multi-summary
Apr 5, 2026
Merged

Add prompt library with multiple LLM results per transcription#53
moona3k merged 73 commits intomainfrom
feature/prompt-library-multi-summary

Conversation

@moona3k
Copy link
Copy Markdown
Owner

@moona3k moona3k commented Apr 4, 2026

Overview

Closes #51.

Replaces the single-summary model with a prompt library and persistent multi-result architecture. Users choose from community or custom prompts, layer on extra instructions, and keep every LLM result per transcription.

Image Image Image Image

Architecture

Data model

Each transcription can have many LLM results. Each result snapshots the prompt that produced it so the record remains meaningful even if the prompt is later edited or deleted.

┌──────────────┐       ┌──────────────────┐       ┌──────────────┐
│   prompts    │       │    summaries     │       │transcriptions│
├──────────────┤       │  (prompt results)│       ├──────────────┤
│ id           │       ├──────────────────┤       │ id           │
│ name         │       │ id               │       │ summary (*)  │
│ content      │  snap │ transcriptionId ─┼──FK──▶│ ...          │
│ category     │◀ ─ ─ ─│ promptName       │       └──────────────┘
│ isBuiltIn    │       │ promptContent    │
│ isVisible    │       │ extraInstructions│        (*) legacy column,
│ isAutoRun    │       │ content          │        kept in sync as a
│ sortOrder    │       │ createdAt        │        best-effort compat
│ createdAt    │       │ updatedAt        │        path for export
│ updatedAt    │       └──────────────────┘
└──────────────┘        CASCADE on transcription delete

The promptssummaries link is a denormalized snapshot (dashed), not a foreign key. This is intentional: results preserve the exact prompt wording at generation time.

Generation pipeline

LLM results are queued and processed serially per transcription. Only one stream is active at a time.

User action / auto-run
        │
        ▼
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│     Enqueue     │────▶│    Streaming     │────▶│    Persisted     │
│  PendingGen     │     │  streamingTask   │     │  PromptResult    │
│  state: .queued │     │  state: .streaming│    │  saved to DB     │
└─────────────────┘     └────────┬─────────┘     └────────┬────────┘
                                 │                         │
                           cancel / error            processNext()
                                 │                         │
                                 ▼                         ▼
                        ┌────────────────┐        next queued item
                        │   Discarded    │        (if any)
                        │  removed from  │
                        │  pending array │
                        └────────────────┘

Everything runs on @MainActor — no concurrent mutation of the pending array.

ViewModel ownership

TranscriptionViewModel
│
│  owns PromptResultsViewModel
│  │   ├── generation queue + streaming lifecycle
│  │   ├── result persistence + unread tracking
│  │   └── model/provider selection
│  │
│  │  uses PromptsViewModel (shared)
│  │       ├── prompt CRUD
│  │       ├── visibility + auto-run toggling
│  │       └── prompt library sheet
│  │
│  ├── tab routing (.transcript | .result(id) | .generation(id) | .chat)
│  ├── transcription lifecycle
│  └── legacy summary sync callbacks

PromptResultsViewModel communicates back to TranscriptionViewModel via closures (onGenerationCompleted, onDeletedPromptResult, onLegacySummaryChanged) rather than direct references, keeping the dependency one-directional.

Tab model

Each persisted result and in-flight generation gets its own tab:

┌────────────┬─────────────┬─────────────────┬──────────────────┬──────┐
│ Transcript │ Gen. Summary│ Meeting Notes   │ ● Action Items…  │  +   │
│            │  (streaming)│  (persisted)    │   (persisted)    │      │
└────────────┴─────────────┴─────────────────┴──────────────────┴──────┘
  .transcript  .generation    .result(id)       .result(id)     popover
               (id)                              unread dot

Streaming tabs transition to result tabs on completion. Cancelled/failed generations remove their tab.

What Changed for Users

  • Prompt picker with community and custom prompts
  • Extra instructions field layered on top of the selected prompt
  • Multiple LLM results per transcription as individually scrollable tabs, newest appended right
  • Prompt library sheet — hide community prompts, CRUD custom prompts, set any prompt as auto-run
  • Auto-run triggers on configured prompt(s) when transcription completes
  • Streaming with real-time token display and cancel support
  • Context menus on result tabs for copy, export, regenerate, and delete

Implementation

Data model and migration

  • Prompt model — name, content, category, visibility, auto-run flag, sort order
  • PromptResult model — transcription FK, snapshotted prompt name/content/extra instructions, generated content, timestamps
  • prompts and summaries tables with cascade delete on transcription removal
  • 7 community prompts seeded on first launch (General Summary is auto-run by default)
  • Migration preserves existing transcriptions.summary data as a PromptResult attributed to the default prompt

Service changes

  • LLMServiceProtocol extended with generatePromptResultStream(transcript:systemPrompt:) for custom system prompts
  • Existing call sites compile unchanged via extension-based default overloads
  • Legacy transcriptions.summary column synced best-effort (non-fatal on failure)

Spec and documentation

  • ADR-013 documents the prompt-library / multi-result architecture
  • spec/12-processing-layer.md updated for the shipped v0.7 contract
  • Future actions, workflows, and agent design moved to draft spec/13-agent-workflows.md

Migration and Compatibility

  • Existing summary data migrated to PromptResult records — no data loss
  • transcriptions.summary remains populated (best-effort) for export and serialization consumers
  • Community prompts can be hidden but not edited or deleted; custom prompts support full CRUD
  • Built-in prompt reconciliation runs on every launch to handle app updates

Test Plan

  • swift test — 1156 tests passing (1143 XCTest + 13 Swift Testing)
  • Prompt repository: seeding, reconciliation, CRUD, case-insensitive uniqueness, visibility toggling
  • PromptResult repository: persistence, ordering, cascade deletes, multiple results per transcription
  • LLM service: custom system prompt plumbing, context truncation
  • PromptResultsViewModel: generation, regeneration, deletion, streaming lifecycle, unread tracking
  • PromptsViewModel: prompt CRUD, visibility, auto-run toggling
  • TranscriptionViewModel: tab routing, transcription switching cancels in-flight work
  • Database migration: legacy summary preservation, built-in prompt reconciliation

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements instruction-aware summary generation with customizable templates. It restructures summary tab management to use ID-based navigation, introduces a generation popover with streaming support, reduces built-in prompts to two templates ("Concise Summary" and "Detailed Summary"), and refactors view-model callback wiring to coordinate summary and transcription state changes.

Changes

Cohort / File(s) Summary
Tab and Navigation Model
Sources/MacParakeet/AppDelegate.swift, Sources/MacParakeetViewModels/TranscriptionViewModel.swift
Changed TranscriptTab enum to support .summary(id:UUID) and .streaming cases instead of fixed .summary. Replaced hasSummaries with hasSummaryTabs. Updated AppDelegate callback wiring so summaryViewModel callbacks now drive transcriptionViewModel state changes (tab selection, deletion handling).
Summary Generation and Streaming
Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift
Introduced summary generation popover with prompt selection and optional extra-instructions field. Added streaming UI support with cancel capability. Replaced fixed summary pane with per-summary content panes and dedicated streaming pane. Implemented dynamic tab ordering and scrollable tab bar with context menus. Added per-summary copy-confirmation tracking and regeneration-in-place behavior.
Summary View Model State and Callbacks
Sources/MacParakeetViewModels/SummaryViewModel.swift
Replaced boolean summaryBadge with ID-based badgedSummaryID and clearBadge(for:) method. Removed expansion state (expandedSummaryIDs). Added public callbacks onSelectSummaryTab and onDeletedSummary. Introduced regenerateSummary(_:transcript:) entry point. Updated generation control flow to cancel existing streams and perform delete-and-replace on existing summaries with same prompt.
Prompt Management UI
Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift
Enhanced text entry UI with custom-styled editor, horizontal padding, and placeholder overlay ("Write your prompt instructions..."). Changed "Add Prompt" button from .borderedProminent to .bordered with custom tint. Added Cmd+Return keyboard shortcut to "Save" button in edit sheet.
Built-in Prompts
Sources/MacParakeetCore/Models/Prompt.swift
Reduced built-in summary prompts from 7 to 2. Renamed first two: "General Summary" → "Concise Summary" and "Meeting Notes" → "Detailed Summary" with updated prompt text. Removed "Action Items", "Key Quotes", "Study Notes", "Bullet Points", and "Executive Brief".
Prompt Error Handling
Sources/MacParakeetViewModels/PromptsViewModel.swift
Added automatic validation-error clearing via didSet on newName and newContent properties, triggering resetValidationError() on field changes rather than only on successful operations.
LLM Service and System Prompt
Sources/MacParakeetCore/Services/LLMService.swift
Updated default summary system-prompt text from "summarizes transcripts" to "Summarize this transcript" (formatting/content normalization).
Database Migration
Sources/MacParakeetCore/Database/DatabaseManager.swift
Removed post-migration cleanup step that set transcriptions.summary to NULL; legacy summary column now preserved for future reference.
Test Coverage and Fixtures
Tests/MacParakeetTests/Database/DatabaseManagerTests.swift, Tests/MacParakeetTests/Database/PromptRepositoryTests.swift, Tests/MacParakeetTests/Database/SummaryRepositoryTests.swift, Tests/MacParakeetTests/Services/LLMServicePromptTests.swift, Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift, Tests/MacParakeetTests/ViewModels/SummaryViewModelTests.swift, Tests/MacParakeetTests/ViewModels/TranscriptionViewModelTests.swift, Tests/MacParakeetTests/Services/LLMServiceTests.swift
Updated all test expectations and fixtures to reflect new built-in prompt names ("Concise Summary", "Detailed Summary"), new badge model semantics (badgedSummaryID instead of summaryBadge), removal of expansion state, prompt count assertions (7→2), and new assertion text in LLM service tests.
Configuration
.gitignore
Added recovery directory pattern recup_dir*/ to ignore recovery artifacts.

Sequence Diagrams

sequenceDiagram
    participant User
    participant TranscriptResultView
    participant SummaryViewModel
    participant LLMService
    participant TranscriptionViewModel

    User->>TranscriptResultView: Tap "+" generate button
    TranscriptResultView->>TranscriptResultView: Show generation popover
    User->>TranscriptResultView: Select prompt & optionally add extra instructions
    User->>TranscriptResultView: Tap generate
    TranscriptResultView->>SummaryViewModel: generateSummary(transcript, promptID, extraInstructions)
    SummaryViewModel->>SummaryViewModel: startGeneration() - cancel any existing stream
    SummaryViewModel->>LLMService: Stream summary based on prompt
    LLMService-->>SummaryViewModel: Streaming chunks
    SummaryViewModel->>SummaryViewModel: Update isStreaming, streamingContent
    SummaryViewModel->>TranscriptResultView: Notify state changes
    TranscriptResultView->>TranscriptResultView: Render .streaming tab with live content
    LLMService-->>SummaryViewModel: Streaming complete
    SummaryViewModel->>SummaryViewModel: Save new Summary to repo
    SummaryViewModel->>SummaryViewModel: Delete existing summary for same prompt
    SummaryViewModel->>SummaryViewModel: Emit onDeletedSummary callback
    SummaryViewModel->>SummaryViewModel: Emit onSelectSummaryTab(newSummaryID)
    SummaryViewModel->>TranscriptionViewModel: Callback: select .summary(newSummaryID)
    TranscriptionViewModel->>TranscriptResultView: Update selectedTab
    TranscriptResultView->>TranscriptResultView: Switch to summary tab, clear badge
Loading
sequenceDiagram
    participant User
    participant SummaryTab
    participant SummaryViewModel
    participant TranscriptionViewModel
    participant TranscriptResultView

    User->>SummaryTab: Tap context menu "Delete Summary"
    SummaryTab->>TranscriptResultView: Trigger delete action
    TranscriptResultView->>TranscriptResultView: Show delete confirmation alert
    User->>TranscriptResultView: Confirm deletion
    TranscriptResultView->>SummaryViewModel: deleteSummary(summaryID)
    SummaryViewModel->>SummaryViewModel: Delete from repo, remove from summaries array
    SummaryViewModel->>SummaryViewModel: Emit onDeletedSummary(summaryID)
    SummaryViewModel->>TranscriptionViewModel: Callback: handleSummaryDeleted(summaryID)
    TranscriptionViewModel->>TranscriptionViewModel: Reset selectedTab to .transcript if matching deleted ID
    TranscriptionViewModel->>TranscriptResultView: Notify selectedTab changed
    TranscriptResultView->>TranscriptResultView: Update tab bar, show transcript content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #41: Modifies DatabaseManager migration logic to add/alter migration steps; this PR also changes migration behavior by removing a legacy cleanup step, creating interdependency in database schema evolution.
  • PR #32: Refactors AppDelegate view-model callback wiring and TranscriptionViewModel state handling; this PR further evolves that callback routing to coordinate summary tab selection and deletion across view models.

Poem

🐰 Whiskers twitch with summary glee,
Templates bloom where prompts used to be,
Streaming summaries flow through the tabs,
Custom instructions fill those instruction gaps,
Two prompts thrive where seven once stood,
This regeneration's clearly good! 🌙✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully implements all primary requirements from issue #51: provides preset summary templates via built-in prompts [Prompt.swift], offers custom instruction field in summary generation UI [TranscriptResultView.swift], supports multi-summary persistence with summary selection [SummaryViewModel.swift, TranscriptionViewModel.swift], and includes prompt management CRUD [PromptsViewModel.swift]. Nice-to-have items (saving custom templates, remembering last-used template) are partially addressed through custom template creation.
Out of Scope Changes check ✅ Passed All code changes are aligned with the stated objectives: AppDelegate wiring manages new view-model callbacks, SummaryPromptsView adds prompt management UI, TranscriptResultView implements multi-summary tab flow with generation popover, database/repository changes support persistence, LLMService integrates custom prompts, and test updates validate new functionality. The .gitignore addition is a standard maintenance change.
Title check ✅ Passed The title directly captures the main architectural change: introduction of a prompt library enabling multiple LLM-generated summaries per transcription, which is the primary objective of this PR.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/prompt-library-multi-summary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a Prompt Library and multi-summary architecture, allowing users to generate and manage multiple summaries per transcript using built-in or custom prompts. Key changes include new database tables for prompts and summaries, dedicated view models for summary and prompt management, and a revamped summary UI with collapsible cards and prompt selection. Feedback includes a warning about a destructive database update that clears legacy summary data, a suggestion to replace a magic number with a named constant for the auto-summary threshold, and a fix for a redundant nil-coalescing operator in the summary generation logic.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift (1)

38-44: Use XCTUnwrap instead of force-unwrap for safer test code.

The force-unwrap on first(where:) will crash the test if "Meeting Notes" isn't found, making failures harder to diagnose. The same pattern appears in testRestoreDefaultsShowsAllBuiltIns.

🛡️ Safer unwrapping
 func testToggleVisibilityChangesPromptState() {
-    let prompt = viewModel.prompts.first { $0.name == "Meeting Notes" }!
+    let prompt = try XCTUnwrap(viewModel.prompts.first { $0.name == "Meeting Notes" })

     viewModel.toggleVisibility(prompt)

Note: This requires changing the test to throws.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift` around lines
38 - 44, Replace force-unwraps in tests with XCTUnwrap to avoid crashes: in
testToggleVisibilityChangesPromptState use let prompt = try
XCTUnwrap(viewModel.prompts.first { $0.name == "Meeting Notes" }) and mark the
test as throws; do the same for the other occurrence in
testRestoreDefaultsShowsAllBuiltIns (unwrap the prompt/result using XCTUnwrap
and make that test throw) so failures produce informative assertions rather than
runtime crashes.
Tests/MacParakeetTests/Database/PromptRepositoryTests.swift (1)

13-19: Hardcoded prompt count (7) may drift from source of truth.

The assertion XCTAssertEqual(prompts.count, 7) is coupled to the number of built-in prompts in Prompt.builtInSummaryPrompts(). If that array changes, this test will fail silently without indicating the root cause. Consider referencing the source directly.

♻️ Reference source of truth
 func testBuiltInPromptsSeededAfterMigration() throws {
     let prompts = try repo.fetchAll()
-    XCTAssertEqual(prompts.count, 7)
+    XCTAssertEqual(prompts.count, Prompt.builtInSummaryPrompts().count)
     XCTAssertTrue(prompts.allSatisfy(\.isBuiltIn))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/MacParakeetTests/Database/PromptRepositoryTests.swift` around lines 13
- 19, The test testBuiltInPromptsSeededAfterMigration currently asserts a
hardcoded count (7); change it to compare against the source-of-truth by using
Prompt.builtInSummaryPrompts().count instead of 7 (keep the other assertions
intact), i.e. fetch prompts via repo.fetchAll() and assert prompts.count ==
Prompt.builtInSummaryPrompts().count so the test follows the canonical data in
Prompt.builtInSummaryPrompts().
Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift (1)

119-131: Use DesignSystem spacing token instead of hardcoded padding.

Line 122 uses .padding(8) which should reference a DesignSystem spacing token for consistency. The same pattern appears on line 202 in editSheet. As per coding guidelines, all views should reference DesignSystem, not hardcoded values.

♻️ Use DesignSystem token
 TextEditor(text: $viewModel.newContent)
     .font(DesignSystem.Typography.body)
     .frame(minHeight: 140)
-    .padding(8)
+    .padding(DesignSystem.Spacing.sm)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift` around
lines 119 - 131, Replace the hardcoded .padding(8) with the DesignSystem spacing
token to follow the design tokens; locate the TextEditor using
viewModel.newContent in SummaryPromptsView (and the matching padding in
editSheet) and swap .padding(8) for the appropriate token from the design system
(e.g., DesignSystem.Spacing.small or the equivalent spacing constant in
DesignSystem.Layout) so the view uses the centralized spacing token instead of a
literal value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plans/active/prompt-library-multi-summary.md`:
- Around line 1-7: Move the "Prompt Library + Multi-Summary Implementation Plan"
document out of the active plans set into the completed plans archive and update
its metadata: change the Status header from **ACTIVE** to **IMPLEMENTED** and
rename the file to follow the completed naming convention including date and
feature, e.g. 2026-04-prompt-library-multi-summary.md; ensure the document
content and links remain unchanged apart from the header and filename update so
the plan is archived properly.

In `@Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift`:
- Around line 907-923: Replace the view-wide Boolean summaryCopied and shared
copiedResetTask with a per-card identifier and reset task so only the card that
was copied shows "Copied": add `@State` private var copiedSummaryID: UUID? and
`@State` private var summaryCopiedResetTask: Task<Void, Never>?, then in the
Button action set copiedSummaryID = summaryID (or the UUID property for that
card), cancel and replace summaryCopiedResetTask with a Task that awaits 2
seconds and then sets copiedSummaryID = nil; update the Label condition to show
"Copied" when copiedSummaryID == summaryID (instead of using summaryCopied) and
keep the existing transcript/chat copy state (copiedResetTask) untouched to
avoid conflicts.

In `@Sources/MacParakeetCore/Database/PromptRepository.swift`:
- Around line 53-56: In delete(id:) ensure built-in prompts cannot be removed by
first reading the Prompt entry inside the dbQueue (e.g., fetch Prompt by id with
a read or write transaction), check its built-in flag/property (e.g.,
Prompt.isBuiltin or Prompt.builtin), and if true throw a repository-level error
(create/use something like PromptRepositoryError.cannotDeleteBuiltin); only call
Prompt.deleteOne(db, key: id) when the prompt is not built-in. This enforces the
“hide-only” invariant in the repository layer rather than relying on UI checks.

In `@Sources/MacParakeetViewModels/SummaryViewModel.swift`:
- Around line 251-276: The save+mirror must be atomic: either wrap both writes
in a DB transaction (if your repos support it) or make the legacy column update
best-effort so a failing transcriptionRepo.updateSummary(...) does not cause the
whole operation to appear failed after summaryRepo.save(summary) succeeded.
Concretely, prefer one of two fixes: 1) use a transaction API (e.g.
summaryRepo.transaction { try summaryRepo.save(summary); try
transcriptionRepo.updateSummary(id: targetTranscriptionID, summary:
summary.content) }) so both succeed or rollback together; or 2) call try
summaryRepo?.save(summary) first and then call
transcriptionRepo?.updateSummary(...) inside its own do-catch that logs the
error but does not rethrow (or if you must revert, explicitly delete the saved
summary on update failure), ensuring UI state updates (isStreaming, summaries
insert, onSummariesChanged, summaryBadge) proceed when the primary save
succeeded. Use the existing symbols summaryRepo.save,
transcriptionRepo.updateSummary(id:), targetTranscriptionID, and the surrounding
UI-update block when implementing.

In `@spec/12-processing-layer.md`:
- Around line 147-150: The spec omits the steady-state legacy mirror behavior
implemented in Sources/MacParakeetViewModels/SummaryViewModel.swift; update
spec/12-processing-layer.md to document that the active SummaryViewModel
continues to mirror the latest summary back into transcriptions.summary after
summary save and delete operations (the code paths around SummaryViewModel's
save/delete handlers at the blocks around lines 251-253 and 306-309), describing
that this ongoing compatibility contract keeps transcriptions.summary populated
for legacy consumers even after the migration/null-out.

In `@spec/13-agent-workflows.md`:
- Around line 34-59: The fenced code blocks in the ASCII diagrams (the
triple-backtick fences around the box starting with
"┌─────────────────────────────────────────────────────────────────┐" and
containing "┌─ Prompt Library ──────────────────────────────┐  SHIPPED     │" )
are missing language identifiers; add an explicit language tag (e.g., ```text)
to each opening fence to satisfy MD040; apply the same fix to the other fenced
blocks called out in the comment (lines referenced around the second box and the
ranges 72-80, 94-104, 110-112, 118-125, 180-184) so every triple-backtick has a
language identifier.
- Line 3: Replace the invalid "Status: **DRAFT**" header in the file (the line
containing "Status: **DRAFT**") with one of the repository's allowed status
labels (ACTIVE, IMPLEMENTED, HISTORICAL, or PROPOSAL); update the header to the
appropriate canonical status (e.g., "Status: **PROPOSAL**") so it conforms to
the project's documentation guidelines.

In `@Tests/MacParakeetTests/ViewModels/SummaryViewModelTests.swift`:
- Line 106: Replace fixed Task.sleep calls in SummaryViewModelTests.swift with a
state-based waiting helper: add a private async
waitUntil(timeout:poll:condition:) helper in the test file that polls a
`@Sendable` condition using Task.sleep(poll) until it becomes true or times out,
then update each occurrence of try await Task.sleep(for: .milliseconds(200))
(and the other listed occurrences at 134, 141, 225, 249, 271) to call waitUntil
with an appropriate timeout and a condition closure that checks the actual test
state (e.g., viewModel.state, an expectation flag, or a published property)
instead of sleeping; ensure the helper calls XCTFail on timeout so failures are
deterministic and provide clear messages.

---

Nitpick comments:
In `@Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift`:
- Around line 119-131: Replace the hardcoded .padding(8) with the DesignSystem
spacing token to follow the design tokens; locate the TextEditor using
viewModel.newContent in SummaryPromptsView (and the matching padding in
editSheet) and swap .padding(8) for the appropriate token from the design system
(e.g., DesignSystem.Spacing.small or the equivalent spacing constant in
DesignSystem.Layout) so the view uses the centralized spacing token instead of a
literal value.

In `@Tests/MacParakeetTests/Database/PromptRepositoryTests.swift`:
- Around line 13-19: The test testBuiltInPromptsSeededAfterMigration currently
asserts a hardcoded count (7); change it to compare against the source-of-truth
by using Prompt.builtInSummaryPrompts().count instead of 7 (keep the other
assertions intact), i.e. fetch prompts via repo.fetchAll() and assert
prompts.count == Prompt.builtInSummaryPrompts().count so the test follows the
canonical data in Prompt.builtInSummaryPrompts().

In `@Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift`:
- Around line 38-44: Replace force-unwraps in tests with XCTUnwrap to avoid
crashes: in testToggleVisibilityChangesPromptState use let prompt = try
XCTUnwrap(viewModel.prompts.first { $0.name == "Meeting Notes" }) and mark the
test as throws; do the same for the other occurrence in
testRestoreDefaultsShowsAllBuiltIns (unwrap the prompt/result using XCTUnwrap
and make that test throw) so failures produce informative assertions rather than
runtime crashes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad12ed9e-6e87-4e80-a78b-6179d99f9d2c

📥 Commits

Reviewing files that changed from the base of the PR and between ae6c5c8 and 9f1e62e.

📒 Files selected for processing (30)
  • CLAUDE.md
  • Sources/MacParakeet/App/AppEnvironment.swift
  • Sources/MacParakeet/AppDelegate.swift
  • Sources/MacParakeet/Views/MainWindowView.swift
  • Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift
  • Sources/MacParakeet/Views/Transcription/TranscribeView.swift
  • Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift
  • Sources/MacParakeetCore/Database/DatabaseManager.swift
  • Sources/MacParakeetCore/Database/PromptRepository.swift
  • Sources/MacParakeetCore/Database/SummaryRepository.swift
  • Sources/MacParakeetCore/Models/Prompt.swift
  • Sources/MacParakeetCore/Models/Summary.swift
  • Sources/MacParakeetCore/Services/LLMService.swift
  • Sources/MacParakeetViewModels/PromptsViewModel.swift
  • Sources/MacParakeetViewModels/SummaryViewModel.swift
  • Sources/MacParakeetViewModels/TranscriptionViewModel.swift
  • Tests/MacParakeetTests/Database/DatabaseManagerTests.swift
  • Tests/MacParakeetTests/Database/PromptRepositoryTests.swift
  • Tests/MacParakeetTests/Database/SummaryRepositoryTests.swift
  • Tests/MacParakeetTests/Services/LLMServicePromptTests.swift
  • Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift
  • Tests/MacParakeetTests/ViewModels/SummaryViewModelTests.swift
  • Tests/MacParakeetTests/ViewModels/TranscriptionViewModelTests.swift
  • Tests/MacParakeetTests/ViewModels/ViewModelMocks.swift
  • plans/active/prompt-library-multi-summary.md
  • spec/11-llm-integration.md
  • spec/12-processing-layer.md
  • spec/13-agent-workflows.md
  • spec/README.md
  • spec/adr/013-prompt-library-multi-summary.md

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Tests/MacParakeetTests/Database/DatabaseManagerTests.swift`:
- Around line 6-18: The prePromptLibraryMigrationIDs test seed is missing the
migration ID "v0.7-snippet-key-action", so update the
prePromptLibraryMigrationIDs array in DatabaseManagerTests (the variable
prePromptLibraryMigrationIDs) to include "v0.7-snippet-key-action" positioned
before "v0.7-prompts-and-summaries" so the test
(testPromptSummaryMigrationPreservesLegacySummaryColumn) will apply the same
migration order as DatabaseManager.swift where v0.7-snippet-key-action is
registered before v0.7-prompts-and-summaries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1dab373-921f-4f4d-bc9d-92bb1632ca77

📥 Commits

Reviewing files that changed from the base of the PR and between 9f1e62e and eb11ff4.

📒 Files selected for processing (3)
  • Sources/MacParakeetCore/Database/DatabaseManager.swift
  • Sources/MacParakeetViewModels/SummaryViewModel.swift
  • Tests/MacParakeetTests/Database/DatabaseManagerTests.swift

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift`:
- Around line 939-944: The summary copy logic currently reuses copiedResetTask
(used by copyToClipboard()), causing one copy's "Copied!" indicator to be
cancelled by the other; introduce a dedicated Task state (e.g., add `@State`
private var summaryCopiedResetTask: Task<Void, Never>?) and update the summary
copy flow to set and cancel only summaryCopiedResetTask when scheduling the
two-second reset (instead of touching copiedResetTask), set/reset
copiedSummaryID as you already do, and ensure summaryCopiedResetTask is
cancelled and nilled in onDisappear alongside the existing cleanup so each copy
indicator has its own reset task.
- Around line 29-30: Remove the dead boolean state by deleting the unused
property summaryCopied from TranscriptResultView; the view already uses the
per-card identifier copiedSummaryID for copy-state tracking, so remove the
declaration "@State private var summaryCopied = false" and ensure no other code
references summaryCopied (if any references exist, switch them to use
copiedSummaryID instead).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f22c7bc3-432a-4545-b393-6af3d7b7ce89

📥 Commits

Reviewing files that changed from the base of the PR and between eb11ff4 and 0ee99e1.

📒 Files selected for processing (4)
  • Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift
  • Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift
  • Sources/MacParakeetViewModels/PromptsViewModel.swift
  • Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift
✅ Files skipped from review due to trivial changes (1)
  • Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift`:
- Around line 724-736: The context menu sets
summaryViewModel.pendingDeleteSummary from inside the TranscriptResultView
contextMenu (when case .summary(let id)), but the confirmation .alert is defined
inside summaryContentPane(summaryID:) which may not be mounted, causing the
delete confirmation to never appear; move the alert out of summaryContentPane
and into a parent view that is always mounted (e.g., TranscriptResultView or the
immediate container that owns summaryViewModel) so that the .alert observing
summaryViewModel.pendingDeleteSummary is always attached, keep the same binding
to summaryViewModel.pendingDeleteSummary and the same deletion action, and
remove the alert from summaryContentPane(summaryID:) to avoid duplicate alerts.
- Around line 665-685: orderedTabs currently always includes the .chat tab
causing a non-functional Chat to appear; update the orderedTabs computed
property (involving TranscriptionViewModel.TranscriptTab) to only append .chat
when the chat feature is actually available by gating it on
viewModel.llmAvailable || viewModel.hasConversations; keep the existing logic
that appends .transcript, summary tabs from
summaryViewModel.summaries.reversed(), and .streaming when
summaryViewModel.isStreaming, but move the .chat append behind the availability
check so the Chat tab is only present when usable.

In `@Sources/MacParakeetViewModels/SummaryViewModel.swift`:
- Around line 263-275: Currently the code deletes the existing summary (using
summaryRepo?.delete and removing from summaries) before ensuring the new summary
is persisted and mirrored, which can lose the last good summary on save/mirror
failure; change the order so you first persist the new summary via
summaryRepo.save(summary) and update the transcription via
transcriptionRepo.updateSummary(id:targetTranscriptionID,
summary:summary.content) (throwing on errors), then only after those succeed
locate and remove the old summary from summaries, call
summaryRepo.delete(id:existing.id) without swallowing errors, and invoke
onDeletedSummary(existing.id); if your repo supports transactions, perform the
save + delete in a single transaction to atomically swap the rows and avoid
duplicates on reload, and ensure onLegacySummaryChanged is called after the
successful mirror.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58f4499b-3962-4b1e-98aa-202620787b18

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee99e1 and 5ab4d6d.

📒 Files selected for processing (15)
  • .gitignore
  • Sources/MacParakeet/AppDelegate.swift
  • Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift
  • Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift
  • Sources/MacParakeetCore/Models/Prompt.swift
  • Sources/MacParakeetCore/Services/LLMService.swift
  • Sources/MacParakeetViewModels/SummaryViewModel.swift
  • Sources/MacParakeetViewModels/TranscriptionViewModel.swift
  • Tests/MacParakeetTests/Database/PromptRepositoryTests.swift
  • Tests/MacParakeetTests/Database/SummaryRepositoryTests.swift
  • Tests/MacParakeetTests/Services/LLMServicePromptTests.swift
  • Tests/MacParakeetTests/Services/LLMServiceTests.swift
  • Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift
  • Tests/MacParakeetTests/ViewModels/SummaryViewModelTests.swift
  • Tests/MacParakeetTests/ViewModels/TranscriptionViewModelTests.swift
✅ Files skipped from review due to trivial changes (5)
  • .gitignore
  • Tests/MacParakeetTests/Services/LLMServiceTests.swift
  • Tests/MacParakeetTests/Services/LLMServicePromptTests.swift
  • Tests/MacParakeetTests/Database/PromptRepositoryTests.swift
  • Tests/MacParakeetTests/Database/SummaryRepositoryTests.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Sources/MacParakeetCore/Services/LLMService.swift
  • Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift
  • Sources/MacParakeetCore/Models/Prompt.swift
  • Tests/MacParakeetTests/ViewModels/TranscriptionViewModelTests.swift

moona3k and others added 18 commits April 4, 2026 14:40
- Collapsed cards show content preview subtitle (first ~100 chars)
- Copy/Delete buttons hidden when collapsed, visible when expanded
- Dropdown separates built-in and custom prompts with divider
- Extra instructions collapsed behind "+ Add instructions" disclosure
- TextEditor placeholders and better contrast in management sheet
- Smooth expand/collapse animation on summary cards

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Copy turns green with checkmark on success
- Delete gets trash icon for visual alignment with Copy
- Both buttons use consistent HStack(icon + text) layout
- Remove collapsed card content preview (distracting)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix per-card copied state (was shared across all cards)
- Copy shows green checkmark per individual card
- Remove box-in-box markdown content (divider instead of nested card)
- Zen empty state: minimal text, sparkles icon, breathing room
- Keyboard shortcut: Cmd+Return on Generate and Save buttons
- "Add instructions" styled as accent-colored link
- Fix alert copy: "This action cannot be undone"
- Management sheet: single primary button (Done), Add Prompt secondary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a summary card is collapsed, the entire bar is now the click
target — not just the chevron and text. Spacer moves inside the
button label when collapsed, outside when expanded (so Copy/Delete
buttons remain independently clickable).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Collapsed cards: entire card area is tappable via onTapGesture +
contentShape(Rectangle()). Expanded cards: chevron + title row
is tappable to collapse, Copy/Delete remain independent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move Spacer() inside the tappable HStack so the entire header row
(chevron + title + empty space) triggers collapse. Copy/Delete
buttons sit outside the tap zone and remain independent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7 built-in prompts was overwhelming. Condensed to just two — Concise Summary
and Detailed Summary — with everything else available as user-created custom
prompts. Also fixed TextEditor placeholder alignment in SummaryPromptsView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each generated summary now gets its own first-class tab in the tab bar
instead of being nested as collapsible cards inside a single Summary tab.
Generation controls moved to a "+" popover button in the tab bar.

- TranscriptTab enum: dynamic .summary(id:) and .streaming cases
- Scrollable tab bar with context menu (copy/delete) on summary tabs
- Full-viewport summary content pane per tab
- Streaming pane with cancel button and live markdown preview
- Generation popover with prompt selector, model picker, extra instructions
- Per-summary badge (badgedSummaryID) replaces global summaryBadge
- Removed accordion state (expandedSummaryIDs, toggleExpanded)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tab capsule already shows streaming dots, so the content pane no longer
needs AIStreamingIndicator or SummarySkeletonView. Replaced with a clean
native ProgressView spinner + "Generating summary..." text while waiting
for first token. Cancel button styled more subtly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chat is now a fixed tab right after Transcript. Summary tabs follow,
with newest first. The + button stays at the end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace bulky AIStreamingIndicator dots with pulsing sparkles icon
  via SF Symbols .symbolEffect(.pulse) — zero extra width, premium feel
- New summary tabs now append to the right (oldest first in tab order)
- Streaming tab always appears at the rightmost position before +

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cancel streaming now navigates to Transcript tab. Added safety fallbacks
in contentArea: if selectedTab points to a streaming tab that's no longer
active or a summary tab whose summary was deleted, falls back to Transcript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## What Changed
- SummaryViewModel: Allow cancelling in-flight stream to start new generation
  (removes !isStreaming guard, calls cancelStreaming() first). Added
  regenerateSummary() method for re-running with same prompt/instructions.
  Duplicate deletion moved AFTER successful save to prevent data loss if
  stream is cancelled mid-flight.
- TranscriptResultView: Added Regenerate button to each summary content pane
  toolbar (before Copy/Delete). Prompt menu shows dot indicator next to
  prompts that already have summaries. Extracted promptMenuItem() helper.

## Root Intent
Users could create duplicate tabs by generating the same prompt twice.
Also needed a way to regenerate any existing summary from its tab without
going through the "+" popover. Critical fix: pre-save duplicate deletion
caused data loss when a stream was cancelled before completing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@moona3k moona3k force-pushed the feature/prompt-library-multi-summary branch from 5ab4d6d to 0df3ff6 Compare April 4, 2026 21:40
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/MacParakeetCore/Models/Prompt.swift (1)

46-72: ⚠️ Potential issue | 🟠 Major

Retiring built-ins needs a cleanup migration.

Changing the built-in set here won't update existing databases: seeding still inserts built-ins by fresh identity, so users who already have the old defaults will keep those rows and also get these renamed ones. Please reconcile built-ins by a stable identifier/name mapping and explicitly remove retired defaults during migration or seeding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/MacParakeetCore/Models/Prompt.swift` around lines 46 - 72, The
built-in prompt change in builtInSummaryPrompts will not update existing DB rows
and will duplicate/leave retired prompts; modify the seeding/migration logic to
reconcile built-ins by a stable identifier (e.g., add and use a stable builtInID
or canonical name) instead of relying on generated identity, update existing
Prompt rows where Prompt.isBuiltIn == true and identifier matches to apply
content/name changes, and remove or mark as retired any old defaults that are no
longer in the built-in set; specifically update the code that seeds DB built-ins
(the seeding/migration routine that inserts Prompt rows) to perform
upsert-by-stable-id for Prompt entries returned by builtInSummaryPrompts and to
delete/flag retired prompts.
Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift (1)

204-249: ⚠️ Potential issue | 🟠 Major

Show edit validation errors inside the sheet.

Any failure surfaced through viewModel.errorMessage only renders in the parent view at Lines 32-36, which is obscured while this sheet is open. A duplicate/empty save currently looks like a no-op; mirror the error text inside editSheet or keep the edit draft/error state local to the sheet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift` around
lines 204 - 249, The sheet’s validation/errors are only shown in the parent view
(viewModel.errorMessage) and are hidden while editSheet(prompt:) is presented,
so users see a silent no-op on invalid saves; surface the error inside the sheet
by showing the error text under the editor and/or keep edit state local. Update
editSheet(prompt:) to display viewModel.errorMessage (or a new local `@State` var
editError that you set when update fails) beneath the TextEditor (near
editContent/editName), clear that error when the sheet opens (when editingPrompt
is assigned) and when Cancel is tapped, and ensure viewModel.updatePrompt call
propagates or sets the visible error so duplicate/empty saves produce visible
feedback to the user.
♻️ Duplicate comments (4)
Tests/MacParakeetTests/Database/DatabaseManagerTests.swift (1)

6-18: ⚠️ Potential issue | 🟠 Major

Missing v0.7-snippet-key-action in pre-seeded migration IDs.

prePromptLibraryMigrationIDs still omits v0.7-snippet-key-action, even though it runs before v0.7-prompts-and-summaries. This seeds an incorrect migration baseline for testPromptSummaryMigrationPreservesLegacySummaryColumn.

Suggested fix
     private let prePromptLibraryMigrationIDs = [
         "v0.1-dictations",
         "v0.1-transcriptions",
         "v0.2-custom-words",
         "v0.2-text-snippets",
         "v0.3-transcription-source-url",
         "v0.4-transcription-diarization-segments",
         "v0.4-transcription-llm-content",
         "v0.5-private-dictation",
         "v0.5-chat-conversations",
         "v0.5-drop-unused-fts",
         "v0.5-transcription-video-metadata",
+        "v0.7-snippet-key-action",
     ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/MacParakeetTests/Database/DatabaseManagerTests.swift` around lines 6 -
18, The pre-seeded migration list prePromptLibraryMigrationIDs is missing
"v0.7-snippet-key-action", which should be included before
"v0.7-prompts-and-summaries" so the test baseline for
testPromptSummaryMigrationPreservesLegacySummaryColumn is correct; update the
array prePromptLibraryMigrationIDs to insert "v0.7-snippet-key-action"
immediately before "v0.7-prompts-and-summaries" so migrations run in the
intended order.
Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift (2)

724-735: ⚠️ Potential issue | 🟠 Major

Attach the delete confirmation to an always-mounted parent.

The tab context menu can set pendingDeleteSummary while Transcript or Chat is selected, but the .alert only exists inside summaryContentPane. In those states the confirmation never appears, or it can surface later on an unrelated pane.

Also applies to: 864-879

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift` around
lines 724 - 735, The context menu in TranscriptResultView currently sets
summaryViewModel.pendingDeleteSummary while the delete confirmation .alert is
only declared inside summaryContentPane, so the alert never appears when
Transcript/Chat is selected; move the .alert that observes
summaryViewModel.pendingDeleteSummary out of summaryContentPane and attach it to
a parent view that is always mounted (e.g., the TranscriptResultView root
container) so the confirmation is shown regardless of which tab is active;
update references to pendingDeleteSummary and remove duplicate alert
declarations (the one at lines ~864-879) so there is a single alert bound to
summaryViewModel.pendingDeleteSummary.

814-824: ⚠️ Potential issue | 🟡 Minor

Give summary copy its own reset task.

This action still reuses copiedResetTask, which the transcript and chat copy paths also use. Copying one thing cancels another path’s timer and can leave a stale “Copied” state behind.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift` around
lines 814 - 824, The Button action for copying a summary reuses the shared
copiedResetTask, causing cancellation conflicts with other copy paths; create
and use a dedicated reset Task for summary copies (e.g., a new variable like
copiedSummaryResetTask) instead of copiedResetTask, update the Button action to
cancel/set that dedicated Task and leave copiedSummaryID management the same so
other copy flows (transcript/chat) no longer interfere with summary copy timers.
Sources/MacParakeetViewModels/SummaryViewModel.swift (1)

263-275: ⚠️ Potential issue | 🟠 Major

Make regenerate-in-place atomic.

This still deletes the previous summary before the replacement row and legacy mirror are durable, and the delete failure is swallowed. A failing save/updateSummary loses the last good summary, while a failed delete can leave duplicate prompt entries because the database does not enforce uniqueness for (transcriptionId, promptName).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/MacParakeetViewModels/SummaryViewModel.swift` around lines 263 - 275,
Make regenerate-in-place atomic by not deleting the old summary until the new
summary and legacy mirror are durably written: call summaryRepo.save(summary)
first and ensure it returns/commits (don't swallow errors with try?), then call
transcriptionRepo.updateSummary(id: targetTranscriptionID, summary:
summary.content) and invoke onLegacySummaryChanged only after success; only then
delete the existing summary (use summaryRepo.delete(id:)) and update the
in-memory summaries and call onDeletedSummary. Prefer wrapping
save/update/delete in a repository/DB transaction if summaryRepo supports it
(e.g., summaryRepo.transaction { save; update; delete }) and remove the silent
ignore of delete errors so failures are propagated/handled instead of leaving
duplicates; refer to summaries, summaryRepo.save, summaryRepo.delete,
transcriptionRepo.updateSummary, onLegacySummaryChanged, and onDeletedSummary
when implementing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift`:
- Around line 204-249: The sheet’s validation/errors are only shown in the
parent view (viewModel.errorMessage) and are hidden while editSheet(prompt:) is
presented, so users see a silent no-op on invalid saves; surface the error
inside the sheet by showing the error text under the editor and/or keep edit
state local. Update editSheet(prompt:) to display viewModel.errorMessage (or a
new local `@State` var editError that you set when update fails) beneath the
TextEditor (near editContent/editName), clear that error when the sheet opens
(when editingPrompt is assigned) and when Cancel is tapped, and ensure
viewModel.updatePrompt call propagates or sets the visible error so
duplicate/empty saves produce visible feedback to the user.

In `@Sources/MacParakeetCore/Models/Prompt.swift`:
- Around line 46-72: The built-in prompt change in builtInSummaryPrompts will
not update existing DB rows and will duplicate/leave retired prompts; modify the
seeding/migration logic to reconcile built-ins by a stable identifier (e.g., add
and use a stable builtInID or canonical name) instead of relying on generated
identity, update existing Prompt rows where Prompt.isBuiltIn == true and
identifier matches to apply content/name changes, and remove or mark as retired
any old defaults that are no longer in the built-in set; specifically update the
code that seeds DB built-ins (the seeding/migration routine that inserts Prompt
rows) to perform upsert-by-stable-id for Prompt entries returned by
builtInSummaryPrompts and to delete/flag retired prompts.

---

Duplicate comments:
In `@Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift`:
- Around line 724-735: The context menu in TranscriptResultView currently sets
summaryViewModel.pendingDeleteSummary while the delete confirmation .alert is
only declared inside summaryContentPane, so the alert never appears when
Transcript/Chat is selected; move the .alert that observes
summaryViewModel.pendingDeleteSummary out of summaryContentPane and attach it to
a parent view that is always mounted (e.g., the TranscriptResultView root
container) so the confirmation is shown regardless of which tab is active;
update references to pendingDeleteSummary and remove duplicate alert
declarations (the one at lines ~864-879) so there is a single alert bound to
summaryViewModel.pendingDeleteSummary.
- Around line 814-824: The Button action for copying a summary reuses the shared
copiedResetTask, causing cancellation conflicts with other copy paths; create
and use a dedicated reset Task for summary copies (e.g., a new variable like
copiedSummaryResetTask) instead of copiedResetTask, update the Button action to
cancel/set that dedicated Task and leave copiedSummaryID management the same so
other copy flows (transcript/chat) no longer interfere with summary copy timers.

In `@Sources/MacParakeetViewModels/SummaryViewModel.swift`:
- Around line 263-275: Make regenerate-in-place atomic by not deleting the old
summary until the new summary and legacy mirror are durably written: call
summaryRepo.save(summary) first and ensure it returns/commits (don't swallow
errors with try?), then call transcriptionRepo.updateSummary(id:
targetTranscriptionID, summary: summary.content) and invoke
onLegacySummaryChanged only after success; only then delete the existing summary
(use summaryRepo.delete(id:)) and update the in-memory summaries and call
onDeletedSummary. Prefer wrapping save/update/delete in a repository/DB
transaction if summaryRepo supports it (e.g., summaryRepo.transaction { save;
update; delete }) and remove the silent ignore of delete errors so failures are
propagated/handled instead of leaving duplicates; refer to summaries,
summaryRepo.save, summaryRepo.delete, transcriptionRepo.updateSummary,
onLegacySummaryChanged, and onDeletedSummary when implementing.

In `@Tests/MacParakeetTests/Database/DatabaseManagerTests.swift`:
- Around line 6-18: The pre-seeded migration list prePromptLibraryMigrationIDs
is missing "v0.7-snippet-key-action", which should be included before
"v0.7-prompts-and-summaries" so the test baseline for
testPromptSummaryMigrationPreservesLegacySummaryColumn is correct; update the
array prePromptLibraryMigrationIDs to insert "v0.7-snippet-key-action"
immediately before "v0.7-prompts-and-summaries" so migrations run in the
intended order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b343acba-cd6f-4b1f-8df7-ef551daad89f

📥 Commits

Reviewing files that changed from the base of the PR and between 5ab4d6d and 0df3ff6.

📒 Files selected for processing (18)
  • .gitignore
  • Sources/MacParakeet/AppDelegate.swift
  • Sources/MacParakeet/Views/Transcription/SummaryPromptsView.swift
  • Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift
  • Sources/MacParakeetCore/Database/DatabaseManager.swift
  • Sources/MacParakeetCore/Models/Prompt.swift
  • Sources/MacParakeetCore/Services/LLMService.swift
  • Sources/MacParakeetViewModels/PromptsViewModel.swift
  • Sources/MacParakeetViewModels/SummaryViewModel.swift
  • Sources/MacParakeetViewModels/TranscriptionViewModel.swift
  • Tests/MacParakeetTests/Database/DatabaseManagerTests.swift
  • Tests/MacParakeetTests/Database/PromptRepositoryTests.swift
  • Tests/MacParakeetTests/Database/SummaryRepositoryTests.swift
  • Tests/MacParakeetTests/Services/LLMServicePromptTests.swift
  • Tests/MacParakeetTests/Services/LLMServiceTests.swift
  • Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift
  • Tests/MacParakeetTests/ViewModels/SummaryViewModelTests.swift
  • Tests/MacParakeetTests/ViewModels/TranscriptionViewModelTests.swift
💤 Files with no reviewable changes (1)
  • Sources/MacParakeetCore/Database/DatabaseManager.swift
✅ Files skipped from review due to trivial changes (4)
  • .gitignore
  • Tests/MacParakeetTests/Services/LLMServicePromptTests.swift
  • Tests/MacParakeetTests/Database/SummaryRepositoryTests.swift
  • Tests/MacParakeetTests/Database/PromptRepositoryTests.swift
🚧 Files skipped from review as they are similar to previous changes (5)
  • Tests/MacParakeetTests/Services/LLMServiceTests.swift
  • Tests/MacParakeetTests/ViewModels/SummaryViewModelTests.swift
  • Sources/MacParakeetViewModels/PromptsViewModel.swift
  • Sources/MacParakeet/AppDelegate.swift
  • Sources/MacParakeetCore/Services/LLMService.swift

moona3k and others added 4 commits April 4, 2026 14:56
Move prompt definitions from hardcoded Swift to a bundled JSON file
(community-prompts.json) so contributors can add new prompts via PR
without touching Swift code. Rename "Built-In" to "Community" and
"Custom" to "My Prompts" in the UI. Add "Suggest a prompt" link
pointing to the JSON file on GitHub.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…utton, and streamlined popover

## What Changed
- Sources/MacParakeet/Views/Components/FlowLayout.swift: New reusable FlowLayout for
  wrapping horizontal chip layouts
- Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift:
  - Replaced cryptic "+" tab button with labeled "Summarize" button (sparkles icon,
    accent-tinted background capsule)
  - Replaced dropdown Menu prompt picker with inline selectable prompt chips
    (capsule style, selected state with accent highlight, dot indicator for existing summaries)
  - Surfaced "Manage Prompts" as a persistent gear icon button next to prompt chips
    (was buried 3 clicks deep inside dropdown)
  - Extra instructions field always visible (removed toggle gate — empty placeholder
    is self-documenting)
  - Reordered tabs: Transcript | [summaries...] | Chat | +Summarize (summaries now
    grouped next to transcript instead of separated by Chat)
  - Widened popover from 380px to 420px for breathing room
  - Added accessibility label on generate button
  - Removed dead `showExtraInstructions` state and `promptMenuItem` helper

## Root Intent
The summary generation popover had multiple UX friction points: the "+" button was
an unlabeled icon, prompt selection required a dropdown menu, "Manage Prompts" was
buried inside the dropdown, extra instructions required an extra click to reveal,
and summaries were separated from the transcript by the Chat tab. These changes
reduce clicks-to-action, improve discoverability, and create a more premium feel.

## Prompt That Would Produce This Diff
Polish the summary generation UX in TranscriptResultView with these changes:
1. Replace the bare "+" icon tab button with a labeled "Summarize" capsule button
   using sparkles icon and accent color tint
2. Replace the dropdown Menu prompt picker with inline selectable prompt chips using
   a FlowLayout (create a new FlowLayout component). Chips show selection state and
   a dot indicator when a summary already exists for that prompt
3. Surface "Manage Prompts" as a gear icon (slider.horizontal.3) button always
   visible next to the prompt chips
4. Always show the extra instructions TextField (remove the toggle/reveal pattern)
5. Reorder tabs so summaries appear between Transcript and Chat
6. Remove the dead promptMenuItem function and showExtraInstructions state

## Files Changed
- Sources/MacParakeet/Views/Components/FlowLayout.swift (+60, new)
- Sources/MacParakeet/Views/Transcription/TranscriptResultView.swift (+64, -61)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
moona3k and others added 19 commits April 4, 2026 17:50
Two SummaryViewModelTests used tight timing margins (700ms and 450ms)
that passed locally but failed on slower CI runners. Increased to 1500ms
and 1000ms respectively to provide adequate headroom.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename the multi-prompt result layer away from the old summary-specific terminology now that custom prompts can produce many result tabs.

This commit:
- renames the core result model, repository, and view model to PromptResult, PromptResultRepository, and PromptResultsViewModel
- updates app wiring, tab selection, and result rendering to use result-oriented naming throughout the feature
- replaces single-slot badge state with per-result unread tracking so multiple completed tabs can remain unread at the same time
- keeps compatibility boundaries intact by preserving the summaries table, the legacy transcription.summary mirror, and the stored prompt category raw value of "summary"
- refreshes repository, view-model, and transcription tests and removes the obsolete Summary* source and test files

Verified with:
- swift test --filter PromptResultsViewModelTests
- swift test --filter TranscriptionViewModelTests
- swift test --filter PromptResultRepositoryTests
Align the prompt-library architecture with the shipped multi-result model.

This commit:
- reframes the prompt library UI around built-in prompts instead of misleading community/runtime wording
- keeps community-prompts.json in sync with the shipped built-in prompt set and excludes it from the SwiftPM target as a repo artifact rather than a runtime resource
- renames the LLM result-generation API from summary-oriented names to prompt-result names while preserving summarize wrappers for compatibility
- renames prompt-result telemetry events away from summary-specific naming so analytics match the feature model
- adds regression coverage for prompt artifact drift and updates telemetry/view-model test doubles accordingly

Verified with:
- swift test --filter PromptRepositoryTests
- swift test --filter PromptResultsViewModelTests
- swift test --filter LLMServiceTests
- swift test --filter TelemetryServiceTests
- swift test
@moona3k moona3k changed the title Implement prompt library and multi-summary flow Add prompt library and multi-result architecture Apr 5, 2026
@moona3k moona3k changed the title Add prompt library and multi-result architecture Add prompt library with multiple LLM results per transcription Apr 5, 2026
moona3k and others added 8 commits April 4, 2026 20:13
## What Changed
- Replaced 7 generic prompts with 6 intentional, high-quality prompts:
  Summary, Action Items & Decisions, Chapter Breakdown, Study Guide,
  Blog Post, and What Stood Out (reflection/observation wildcard)
- Each prompt specifies exact output structure, conditional sections,
  and behavioral constraints that prevent filler
- Synced community-prompts.json artifact with new prompt definitions
- Updated all test references from old prompt names to new ones
- Made isAutoRun an explicit parameter on makeBuiltInPrompt instead
  of hardcoding against "General Summary" name match
- Added Merkaba watermark and polish to PromptLibraryView

## Root Intent
The original 7 prompts were generic and overlapping (e.g., "Bullet Points"
vs "General Summary" vs "Executive Brief" all produced similar output).
Researched Granola, Fireflies, tl;dv, and open-source prompt libraries
to identify what separates great built-in prompts from mediocre ones:
specificity, conditional sections, and behavioral constraints. The new
set covers distinct use cases with no overlap.

## Prompt That Would Produce This Diff
Replace the 7 generic built-in prompts in Prompt.builtInPrompts() with
6 high-quality prompts: Summary (auto-run default), Action Items &
Decisions, Chapter Breakdown, Study Guide, Blog Post, and What Stood
Out. Each prompt should specify exact output structure with markdown
headings, include behavioral constraints (omit sections when irrelevant,
don't fabricate consensus, don't pad), and adapt to content type
(monologue vs conversation). Update community-prompts.json to match.
Fix all tests that reference old prompt names or counts.

## Files Changed
- Sources/MacParakeet/Views/Transcription/PromptLibraryView.swift (~31)
- Sources/MacParakeetCore/Resources/community-prompts.json (~38)
- Tests/MacParakeetTests/Database/PromptRepositoryTests.swift (~52)
- Tests/MacParakeetTests/ViewModels/PromptResultsViewModelTests.swift (~4)
- Tests/MacParakeetTests/ViewModels/PromptsViewModelTests.swift (~12)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@moona3k moona3k merged commit 2431d4a into main Apr 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Summary instructions with preset templates

1 participant