feat: Notitie editor in the app, dictation routed to system or editor

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 09:47:54 +02:00
co-authored by Claude Sonnet 5.5
parent ab7359156b
commit f390d310f4
12 changed files with 433 additions and 34 deletions
+24 -10
View File
@@ -6,20 +6,25 @@ import SstData
import SstWhisper
import SwiftUI
/// Decides at insertion time whether dictated text may be pasted.
/// Not when Sst itself is in front (it would land in Sst's own search field) and not for recordings started in the window.
/// Decides at insertion time where dictated text goes.
/// Sst in front: into the Notitie editor if (and only if) it has the cursor, otherwise nowhere (it would land in Sst's own
/// search field). Another app in front: pasted there, except for recordings started in the window.
final class InsertionPolicy {
var windowSession = false
func allowed() -> Bool {
!windowSession && NSWorkspace.shared.frontmostApplication?.bundleIdentifier != Bundle.main.bundleIdentifier
func route() -> InsertionRoute {
MainActor.assumeIsolated {
let sstInFront = NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier
if sstInFront { return NoteEditorRegistry.shared.active != nil ? .editor : .none }
return windowSession ? .none : .system
}
}
}
@MainActor
final class AppModel: ObservableObject {
enum Section: String, CaseIterable, Identifiable {
case transcripts, dictionary
case transcripts, dictionary, note
var id: String { rawValue }
}
@@ -37,6 +42,7 @@ final class AppModel: ObservableObject {
let history: HistoryStore
let dictionary: DictionaryStore
let storage = ModelStorage()
let note: NoteStore
let historyList: HistoryViewModel
let dictionaryList: DictionaryViewModel
@@ -77,6 +83,7 @@ final class AppModel: ObservableObject {
let folder = FileManager.default.temporaryDirectory.appending(path: "sst-preview-\(UUID().uuidString)", directoryHint: .isDirectory)
let dictionary = DictionaryStore(fileURL: preview ? folder.appending(path: "dictionary.txt") : AppPaths.dictionary)
let history = HistoryStore(fileURL: preview ? folder.appending(path: "history.json") : AppPaths.history)
let note = NoteStore(fileURL: preview ? folder.appending(path: "note.txt") : AppPaths.note)
let levelSource = self.levelSource
let recorder = AudioRecorder()
recorder.onLevel = { levelSource.update($0) }
@@ -86,15 +93,17 @@ final class AppModel: ObservableObject {
self.corrector = corrector
self.dictionary = dictionary
self.history = history
self.note = note
self.historyList = HistoryViewModel(store: history)
self.dictionaryList = DictionaryViewModel(store: dictionary, knowledge: knowledge)
self.controller = DictationController(
recorder: recorder,
transcriber: transcriber,
cleaner: PassthroughCleaner(),
inserter: GatedInserter(
inner: TextInserter(pasteboard: SystemPasteboard(), paster: CGEventPaster()),
shouldInsert: { policy.allowed() }),
inserter: RoutingInserter(
system: TextInserter(pasteboard: SystemPasteboard(), paster: CGEventPaster()),
editor: EditorInserter(),
route: { policy.route() }),
corrector: corrector,
vocabularyProvider: { settings.usePriming ? VocabularyBuilder.build(from: dictionary.entries) : [] })
@@ -221,14 +230,18 @@ final class AppModel: ObservableObject {
NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.dictionary.reloadIfChanged() }
}
NotificationCenter.default.addObserver(forName: NSApplication.willTerminateNotification, object: nil, queue: .main) { [history] _ in
MainActor.assumeIsolated { history.flush() }
NotificationCenter.default.addObserver(forName: NSApplication.willTerminateNotification, object: nil, queue: .main) { [history, note] _ in
MainActor.assumeIsolated {
history.flush()
note.flush()
}
}
}
private func start() async {
do { try dictionary.load() } catch { lastError = error.localizedDescription }
dictionary.startWatching()
note.load()
switch history.load() {
case .recoveredCorrupt:
lastError = "Het geschiedenisbestand was beschadigd en is opzij gezet."
@@ -364,6 +377,7 @@ final class AppModel: ObservableObject {
// MARK: preview
private func loadPreviewData() {
note.text = "Notities voor vrijdag: Supabase-migratie plannen en Claude Code uitproberen."
try? dictionary.load()
try? dictionary.apply { file in
file.append(.term("Anthropic"))