Add durable recording notes with live transcription - #95
Conversation
- Avoid leaking dictionary terms into diarized Whisper segments - Omit vocabulary bias for Parakeet offline fallback and enhancement paths - Add coverage for diarization options and streaming finalize behavior
Record Granola as a reference-only constraint, keep post-foundation automation deferred, and preserve the dirty main worktree through an isolated feature worktree.
Define validated dictate, voice-note, and meeting lifecycles in PindropCore. Add seven flat UUID-linked SwiftData records, a lightweight V12-to-V13 migration, complete store health checks, and migration/state-machine coverage. Keep runtime capture wiring deferred to later phases.
Persist Quick Capture and speak-to-append history, notes, CaptureSession revisions, microphone sources, and human-note references. Preserve first-source IDs, serialize editor autosaves, enforce exact-handle cancellation, and teach app repair/test-mode paths that V13 is current.
Retain microphone and system audio as separate deterministic PCM artifacts with source-owned ledger chunks and failures. Keep mixed final ASR compatible, survive either source failing, serialize Core Audio teardown, and ensure final transcript inputs match retained sources.
Rotate microphone and system audio into five-minute durable chunks, transcribe bounded files sequentially, checkpoint progress, derive diarization best-effort, reserve stable History IDs, and recover interrupted capture/finalization work across relaunch and orderly shutdown.
Snapshot immutable provider, model, diarization, and note prompt selections for each capture stage. Reuse those snapshots during recovery and fail closed when a persisted selection is no longer available. Migrate legacy V13 prompt references into V14 prompt companions or tombstones before runtime. Expose the effective next-session assignments as a read-only Models summary. Verified: just test-shared; just shared-build-ios; just build; just test; live macOS accessibility-tree inspection.
Run artifact-only streaming for quick capture and note append, and persist only stable cumulative text with microphone, provider, and parent provenance. Make checkpoint and final revision retries idempotent. Serialize engine teardown, scope generic and artifact ownership, and prevent stale output from mutating a successor session. Expose deterministic recovery candidates while deferring user-facing delivery until capture intent is persisted. Verified: just test-shared; just shared-build-ios; just build; just test; focused race and persistence tests.
Create protected human anchor notes before meeting finalization and generate derived notes only after History and final transcript checkpoints are durable. Freeze logical provider, model, and prompt assignments while late-binding provider configuration and credentials. Keep transient generation failures retryable in finalizing sessions. Persist canonical transcript provenance plus a separate immutable anchor-content snapshot. Render both in a read-only Sources panel, reject citation spoofing, and fail closed on malformed provenance. Verified: just test-shared; just shared-build-ios; just build; just test; just l10n-lint; focused citation UI test; live panel inspection.
Replace the Home-first shell with Capture, Workspace, and Tools navigation. Make Dictate, Voice Note, and Meeting first-class routes while preserving all five supporting destinations and Command-1 through Command-8 shortcuts. Use controller-owned route state, one-shot Library requests, localized labels, and a single observable capture-admission state. Capture starts now reject duplicate work, update meeting-sheet availability live, and cannot commit after termination begins. Verified with just build, just test, localization lint, focused main-shell UI tests, LTR and RTL runtime inspection, and independent correctness, interface, localization, and Swift concurrency reviews.
Delete canonical source audio after explicit meeting cancellation, retain durable cleanup retry state, and keep cancellation exact-handle scoped across delayed tasks and successors. Require final-ASR coverage for every durable chunk before history, note, or session completion. Successful retries recover matching persisted failures while permanent source gaps remain timed placeholders. Add regressions for full-session artifact removal, symlink safety, idempotent cancellation, 90-minute chunk coverage, incomplete completion, and retry recovery.
Detect same-version model-hash drift before SwiftData opens the store. Back up the SQLite store, WAL, and SHM, then refresh exact version metadata and model cache only after structural parity checks. Restore missing Core Data primary-key registrations in the same repair transaction and run the shared idempotent V14 prompt-snapshot backfill on every container open. The production failure was an intermediate V14 hash set reported as an unknown staged-migration version. Regression tests cover hash drift, semantic prompt backfill, repaired-entity writes, reopen, and backup preservation.
Use the existing localized View all string for the Dictate activity link instead of an uncatalogued English-only label.
Reconcile all fourteen decisions against the implemented branch. Record ten implemented outcomes, the active independence constraint, isolated-worktree preservation, and the two intentional deferrals. Document the three capture workflows and current main-window navigation. Dictate remains an explicit capture-domain migration follow-up; calendar automation, cross-meeting chat, camera consent, and voice-note recovery delivery remain out of scope.
…editor - Dictate: restore the hero-first dashboard with a top-right Start dictating action; drop the title block and Change shortcut pill - Voice Note / Meeting: editorial heroes with weekly counts, start actions, and Recent sections backed by real notes and meetings - Note editor: document-size window; markdown heading markers render dimmed in the left margin instead of inline - Extract NoteEditorWindowControllerRegistry.presentEditor for reuse - Add capture-pillar strings across all locales and regenerate catalogs - Update main-shell UI test to open Settings via the sidebar
# Conflicts: # Packages/PindropShared/Sources/PindropSpeech/TranscriptionService.swift # Packages/PindropShared/Tests/PindropSpeechTests/TranscriptionServiceTests.swift # Pindrop/AppCoordinator.swift # PindropTests/OutputManagerTests.swift # PindropTests/StreamingSessionControllerTests.swift
…em IA) Phase 1 of the notes-that-record plan (plans/notes-that-record-plan.md): P0: CaptureEnhancedPanelModel, NoteViewStateModel, and CaptureIntentModel land as schema V15 (lightweight, append-only). Store repair learns a per-version table map so V15 stores repair upward, never down to V14. WP0: typography roles absorb HomeLayoutMetrics font values, focus rings move into the shared components, IconSlot fixes icon lanes, and the duplicated diarization banner becomes InlineNotice + StageProgressRow. WP1: sidebar becomes Capture (Dictate, Notes), Workspace (Library, Stats), Tools (Dictionary, Models). Legacy voice-note and meeting raw values resolve to Notes. NotesRoute adds an in-window note destination with a stub page, and capture entry points collapse into one NoteCaptureRequest. Docs: approved plan, agent briefing, and the extracted design spec. Verified: just build, just test (870 tests, 79 suites), focused suites per package. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
Phase 2 of the notes-that-record plan: P1: CaptureSessionMode gains .note; NoteCaptureHandle carries an optional system-audio source and replaces MeetingCaptureHandle. OwnedNoteCapture supports one- or two-source sessions; a mic-only capture creates no system-audio row. MeetingCaptureSpoolPlan.systemAudioSourceID is optional through the media layer. noteCaptureRecoveryCandidates() is the single recovery scanner with thin legacy shims. WP2: one Notes page replaces the Voice Note pillar, the Meeting pillar, and the old workspace list: humanized serif header meta, split New note button, date groups, capture-aware rows with Enhanced badges and a live-row treatment, and a shared listKeyboardSelection modifier. The status bar gains New note items wired through the main window's capture seam. MeetingCaptureOptionsSheet is gone; speaker count moves to the note page. Verified: just build; full Unit plan 888 tests in 80 suites; focused CaptureSessionStoreTests (65) and LongMeetingReliabilityTests (9, durability gate). Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
…capture Phase 3 of the notes-that-record plan: P2: note-mode sessions cannot start without a validated CaptureIntent; the intent row lands in the same transaction as the session and sources. newNote intents bind to their note when it exists, recovery snapshots carry the intent, and legacy sessions honestly report nil. Origins thread through the main-window, menu-bar, and hotkey entry points. P3: the source-separated backend forwards microphone buffers to the streaming pump (system audio never reaches it), mic-only spool plans skip the system child entirely, and the streaming controller's artifact API is keyed on NoteCaptureHandle with a 2-hour live-transcription bound that degrades to spool-only. This is the recorder-level groundwork that lets P4 route every note capture through the durable path. Verified: just build; full Unit plan 895 tests in 80 suites; package suites CaptureIntentPersistenceTests (12) + CaptureSessionStoreTests (65); AudioRecorderTests (52), StreamingSessionControllerTests (18), LongMeetingReliabilityTests green. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
P4 step 1 of 2 (bisectable). NoteCaptureController owns the full note-capture lifecycle moved out of AppCoordinator: durable session start with intent, mic-only or dual-source spool, live artifact transcription, finalization (batch ASR with live-text fallback, diarization only for system-audio captures), record persistence and linking, note generation, failure recording, and termination checkpointing. NoteCaptureState is the observable the UI will bind. AppCoordinator conforms to CaptureArbiter (claims, model activation, progress, navigation) and keeps thin forwarders; it shrinks from 9,434 to 8,114 lines. VoiceNoteCaptureHandle is gone; the store speaks NoteCaptureHandle everywhere. Every main-window and menu-bar note capture now routes through the durable path, ending the 10-minute voice-note cap on those flows. The quick-capture hotkey still uses the legacy path until step 2 deletes the forwarders. Verified: just build; full Unit plan 911 tests in 81 suites (3 runs); NoteCaptureControllerTests (16); coordinator, streaming, recorder, and LongMeetingReliability suites green; CaptureSessionStoreTests 65/65. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
P4 step 2 of 2. The quick-capture hotkey now runs through the unified durable controller (origin-aware: no window yanking, note opens on finish, silent captures leave no note behind). The transitional forwarders, the in-memory quick-capture pipeline, and its stop routes are deleted; recovery orchestration moves onto the controller with the coordinator keeping only the scheduling shell. AppCoordinator drops to 7,370 lines (from 9,434 at the start of P4). Note-append keeps the legacy voice-note path deliberately; it dies with the editor-window retirement. Mic-only manual captures regain voice-isolation preprocessing (they are the successors of quick capture); mixed system audio stays raw so other speakers are not suppressed. Verified: just build; full Unit plan 917 tests in 81 suites; focused controller/coordinator/recorder/streaming/reliability suites green; CaptureSessionStoreTests 67/67. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
Phase 5 of the notes-that-record plan: P5: CaptureEnhancedPanelModel becomes a working artifact store. Panels are per-template, regenerable, and supersede atomically; every generation allocates the next assignment attempt and freezes provider + prompt snapshots in the same transaction. NoteEnhancementService runs the generation flow on the existing MeetingNoteDerivation evidence envelope, and NoteCaptureController auto-generates a panel for every recorded note on finish. Generation failure records on state and never fails the capture. New captures write panels only; legacy generated notes surface as read-only panels. Save-as-note exists as an explicit operation. P6: noteCaptureViews(noteID:) resolves the three views (typed notes, panels, transcript) for every note shape: legacy meeting, legacy voice note, plain typed, and V15. Transcript assembly shares the exact diarized spans the citations use, attributes speakers via participant profiles with honest unattributed fallbacks, falls back to live checkpoints during capture, and respects independent transcript deletion. View selection persists on NoteViewStateModel. Verified: just build; full Unit plan 933 tests in 82 suites; package suites 327 (PindropData) including the 33 new panel/view tests. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
WP3 of the notes-that-record plan. The stub becomes the real page: header rail with a split Record control, editable serif title on the shared gutter axis, the three-view SegmentedViewToggle with persisted selection, and a 720pt canvas. The proven MarkdownEditor and NoteEditorPersistenceController stack moves in-window verbatim; the editor is never disabled while recording. The markdown renderer now draws real bullet and ordered markers with tight in-list rhythm, serif heading hashes right-aligned in the gutter, blockquote bars, inline code chips, and fenced code cards, all while raw markdown stays editable. Capture states bind to NoteCaptureState: capture strip with live one-liner, StageProgressRow finalization with honest copy, enhancement failure notice with Try again, and an enhanced-ready dot that never auto-switches views. Nothing presents the separate editor window any more. Verified: just build; full Unit plan 992 tests in 86 suites; new NotePagePresentation (33) and MarkdownBlockGrammar (26) suites. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
WP4-WP6 of the notes-that-record plan: WP4: CaptureBar in page and global densities on a pure presentation type; the global bar pins to every destination while a capture runs elsewhere; the sidebar status card becomes a navigating button with a finalizing phase; the Notes item shows a record dot while live; list REC rows tick in their own clock; Cancel joins the note-page overflow behind a confirmation. WP5: transcript turns render as speaker bubbles sharing the participant color rule with Library, with search + match counts, click-to-seek, a playback bar, and the live transcript sheet (36pt collapsed row, 40%/70% snap points, committed vs tentative text, Jump to live, Esc collapses). WP6: the Enhanced view renders sections with citation resolution feeding both inline chips and the Sources disclosure (the disclosure is the shipped surface: generation sanitizes inline markers away by design), a TemplateMenuButton on a shared MenuButtonChrome with instant switching between generated panels and in-place regeneration, and honest read-only treatment for legacy meeting notes. Verified: just build; full Unit plan 1090 tests in 93 suites; package suite 914 tests in 67 suites. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
WP7 + WP9 close the notes-that-record plan: WP7: the Start dictating CTA sits on the kicker baseline with the shortcut inside the button and swaps in place to a recording control (dot, mono elapsed, level bars, Stop) with no layout jump. Stats drop to three tiles with Sessions in the chart header; the slogan empty state becomes plain copy; recent rows gain keyboard selection; the dead pillar presentation helpers leave with their tests. WP9: all 111 outstanding keys translated across the 30 non-English locales with placeholder validation; l10n-sync run; l10n-lint clean. Three latent slug-collision bugs fixed (Delete note menu item, edited-time footer, speaker Current label). VoiceOver labels for rows, dots, and chips; reduce-motion audited; RTL mirroring with LTR-pinned key sequences; note page focus starts at the title. AppUITestSurface.notePage ships a real V15 fixture with a CI-shaped UI test, and two never-registered test suites now run (+90 tests, one unsound timing test disabled with rationale). Verified: just build; full Unit plan 1188 tests in 98 suites; just l10n-lint clean; UI test plan compiles. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
Cleanup pass against the Paper design after first hands-on use: One CapturePrimaryButton now renders both the Dictate CTA and the Notes split New note button, which also fixes the split button stretching to the header height (its divider filled an unbounded frame). Secondary chrome pads 7/13 to match the mock. Paper fidelity: the Dictate page gets its 56pt frame and the 18pt gap below the kicker row; the Notes header keeps 24 above the list; the note page's header rail and dock span the pane while title, meta, and canvas keep the 720pt measure; title and meta form one 14pt block; the page capture bar's elapsed uses mono 13. Auto-naming is an AI job now: every recorded note that is still untitled after capture gets a title from the .noteMetadata assignment, with the derived-title heuristic as fallback, a re-fetch guard so a person's rename always wins, and no delay to capture completion. Quick-capture titling folds into the same job. Verified: just build; full Unit plan 1193 tests in 98 suites, including 5 new auto-naming tests. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
FAB system (search resting, hover fan, morphing pills), merged Enhanced chip dropdown, source peek replacing citation chrome, ask-this-note dock, solo-transcript pause breaks, speakers popover, title hover affordance. Values extracted from Paper boards 52, 55, 56, 65, 70, 71 and crops 66-69. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
…ords fix UI: the three note views are individual chips; the selected Enhanced chip carries the merged dropdown (Enhanced notes header with regenerate, glyphed template rows, All templates, New template), replacing the separate template button. Citation chips and the Sources disclosure give way to source peek: hover a bullet for the magnifier, click for the quoted span with timestamp, speaker, and Show in transcript. The speakers chip opens a popover with profile-backed renames (unmatched voices say honestly why they cannot be renamed yet). The title shows a pencil on hover. Backend: the dormant VoiceActivityDetector now segments mic-only captures into paragraph blocks at 1.2s pauses, persisted as single-speaker diarized segments so every existing validator applies; solo transcripts render as time-coded blocks with a mono gutter. VAD and streaming models are required first-run downloads with a launch catch-up for existing installs. The duplicated-last-words bug is fixed at its root: the refinement coordinator never notified the sink when a verbatim tentative tail was promoted to committed, and the note transcript concatenates the two channels. Delivery now tracks the split, proven by failing-first tests. Verified: just build; full Unit plan 1217 tests in 98 suites; package suite 941; LongMeetingReliabilityTests green. Claude-Session: https://claude.ai/code/session_01CLPPXoX8t4asUdmdzooQjc
The system channel now carries per-speaker labels instead of one "Call audio" header. The diarizer gets its own stream and its own detached consumer, fed every system buffer whichever channel owns the streaming engine, so its speaker cache and its frame clock both stay coherent while the microphone holds the engine. Slots are numbered 1-based in first-finalized-speech order past a 0.35 s noise gate, never the model's own track numbers. A speaker change cuts a paragraph only while the system channel owns the engine, only past the hysteresis floor, and only once per window: changes inside it coalesce, last one wins. The late boundary splits the open span retroactively against the commit stamps, so text decoded before the switch stays with the speaker who said it. The cut is recorded as an offset rather than written into the committed text, because the durable checkpoint takes every write as an extension of the previous one and one inserted character would end persistence for the capture. The capture degrades to channel labels for its whole life when the bundle is missing or the bounded load fails, and a runtime kill switch drops live labels when partial arrival latency stays over budget for three windows.
A slot that has said three seconds is embedded from the diarizer's ring and matched against the participant profiles, and a match names it in place: the whole slot is repointed, so a reader never sees a name start partway down a transcript. The header keeps a "was Speaker 2" affix for the rest of the capture, derived from promotedAt rather than delivered once. LiveSpeakerEmbedder is its own actor with its own OfflineDiarizerManager on its own serial executor. FluidSpeakerDiarizer is @mainactor and would block the main actor for hundreds of milliseconds per clip, which is visible orb jank, so the live path never reaches for it. The embedder loads at the first promotion attempt and is released at step 6 of the stop sequence, before the offline pass starts. The live match thresholds are new constants beside the offline pair, and higher. The offline numbers were tuned against whole-recording embeddings; a live label comes from a six second slice of conference audio, and a wrong name shown for forty minutes is the outcome this phase most needs to avoid. Both numbers are placeholders that the gated calibration harness replaces with a measurement. Everything else degrades to "Speaker 2": a missing offline model, an embedding space mismatch, a budget overrun, or a match inside the live margin. Tiers only go up, a named slot is never re-checked, and the live path records no training evidence.
The note page reuses DiarizationSetupIssueBanner for the two live-speaker faults a reader can act on: the model is not downloaded, or its bounded load failed. Its download covers both bundles, because a real name needs the offline embedder as well, and a fetch that took only the streaming model would leave every slot at "Speaker 2" with nothing left to press. The settings row is the switch the whole feature hangs off. It gates engine startup and nothing else: diarizationFeatureEnabled still owns the finalize stage, so turning live names off never strips the speakers out of a finished note. Two chips, both checked against the collapsed sheet row. Neither states a fact the app cannot observe: the four-voice line names what the feature covers and claims no headcount, and the paused line promises a second pass only when finalize is actually going to run one. The reconciliation line reads from the flag inside the diarization payload, so it survives quit and relaunch and shows on every open until the reader dismisses it. A long meeting finalizes minutes after stop, usually into an empty room, and a transient line would be delivered almost only to a reader who was already watching. Dismissal clears the one key and keeps the rest of the payload: the mic-only ranges are still what makes the recorder "You".
Eight new strings for Phase 2 live speaker names: the two setup banner messages, the paused and capacity chips on the live sheet, the sentence the chip appends when the offline pass is scheduled, the note page line about the second check, and the Settings row title and description. All 30 non-English locales are translated, not left in English. Each locale reuses the word it already uses for "speaker" and for the finished note, so the live names read like the rest of the app instead of like a bolted-on feature. LocalizationKeyResolutionTests now covers the eight strings, so a locale that misses one fails the suite instead of hiding behind the English fallback that `localized` returns.
FluidAudio materializes the streaming Sortformer bundle under Repo.sortformer.folderName, which is "sortformer", not under the Hugging Face repo name. Readiness that looked for the repo name stayed false after a good download, so the model reported itself incomplete and live speaker labels never turned on.
Thirteen new strings for the Phase 3 meeting entry points: the two menu bar rows, the call notification title, body, and decline action, the four Meetings settings rows with their two descriptions, and the four parts of the one-time ask. All 30 non-English locales are translated, not left in English. Four rows of the section 5.7 table needed no new key. "New note" and "Record" were already in the tree, and so were "Meetings" and "Not now", which the table did not mark. "New note with system audio" stays, because NotesPresentation.NoteCreationOption still uses it. Each locale reuses the words it already uses for a call, a meeting note, the system audio, and System Settings, so the meeting rows read like the rest of the app. LocalizationKeyResolutionTests now pins all of them in English and asks every locale for a translation. "Meetings" is left out of the translation check on purpose: German keeps the English word, and an equality check would read that correct translation as a miss.
…eset The note page header already carried the template control: the Enhanced chip opens a menu that lists every preset, checks the one that wrote the panel on screen, and regenerates through the existing onGenerateEnhancedPanel path. It said nothing when no template wrote the panel, so the menu now reads "No template. The note is written as plain notes." above the list. No second control was built. The record button dropdown gains a Template submenu, built from the same preset rows in the same order, which records the choice before the start. NoteCaptureRequest carries the identifier into captureIntentRequest, so the durable intent holds it and a capture recovered after a crash is written with the template the person picked. NoteCaptureRequest.meetingNote now carries BuiltInPresets.meetingNotes.identifier. All three meeting entry points read that one preset: "New meeting note", "Record this call", and the notification "Record" action. "New note" stays template-neutral. StatusBarControllerTests.newMeetingNoteRequestsBothSourcesAndNoTemplate is renamed to newMeetingNoteRequestsBothSourcesAndTheMeetingTemplate. Its meaning changes in this commit: Phase 3 kept the meeting note template-neutral on purpose until the picker existed, so the template and the control that explains it ship together.
Startup recovery read only the system-audio subset of the recovery candidates, so an interrupted microphone-only capture stayed interrupted forever and its committed words landed nowhere a reader looks. Recovery now reads every interrupted note capture and resolves a destination first: the anchor the capture bound wins, then the note the durable intent names, then a fresh note. A destination that was deleted costs a title, never the transcript. Nothing in the recovery path raises a window. The shell callbacks that open a note belong to a live capture; a recovered one waits in the library. The Recovered state is derived, not stored. Phase 4 adds no persistence: a note is recovered when a capture session bound to it has a failure record that recovery stamped, which is one fetch per library page inside the existing row-facts pass and one fetch for a note page. Renames recoveryLeavesMicrophoneOnlyCapturesForTheirDeliveryStep to recoveryDeliversMicrophoneOnlyCaptures. That test pinned the gap this commit closes, so it now asserts the delivery it was waiting for.
Cleanup item 7 of section 6.4. Round B replaced the numeric chips with EnhancedSourcePeekRow, so no view draws a chip. Section 6.4 says no view reads EnhancedNoteBlock.citations. peekTarget does, and that read is what makes a line peekable at all. So the block keeps the only thing the read needs, the citation identifiers that resolved, and loses the chip type with its undrawn label, segmentID, and startOffset. The make() citations parameter and the panelCitations load stay: they build the source rows the peek shows. The chip-field assertions in EnhancedViewPresentationTests go with the type. Each one is restated on the identifiers, so the resolution rules stay covered.
Cleanup item 8 of section 6.4. canPause was hardcoded false and nothing branched on it, so it was a false affordance in a state model that Phase 3 and Phase 4 both extend. Decision 52 of the design document now holds the rationale it carried: v1 has no pause because AudioRecorder cannot pause a durable spool and CaptureSession.isValid encodes state in revision parity, so a paused lifecycle state would mean rewriting the validator. Its single assertion in NoteCaptureControllerTests goes with it.
Ten new strings for the Phase 4 finalization surfaces: the "Matching names" stage, the "Skipped" word a stage that does not apply wears, the stall line and its action, the transcribing failure row, the line over the settling transcript, the recovered note line and its library chip, and the two template picker strings P4.2 started calling. All 30 non-English locales are translated, not left in English. The four shipped stage names are reused, not renamed. "Sealing audio", "Transcribing", "Identifying speakers", and "Writing note" already exist in every locale and mirror MediaTranscriptionStage, so renaming them would churn 31 files and split two surfaces that agree today. "Template" collides with the shipped "Template: %@" chip on the slug the importer derives, so it takes the digest form the importer itself would have produced. Each locale reuses the word it already uses for a template, a transcript, and a recording. LocalizationKeyResolutionTests pins the new strings in English and asks every locale for a translation, which is the only place a missing one is visible: localized falls back to the English source on screen.
Three reviews of P4.1 to P4.8 against section 6 of the design. Fixed: - The checklist ticked off both speaker rows for work that never ran. The system channel alone does not make finalize diarize: it also needs the setting on and the model on disk. `appliesToThisCapture` now reads `isOfflineSpeakerPassScheduled`, which is the predicate finalize applies. - The checklist walked backwards once per sealed chunk. `.diarizing` was reported before every chunk call and `.transcribing` after it, so a long meeting retracted a checkmark per chunk and attributed an ASR failure to the speaker row. It is reported once, after the loop. - The failed row and its 31-locale sentence could not render: the dock unmounts for a failed capture. `showsCaptureStrip` keeps it up for a capture that failed while finalizing, and the page draws that checklist. - Two rows read `Writing note`. `.enhancing` takes the shipped `Enhancing…`, so no stage name is renamed and no locale file changes (decision 50). - The record dropdown's neutral row promised plain notes, which the pipeline cannot produce: nil falls back to the settings template. It reads `Default`, a shipped string, and says what it does. - The header menu's empty-template line was unreachable. It keys on whether any row is checked, which is the state a panel written with no template reaches through the generator's own default identifier. - VoiceOver read the checklist as six bare names. The row label carries the status word and the stall action title. - The settling sentence was drawn twice at once. It keeps its host over the turns; the footer says what the app is doing with the recording. - The stall action could open an empty sheet when the capture heard nothing. - `completedStages(before:)` lost both call sites in P4.1. It goes, and so do its four assertions: they pinned copy no surface draws. - `aRecoveredCaptureWithNoBoundNoteCreatesOne` asserted a tautology. It now names both titles the auto-naming path produces. Rejected: rewriting the bodies of the three landed deletion commits (each is already its own commit, as section 8.3 asks) and regenerating `L10nKeys.swift` (`just l10n-sync` is idempotent on the committed tree). Renamed `everyStageHasAnHonestNameAndAPredecessor` to `everyStageHasAnHonestName`, since the predecessor line is gone.
The 10 s bound tripped in the gated integration run whenever a Nemotron load ran at the same time as the Sortformer load, which is what a cold first capture looks like, and the reader would have been told the model could not be loaded. Measured: 3.1 s for a warm load alone, over 10 s with a concurrent Nemotron load, 63 s only when downloading. 30 s absorbs the contention and still refuses a download. The load runs concurrently with capture start, so a slow load only delays labels, never audio. Evidence: LiveDiarizationEngineTests 11/11, just build, and the LiveDiarizationIntegrationTests plus LiveDiarizationLatencyIntegrationTests pair passing together (13.8 s and 244.8 s) after failing at 10 s.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 441dc2732b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| recordID = try captureSessionStore.reserveMeetingTranscriptionRecordID(handle) | ||
| } | ||
| do { | ||
| let record = try historyStore.save( |
There was a problem hiding this comment.
Attach recorded-note audio to the history record
For every normally finalized capture, this save call omits managedMediaPath (the live-transcript fallback does too), and no subsequent updateManagedMediaPath exists for note captures. NotePageView.refreshPlayableAudio only exposes playback when that field resolves to an existing file, so every production recorded note lacks the Play recording control even though its source chunks were retained. Materialize and attach a playable capture asset, or teach the view to resolve the capture ledger, before completing the session.
AGENTS.md reference: AGENTS.md:L166-L167
Useful? React with 👍 / 👎.
| askTask = Task { @MainActor in | ||
| defer { askMessages = noteChatService.conversation(noteID: noteID) } | ||
| do { | ||
| _ = try await noteChatService.ask(noteID: noteID, question: trimmed) |
There was a problem hiding this comment.
Flush pending drafts before AI reads the note
When someone edits the note and immediately presses Ask—or regenerates a panel—the latest snapshot can still be waiting in the editor's 500 ms debounce while this call dispatches by note ID. Both NoteChatService and NoteEnhancementService then reread the durable SwiftData row, so the prompt can omit visible edits and a generated panel can persist a stale humanAnchorContentSnapshot. Flush the pending draft or pass the live snapshot before dispatching either AI action.
Useful? React with 👍 / 👎.
| let previous = reportedCall | ||
| reportedCall = call | ||
|
|
||
| guard let call else { |
There was a problem hiding this comment.
Clear the previous call when switching detected apps
When two catalog apps overlap and the currently reported app ends while the other remains active, the monitor publishes call B directly after call A without an intermediate nil. This assignment therefore skips the branch that withdraws A's notification and clears its declined bundle identifier; the old alert remains, and choosing “Not now” for A can suppress future A calls indefinitely. Treat a bundle-identifier change as the end of the previous call before processing the replacement.
Useful? React with 👍 / 👎.
Add recording notes with durable audio, live transcripts, speaker attribution, generated notes, and transcript citations in Ask.
PindropSharedwith macOS and iOS build targets.Validation:
just buildpasses.just testpasses with 1,451 tests in 111 suites.just l10n-lintandgit diff --checkpass.