Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,9 +576,8 @@ verbatim transcript, all inside the same request, and it rides **every** request
**The switch is applied to the response, not the request** (since 2026-09-11). The response
carries both `text` (the verbatim transcript, "never altered by the LLM") and `llm_response`
(the rewrite), so `AssemblyAITranscriber.transcript(from:)` simply picks: with **enhanced
transcripts** on (`EnhancedTranscriptsStore.pastesRewrite`, on by default and off while "speak all
punctuation" is on, read per request via the transcriber's injected `enhancedTranscripts` closure)
it prefers the rewrite and falls back to
transcripts** on (`EnhancedTranscriptsStore`, on by default, read per request via the
transcriber's injected `enhancedTranscripts` closure) it prefers the rewrite and falls back to
`text` when `llm_response` is null or blank — the rewrite is best-effort (5 s server-side
budget), so a rewrite failure (`llm_error`) is a logged degradation, never a user-facing error.
With the setting off it returns `text`, and the rewrite that came back is discarded unread.
Expand Down Expand Up @@ -956,16 +955,7 @@ Engine-side stores, all `UserDefaults`-backed value types with the same shape:
pairs; re-read per transcript via the session's `textShortcutsProvider` and applied locally by
`TextShortcutExpander` between the response and the paste — never on the wire. Like
`StyleProfileStore` it has a setter, because the value is encoded; the Settings window's Text
Shortcuts pane writes through it),
**`SpokenPunctuationStore`** (`BlurtSpokenPunctuation`, off by default — the "speak all
punctuation" mode, re-read per transcript via the session's `spokenPunctuationProvider`. While
on, `SpokenPunctuationFormatter` strips every mark the service inserted and turns the spoken
ones — "comma", "question mark", "new paragraph" — back into symbols, locally, before
`TextShortcutExpander` runs, so a shortcut's saved text keeps its own punctuation. It formats the
**verbatim** transcript, never the rewrite, since the rewrite is told to fix punctuation and may
already have turned a spoken "comma" into a mark the strip would remove: so it overrides
enhanced transcripts and the active style, through the one rule both read,
`EnhancedTranscriptsStore.pastesRewrite`. The request is unchanged).
Shortcuts pane writes through it).
- **`DefaultsKey`** (`Config/DefaultsKey.swift`) defines every key those stores write, one case each,
and each store's `defaultsKey` reads its case from there rather than spelling a string literal.
**`PersistedSettings.allDefaultsKeys`** is therefore just `DefaultsKey.allCases`, and
Expand Down
40 changes: 3 additions & 37 deletions App/Blurt/Blurt/Wizard/DeveloperSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import BlurtEngine
import OSLog
import SwiftUI

// The Advanced pane's three standalone sections: the update check, the
// developer-mode switch and the start-over button. All are Settings-only (none
// gates setup, so none is a wizard step), and they live here rather than in
// The Advanced pane's two standalone sections: the developer-mode switch and
// the start-over button. Both are Settings-only (neither gates setup, so
// neither is a wizard step), and they live here rather than in
// `SettingsWindowRoot` because that file is at the repo's file-length limit.

/// The Developer section of the Settings window: an opt-in switch for developer
Expand Down Expand Up @@ -194,37 +194,3 @@ struct ResetSection: View {
NSApp.terminate(nil)
}
}

