Open saved session: visible progress + clear errors (no silent no-op)

The status line only rendered inside the last-in-memory-session block, so 'Open
saved session' processed invisibly — looked like nothing happened. Now: the
transcript status (with a spinner) is always shown, the processing(0,0) reconcile
phase reads 'Working… (this can take a few minutes)', and invalid picks surface an
alert (not a recorded session / already processing / unreadable transcript) instead
of doing nothing.
This commit is contained in:
Grant Gilliam
2026-06-08 12:16:52 -05:00
parent 3bc169533a
commit 9a18664429
2 changed files with 38 additions and 11 deletions
@@ -444,13 +444,16 @@ final class SessionController: ObservableObject {
if let folder = lastSession?.folder { openEditor(folder: folder) } if let folder = lastSession?.folder { openEditor(folder: folder) }
} }
/// Open the editor for any session folder that has a `speakers.json`. /// Open the editor for any session folder that has a `speakers.json`. Returns
private func openEditor(folder: URL) { /// false if it couldn't load (no/empty transcript).
@discardableResult
private func openEditor(folder: URL) -> Bool {
guard let model = RecapEditModel(folder: folder, voiceprints: voiceprints, guard let model = RecapEditModel(folder: folder, voiceprints: voiceprints,
baseURL: settings.backendBaseURL, skipTLS: settings.skipTLSVerification, baseURL: settings.backendBaseURL, skipTLS: settings.skipTLSVerification,
templates: settings.recapTemplates, defaultTemplateId: settings.defaultTemplateId) templates: settings.recapTemplates, defaultTemplateId: settings.defaultTemplateId)
else { return } else { return false }
EditorWindow.shared.show(model: model) EditorWindow.shared.show(model: model)
return true
} }
/// Pick any past session folder and open it: edit it if already transcribed, /// Pick any past session folder and open it: edit it if already transcribed,
@@ -467,13 +470,22 @@ final class SessionController: ObservableObject {
guard panel.runModal() == .OK, let folder = panel.url else { return } guard panel.runModal() == .OK, let folder = panel.url else { return }
let fm = FileManager.default let fm = FileManager.default
if fm.fileExists(atPath: folder.appendingPathComponent("speakers.json").path) { if fm.fileExists(atPath: folder.appendingPathComponent("speakers.json").path) {
openEditor(folder: folder) if !openEditor(folder: folder) {
Self.alert("Couldn't open this session — its transcript looks empty or unreadable.")
}
return return
} }
// Not transcribed yet needs the raw tracks to (re)process. // Not transcribed yet needs the raw tracks to (re)process.
let mic = folder.appendingPathComponent("mic.wav") let mic = folder.appendingPathComponent("mic.wav")
let sys = folder.appendingPathComponent("system.wav") let sys = folder.appendingPathComponent("system.wav")
guard fm.fileExists(atPath: mic.path), fm.fileExists(atPath: sys.path), !isProcessing else { return } guard fm.fileExists(atPath: mic.path), fm.fileExists(atPath: sys.path) else {
Self.alert("\(folder.lastPathComponent)” isnt a recorded session (no mic.wav / system.wav).")
return
}
guard !isProcessing else {
Self.alert("Already processing a session — let it finish first.")
return
}
transcriptStatus = .processing(0, 1) transcriptStatus = .processing(0, 1)
recapURL = nil recapURL = nil
let selfName = settings.selfName let selfName = settings.selfName
@@ -500,6 +512,15 @@ final class SessionController: ObservableObject {
folder.lastPathComponent.split(separator: "_").last.map(String.init) ?? "manual" folder.lastPathComponent.split(separator: "_").last.map(String.init) ?? "manual"
} }
private static func alert(_ message: String) {
let a = NSAlert()
a.messageText = "Open saved session"
a.informativeText = message
a.alertStyle = .informational
NSApp.activate(ignoringOtherApps: true)
a.runModal()
}
private func fail(_ message: String) { private func fail(_ message: String) {
recorder = nil recorder = nil
visualCapture = nil // recorder.start() failed before visual started; nothing running visualCapture = nil // recorder.start() failed before visual started; nothing running
+9 -3
View File
@@ -99,14 +99,19 @@ struct MenuBarView: View {
} }
Spacer() Spacer()
} }
if !transcriptText.isEmpty {
Text(transcriptText).font(.caption).foregroundStyle(transcriptColor)
}
} }
HStack(spacing: 6) {
Button("Open saved session…") { session.openSavedSession() } Button("Open saved session…") { session.openSavedSession() }
.buttonStyle(.link).font(.caption) .buttonStyle(.link).font(.caption)
.disabled(transcriptProcessing) .disabled(transcriptProcessing)
if transcriptProcessing { ProgressView().controlSize(.small) }
}
// Always-visible status (covers "Send to backend" AND "Open saved session").
if !transcriptText.isEmpty {
Text(transcriptText).font(.caption).foregroundStyle(transcriptColor)
}
} }
} }
@@ -152,6 +157,7 @@ struct MenuBarView: View {
private var transcriptText: String { private var transcriptText: String {
switch session.transcriptStatus { switch session.transcriptStatus {
case .idle: return "" case .idle: return ""
case .processing(_, let t) where t == 0: return "Working… (this can take a few minutes)"
case .processing(let d, let t): return "Transcribing… chunk \(d)/\(t)" case .processing(let d, let t): return "Transcribing… chunk \(d)/\(t)"
case .done(let s, let seg): return "Transcript ready · \(s) speakers · \(seg) segments" case .done(let s, let seg): return "Transcript ready · \(s) speakers · \(seg) segments"
case .failed(let m): return "Transcript failed: \(m)" case .failed(let m): return "Transcript failed: \(m)"