-
Notifications
You must be signed in to change notification settings - Fork 4
Offer AI-notes generation for transcripts that never got a summary #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,15 +48,33 @@ public final class SummaryController { | |
| model: choice.model, effort: choice.effort) | ||
| self.summaryExists = FileManager.default.fileExists( | ||
| atPath: NoteLayout.summaryURL(noteFolder: noteFolderURL).path) | ||
| self.transcriptHasCues = VTT.transcriptHasCues(noteFolder: noteFolderURL) | ||
| watcher = FileTreeWatcher.observing(url: noteFolderURL) { [weak self] in | ||
| self?.refreshSummaryExists() | ||
| self?.refreshFileState() | ||
| } | ||
| } | ||
|
|
||
| private func refreshSummaryExists() { | ||
| /// Internal (not private) so tests can drive the watcher path | ||
| /// synchronously. | ||
| func refreshFileState() { | ||
| let summaryExisted = summaryExists | ||
| summaryExists = FileManager.default.fileExists(atPath: summaryFileURL.path) | ||
| transcriptHasCues = VTT.transcriptHasCues(noteFolder: noteFolderURL) | ||
| // Filesystem is truth: an external delete of summary.md (Finder, | ||
| // git, a chat thread) must tell the note view to drop the editor's | ||
| // held text, or closing the note resurrects the file. Idle-only: | ||
| // the fresh-regenerate delete lands here with status already | ||
| // .working (or .failed), and there the held text is the documented | ||
| // failed-run recovery. | ||
| if summaryExisted && !summaryExists && status == .idle { | ||
| summaryFileRemovedExternally?() | ||
|
Comment on lines
+69
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If an in-place generation fails while an existing summary remains loaded, Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| /// The note view's hook to drop its AI-notes editor text after an | ||
| /// external delete; nil once the view is gone (nothing holds text then). | ||
| var summaryFileRemovedExternally: (() -> Void)? | ||
|
|
||
| var summaryFileURL: URL { NoteLayout.summaryURL(noteFolder: noteFolderURL) } | ||
|
|
||
| /// Flushes any pending debounced editor autosaves so the summarizer | ||
|
|
@@ -71,6 +89,11 @@ public final class SummaryController { | |
| /// in-app generations. | ||
| private(set) var summaryExists: Bool | ||
|
|
||
| /// Whether the transcript on disk has at least one cue — stored and | ||
| /// watcher-refreshed like summaryExists, so late-draining uploads and | ||
| /// external edits flip the first-generation offer live. | ||
| private(set) var transcriptHasCues: Bool | ||
|
|
||
| private var watcher: FileTreeWatcher? | ||
|
|
||
| /// The shared live availability model — the same source the note | ||
|
|
@@ -89,6 +112,38 @@ public final class SummaryController { | |
| enabled && transcriptHasCues && codexAvailable && !alreadyWorking && !recordingActive | ||
| } | ||
|
|
||
| /// Whether the note view offers a first-generation button (issue #3): | ||
| /// the note has a transcript worth summarizing but no AI notes and no | ||
| /// run in flight — the states where the tab strip (and so the | ||
| /// regenerate button) doesn't exist yet. Codex availability is | ||
| /// deliberately absent: like the regenerate button, the offer shows | ||
| /// disabled with an explanatory tooltip when Codex is degraded. | ||
| nonisolated static func shouldOfferFirstGeneration( | ||
| enabled: Bool, transcriptHasCues: Bool, summaryExists: Bool, statusIdle: Bool, | ||
| recordingActive: Bool | ||
| ) -> Bool { | ||
| enabled && transcriptHasCues && !summaryExists && statusIdle && !recordingActive | ||
| } | ||
|
|
||
| /// The offer predicate over this note's live state. | ||
| var canOfferFirstGeneration: Bool { | ||
| Self.shouldOfferFirstGeneration( | ||
| enabled: SimbiSettings.current().aiNotesEnabled, | ||
| transcriptHasCues: transcriptHasCues, summaryExists: summaryExists, | ||
| statusIdle: status == .idle, | ||
| recordingActive: RecordingController.isCapturing(noteFolderURL: noteFolderURL)) | ||
| } | ||
|
|
||
| /// The first-generation button. Same guards as retry(); nothing to | ||
| /// delete, and beginRun() detects first-ness from disk so the | ||
| /// placeholder and tab strip appear through the existing paths. | ||
| func generateFirst() { | ||
| guard SimbiSettings.current().aiNotesEnabled, status != .working, codexAvailable, | ||
| !RecordingController.isCapturing(noteFolderURL: noteFolderURL) | ||
| else { return } | ||
| generate() | ||
| } | ||
|
|
||
| /// The recording controller's clean-stop hook. | ||
| func recordingDidStop() { | ||
| let hasCues = VTT.transcriptHasCues(noteFolder: noteFolderURL) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a media import is transcribing or fixing,
recorder.statusremains.idlewhile cues are progressively written, so this condition exposes the new generate button during the import.generateFirst()likewise has noImportController.isImportingguard, allowing generation from a partial transcript; if the import finishes while that run is still working, the completion hook skips its corrective auto-generation becausealreadyWorkingis true, leaving incomplete AI notes. Gate both the offer and action on the import being idle.Useful? React with 👍 / 👎.