/// The Updates section of the Settings window: the running version and a
/// "Check for Updates" button that runs the check and reports the result in a
/// modal (see `UpdateCheckModel`). The same check is reachable from the
/// "Check for Updates…" app-menu command and the menu-bar item; all three share
/// the one `UpdateCheckModel` owned by `AppDelegate`, so a check from any place
/// runs through the same controller.
struct UpdateSection: View {
let model: UpdateCheckModel

var body: some View {
Section {
// "Blurt 0.1.31" — the label is the engine's (shared with the result
// alerts, so the two can't name the version differently).
SettingRow(title: model.versionLabel, systemImage: "arrow.triangle.2.circlepath") {
HStack(spacing: 8) {
// A user-initiated check that can stall on a slow connection needs
// visible progress, or the button reads as dead until the result
// alert lands. Show a spinner and disable the button while in flight
// (the model already ignores a second check) — the native equivalent
// of Sparkle's "Checking for updates…".
if model.isChecking {
ProgressView().controlSize(.small)
}
Button("Check for Updates") { model.checkForUpdates() }
.disabled(model.isChecking)
.accessibilityIdentifier(UITestIdentifiers.updateCheck)
}
}
} header: {
Text("Updates")
}
}
}
80 changes: 46 additions & 34 deletions App/Blurt/Blurt/Wizard/SettingsWindowRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,46 +106,30 @@ private struct AdvancedSettingsTab: View {
/// switch. Every dictation request asks AssemblyAI's dictation API for its
/// server-side cleanup rewrite, so the response always holds both versions;
/// while this is on (the default) the polished one is pasted, and turned off
/// the verbatim transcript is pasted exactly as spoken. "Speak all punctuation"
/// (on the Text Shortcuts pane) formats the verbatim transcript itself, so it
/// overrides this switch, which is disabled while it is on
/// (`EnhancedTranscriptsStore.pastesRewrite`). The transcriber reads the same
/// default this toggle writes at every request, so a change applies to the next
/// dictation. Settings-only — not a wizard step, since it never gates setup.
///
/// Nothing here may grow the pane: Advanced is a non-scrolling form, and one
/// extra row here was enough to push its Reset button out of reach on the CI
/// runner's display (`testResetAsksBeforeDoingAnything`, "Not hittable").
/// the verbatim transcript is pasted exactly as spoken. The transcriber reads
/// the same default this toggle writes at every request, so a change applies to
/// the next dictation — see `AssemblyAITranscriber.transcript(from:)`.
/// Settings-only — not a wizard step, since it never gates setup.
private struct TranscriptionSection: View {
// The unset default comes from the store, not a literal here: the transcriber
// reads the same slot per request, and two spellings of "unset means on" would let
// the toggle and the request disagree about an untouched install.
@AppStorage(EnhancedTranscriptsStore.defaultsKey)
private var enhancedTranscripts = EnhancedTranscriptsStore.defaultValue
@AppStorage(SpokenPunctuationStore.defaultsKey) private var spokenPunctuation = false

var body: some View {
Section {
Toggle(isOn: $enhancedTranscripts) {
Label("Enhanced transcripts", systemImage: "wand.and.stars")
}
.disabled(spokenPunctuation)
.help(spokenPunctuation ? "Off while Speak all punctuation is on." : "")
.accessibilityIdentifier(UITestIdentifiers.enhancedTranscriptsToggle)
} header: {
Text("Transcription")
} footer: {
Text(footer)
}
}

private var footer: String {
if spokenPunctuation {
return "Off while Speak all punctuation is on, which pastes your words as spoken with only "
+ "the punctuation you say."
Text(
"Polishes each dictation before pasting — removing filler words and fixing punctuation. "
+ "Turn off to paste your words exactly as spoken.")
}
return "Polishes each dictation before pasting — removing filler words and fixing punctuation. "
+ "Turn off to paste your words exactly as spoken."
}
}

Expand All @@ -155,8 +139,8 @@ private struct TranscriptionSection: View {
/// request (see `CleanupInstruction.sendable(appending:)` / `StyleProfileStore`),
/// so the enhanced-transcript polish also applies the user's formatting
/// preferences. Optional — with none defined the request is exactly what ships
/// today. Disabled while enhanced transcripts are off (or overridden by spoken
/// punctuation), since the rewrite they shape is not pasted then.
/// today. Disabled while enhanced transcripts are off, since the instruction
/// they extend is not sent at all then.
///
/// Each row is a name and a way in: all editing happens in the sheet below, for
/// the reasons on `APIKeyStepView`'s. Which style is *active* is deliberately
Expand All @@ -166,10 +150,6 @@ private struct TranscriptionSection: View {
private struct StyleProfilesSection: View {
@AppStorage(EnhancedTranscriptsStore.defaultsKey)
private var enhancedTranscripts = EnhancedTranscriptsStore.defaultValue
@AppStorage(SpokenPunctuationStore.defaultsKey) private var spokenPunctuation = false

/// The view-side spelling of `EnhancedTranscriptsStore.pastesRewrite`.
private var stylesApply: Bool { enhancedTranscripts && !spokenPunctuation }

/// Bound to observe, not to write: the store owns the JSON encoding, so it
/// decodes this slot and the sheet writes through it, while `@AppStorage` is
Expand Down Expand Up @@ -205,13 +185,11 @@ private struct StyleProfilesSection: View {
// enhanced transcripts off the rewrite a style shapes is discarded
// unread, so describing the limit is the less useful half.
Text(
stylesApply
enhancedTranscripts
? "Up to \(StyleProfileStore.profileLimit) styles."
: spokenPunctuation
? "Styles don’t apply while Speak all punctuation is on."
: "Style preferences need enhanced transcripts turned on.")
: "Style preferences need enhanced transcripts turned on.")
}
.disabled(!stylesApply)
.disabled(!enhancedTranscripts)
.sheet(item: $editing) { profile in
StyleProfileEditorSheet(profile: profile, isExisting: profiles.contains(profile))
}
Expand Down Expand Up @@ -386,3 +364,37 @@ private struct StyleProfileEditorSheet: View {
dismiss()
}
}

/// The Updates section of the Settings window: the running version and a
/// "Check for Updates" button that runs the check and reports the result in a
/// modal (see `UpdateCheckModel`). The same check is reachable from the
/// "Check for Updates…" app-menu command and the menu-bar item; all three share
/// the one `UpdateCheckModel` owned by `AppDelegate`, so a check from any place
/// runs through the same controller.
private struct UpdateSection: View {
let model: UpdateCheckModel

var body: some View {
Section {
// "Blurt 0.1.31" — the label is the engine's (shared with the result
// alerts, so the two can't name the version differently).
SettingRow(title: model.versionLabel, systemImage: "arrow.triangle.2.circlepath") {
HStack(spacing: 8) {
// A user-initiated check that can stall on a slow connection needs
// visible progress, or the button reads as dead until the result
// alert lands. Show a spinner and disable the button while in flight
// (the model already ignores a second check) — the native equivalent
// of Sparkle's "Checking for updates…".
if model.isChecking {
ProgressView().controlSize(.small)
}
Button("Check for Updates") { model.checkForUpdates() }
.disabled(model.isChecking)
.accessibilityIdentifier(UITestIdentifiers.updateCheck)
}
}
} header: {
Text("Updates")
}
}
}
19 changes: 0 additions & 19 deletions App/Blurt/Blurt/Wizard/TextShortcutsSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,17 @@ import SwiftUI
///
/// Rows follow the Styles section's shape: each is a read-out plus an "Edit…"
/// way in, and all editing happens in the sheet below.
///
/// The "Speak all punctuation" switch sits above the list: it is the other
/// local rewrite of what was said (`SpokenPunctuationFormatter`, run just
/// before the shortcuts), and this is the one pane that scrolls — the Advanced
/// pane, beside the enhanced-transcripts switch it overrides, hugs its content
/// and had no height to spare (see `TranscriptionSection`).
struct TextShortcutsSection: View {
/// Bound to observe, not to write — the store owns the JSON encoding, as with
/// `StyleProfilesSection`.
@AppStorage(TextShortcutStore.defaultsKey) private var rawShortcuts = ""
@AppStorage(SpokenPunctuationStore.defaultsKey) private var spokenPunctuation = false

/// The shortcut the sheet is editing, or nil while it's closed.
@State private var editing: TextShortcut?

var body: some View {
let shortcuts = TextShortcutStore().shortcuts(decoding: rawShortcuts)
Form {
Section {
Toggle(isOn: $spokenPunctuation) {
Label("Speak all punctuation", systemImage: "quote.opening")
}
.accessibilityIdentifier(UITestIdentifiers.spokenPunctuationToggle)
} header: {
Text("Punctuation")
} footer: {
Text(
"Punctuation appears only where you say it — “comma”, “period”, “question mark”, "
+ "“new line”. Overrides Enhanced transcripts.")
}
Section {
ForEach(Array(shortcuts.enumerated()), id: \.element.id) { index, shortcut in
HStack(spacing: 12) {
Expand Down
3 changes: 1 addition & 2 deletions App/Blurt/BlurtUITests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ observable UI rather than mocking views.

- **Settings page** — `SettingsUITests`: the real Settings window's API-key flow
(save / reject / reveal, and the change → cancel round-trip back to the saved
row), the hotkey picker, the sound picker, and "Speak all punctuation" (on
the Text Shortcuts pane) disabling the enhanced-transcripts switch it overrides.
row), the hotkey picker, and the sound picker.
- **Text shortcuts** — `TextShortcutsUITests`: the Settings pane's add / edit /
delete round-trip through the editor sheet, and a duplicate phrase (differing
only in case and separators) leaving Save disabled.
Expand Down
22 changes: 0 additions & 22 deletions App/Blurt/BlurtUITests/SettingsUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,28 +122,6 @@ final class SettingsUITests: BlurtUITestCase {
XCTAssertEqual("\(toggle.value ?? "")", "1", "Clicking should switch developer mode on")
}

/// "Speak all punctuation" (Text Shortcuts pane) starts off, and switching it
/// on disables the enhanced-transcripts switch it overrides on the Advanced
/// pane (`EnhancedTranscriptsStore.pastesRewrite`).
func testSpokenPunctuationDisablesEnhancedTranscripts() {
let settings = openSettingsWindow()
let shortcuts = selectSettingsTab(settings, named: UITestIdentifiers.textShortcutsTab)

let toggle = shortcuts.anyDescendant(identified: UITestIdentifiers.spokenPunctuationToggle)
XCTAssertTrue(toggle.waitForExistence(timeout: 10), "Speak all punctuation toggle not found")
XCTAssertEqual("\(toggle.value ?? "")", "0", "Speak all punctuation should start switched off")

toggle.click()
XCTAssertEqual("\(toggle.value ?? "")", "1", "Clicking should switch spoken punctuation on")

// Switched from `shortcuts`, not `settings`: selecting a pane retitles the
// window, so the proxy from before the first switch is stale.
let advanced = selectSettingsTab(shortcuts, named: UITestIdentifiers.advancedSettingsTab)
let enhanced = advanced.anyDescendant(identified: UITestIdentifiers.enhancedTranscriptsToggle)
XCTAssertTrue(enhanced.waitForExistence(timeout: 10), "Enhanced transcripts toggle not found")
XCTAssertFalse(enhanced.isEnabled, "Spoken punctuation overrides enhanced transcripts")
}

/// The Advanced pane's "Check for Updates" button runs the check and reports
/// the result in a modal. Under UI testing the check is stubbed offline to
/// always report up-to-date, so clicking it surfaces the "You’re up to date"
Expand Down
1 change: 0 additions & 1 deletion App/Blurt/Shared/UITestIdentifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ enum UITestIdentifiers {
static let soundPicker = "settings.sound.picker"
static let developerToggle = "settings.developer.toggle"
static let enhancedTranscriptsToggle = "settings.enhancedTranscripts.toggle"
static let spokenPunctuationToggle = "settings.spokenPunctuation.toggle"
static let updateCheck = "settings.update.check"
/// The Advanced pane's "Reset…" button (`SettingsWindowRoot`'s reset section).
/// Only the row button is identified: the confirmation it opens is an alert,
Expand Down
2 changes: 0 additions & 2 deletions Sources/BlurtEngine/Config/DefaultsKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ enum DefaultsKey: String, CaseIterable {
case micDeviceUID = "MicDeviceUID"
/// The JSON list of spoken-phrase → text expansions (`TextShortcutStore`).
case textShortcuts = "TextShortcuts"
/// The "speak all punctuation" switch (`SpokenPunctuationStore`).
case spokenPunctuation = "SpokenPunctuation"

/// The key this case actually reads and writes, under the configured host
/// identity. A computed property rather than a stored string because the
Expand Down
9 changes: 0 additions & 9 deletions Sources/BlurtEngine/Config/EnhancedTranscriptsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,4 @@ public struct EnhancedTranscriptsStore {
var isEnabled: Bool {
defaults.object(forKey: Self.defaultsKey) as? Bool ?? Self.defaultValue
}

/// Whether the rewrite is what gets pasted: this switch, unless "speak all
/// punctuation" is on — that mode formats the verbatim transcript, since the
/// rewrite is told to fix punctuation and may already have turned a spoken
/// "comma" into a mark the formatter would then strip (`SpokenPunctuationStore`).
/// The one rule both the transcriber and the Recent row's style chip read.
var pastesRewrite: Bool {
isEnabled && !SpokenPunctuationStore(defaults: defaults).isEnabled
}
}
34 changes: 0 additions & 34 deletions Sources/BlurtEngine/Config/SpokenPunctuationStore.swift

This file was deleted.

Loading
Loading