Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Snap and Snap & Talk: ⌘Z in a text field undoes the typing, and a narration whose recorder stops by itself is kept

Open
#380 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
22/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
swift

Research direction

Two separate bugs in the Mac app. For the first, start at the Undo and Redo buttons in SnapEditorView.swift and at undo() in ImageWorkspaceEditing.swift. For the second, start at the meter timer and keepNarration in ReadbackModel.swift. Checks live in ImageWorkspaceChecks.swift and ReadbackChecks.swift, run via LocalVoice --check-image-workspace and --check-readback. Done means the new checks pass on native macOS, not only in the offscreen harness.

Written by the indexing model from the issue text.

Description

bug

Outcome: Snap and Snap & Talk: ⌘Z in a text field undoes the typing, and a narration whose recorder stops by itself is kept.

Verified by an independent skeptic against the release train head d7cea35 (round four of the 15–16 October pass: energy, leaks, first run, keyboard, concurrency, settings propagation, public docs).

Snap editor: ⌘Z in Title, Tags or Notes undoes the last mark on the image, not the typing

What a person sees: After any image edit (an arrow, a crop, a rotate or a text box), someone types a title or note and presses ⌘Z to fix a typo. The last annotation disappears from the image and the typo stays. Each further ⌘Z removes another mark, and while any image edit remains, typing in these fields cannot be undone from the keyboard.

Evidence (d7cea35): SnapEditorView.swift:46-49: Undo carries .keyboardShortcut("z", modifiers: .command) and Redo ("z", [.command, .shift]), disabled only when their stacks are empty. ImageWorkspaceEditing.swift:91, :109-121: undoStack holds SnapEdit only, and undo() restores draft.edit. SnapModel.swift:5-14: title, notes and tags are outside edit. SnapEditorView.swift:128-133: the Title and Tags TextFields and the Notes TextEditor bind to draft.title, draft.tags and draft.notes. CaptureImagePreview.swift:405-416: performKeyEquivalent handles Escape, arrows and zoom keys (command(for:) :418-424 has no 'z') and otherwise calls super, where the hosting view's SwiftUI key equivalent matches before the main menu. main.swift:455-456: Edit › Undo/Redo send undo:/redo: to the first responder, which is the field's own text undo. No check sends ⌘Z while a text field has focus.

Change: Make the change in SnapEditorView.swift only.

  1. Add private enum DetailsField: Hashable { case title, tags, notes } and @FocusState private var detailsField: DetailsField?. Keep this separate from the existing textFocused, which the "Text on image" caption uses to end its undo run.
  2. Add .focused($detailsField, equals: .title) to the Title TextField, .tags to the Tags TextField and .notes to the Notes TextEditor.
  3. Change Undo to .keyboardShortcut(detailsField == nil ? KeyboardShortcut("z", modifiers: .command) : nil). Change Redo the same way with [.command, .shift]. The optional-shortcut form is available on the macOS 14 deployment target, and the same pattern is used at Sources/StageKit/PersonaLibraryView.swift:136.
  4. Leave these as they are:
    • the buttons, their .disabled and their help text;
    • CaptureImagePreviewPanel;
    • the canvas, which keeps image undo;
    • the caption TextEditor, which keeps image undo as #331 decided.
  5. Add one sentence to docs/snap.md:22: in Title, Tags and Notes, ⌘Z and ⇧⌘Z belong to the text field, not to the image.

PR wording should say "⌘Z in Title, Tags or Notes no longer removes marks from the image". It should not promise that the field's own undo reaches the saved Snap. That separate problem should be recorded with its own evidence: on macOS 26.5, TextKit 2 field and TextEditor undo posts no did-change notification, so SwiftUI bindings stay stale. Do not widen this fix to handle it.

Acceptance: This goes in LocalVoice --check-image-workspace (run by scripts/test.sh:91), inside the existing live-panel block at ImageWorkspaceChecks.swift:156. At that point addText() has already left a mark, so Undo is enabled.

  1. Record liveEditor.draft.edit and liveEditor.undoStack.count.
  2. Find the editable NSTextField whose placeholder is "Title" under livePanel.contentView. Call livePanel.makeFirstResponder on it, then settle(preview) so FocusState updates.
  3. Build a ⌘Z keyDown (keyCode 6, characters "z", windowNumber livePanel.windowNumber) and assert:
    • livePanel.performKeyEquivalent(with:) returns false;
    • draft.edit and the undo count are unchanged.
  4. Do the same for ⇧⌘Z and the redo stack. Repeat step 3 with the "Image notes" TextEditor's NSTextView as first responder.
  5. Make liveCanvas first responder, settle, send ⌘Z, and assert that performKeyEquivalent returns true and the undo count drops by exactly one.
  6. Select the text mark, focus its caption NSTextView, settle, send ⌘Z, and assert that the image undo still runs. This keeps the #331 contract.

Step 3 fails on d7cea35: the scratch program showed a SwiftUI button shortcut firing from a non-key

Value: 3 of 5 · Grammar: Quality · Risk: Low for the change itself: one view, an optional shortcut, and a pattern already used in the repo.

Behaviour after the fix

  • ⌘Z in Title, Tags or Notes goes to AppKit's window undo manager. That manager is shared, so after its own typing has been undone, ⌘Z in one field can undo earlier typing in Notes or the caption TextEditor. This is standard Mac behaviour.
  • On macOS 26.5, TextKit 2 undo did not notify SwiftUI in my offscreen program. The field can show the reverted text while draft.tit
Snap & Talk: a narration whose recorder stops by itself is kept, not left 'recording' at 0:00 and then deleted on Stop

What a person sees: If the recorder stops under someone mid-narration (a full disk, or an input device that drops), the page and toolbar keep saying narration is live, the timer reads 0:00 and the 5-minute auto-stop can never fire. Pressing Stop then deletes the WAV holding everything said before the failure and reports 'No clear speech was captured'. Cancel also skips its 'Discard this narration?' question. Report a Problem's voice note already ends and keeps its audio in the same situation.

Evidence (d7cea35): ReadbackModel.swift:402 declares AVAudioRecorderDelegate. :1059-1062 sets capture.delegate = self. Neither audioRecorderDidFinishRecording nor audioRecorderEncodeErrorDidOccur exists outside BugReportModel.swift:42-47. The meter Timer (:1046-1055) sets recordingElapsed = recorder.currentTime (:1049); AVAudioRecorder reports currentTime as 0 when not recording, so the >= 300 stop (:1052) never fires and the UI shows 0:00. The Timer is invalidated only in keepNarration (:1071) and cancelNarration (:1123). keepNarration computes duration = recorder?.currentTime ?? recordingElapsed (:1070). With 0, the guard at :1074 fails, :1075 removes context.pendingURL and :1076 restores with 'No clear speech was captured…'. requestCancelNarration uses cancelAsksFirst(elapsed: recordingElapsed) (:1109), which is false at 0. For comparison, BugReportRecorder forwards both callbacks to onFinish, and BugReportModel wires that to stopRecording, which reads the file.

Change: In Sources/LocalVoice/ReadbackModel.swift:

  1. Meter tick (:1049): change to self.recordingElapsed = max(self.recordingElapsed, recorder.currentTime). A stopped recorder can then never reset the take's length, the on-screen time or the Cancel question to 0.
  2. keepNarration (:1070): change to let duration = max(recordingElapsed, recorder?.currentTime ?? 0).
  3. Add two methods:
    • nonisolated func audioRecorderDidFinishRecording(_ r: AVAudioRecorder, successfully: Bool)
    • nonisolated func audioRecorderEncodeErrorDidOccur(_ r: AVAudioRecorder, error: Error?)
      Each does Task { @MainActor in self.narrationRecorderEnded(r) }, the same pattern as BugReportModel.swift:42-47.
  4. Add an internal func narrationRecorderEnded(_ ended: AnyObject). It returns unless isRecording is true and ended === recorder, so the callback that follows our own stop(), Cancel or Quit is a no-op, including after a new take has started. Otherwise it calls keepNarration(cue: true). If the take was kept (the section is now queued), it replaces the notice with one plain sentence, for example: "The microphone stopped, so narration ended. What was recorded was kept." A too-short or quiet take keeps the existing "No clear speech" path.

No new UI, settings or doors. No other files change except the check.

Acceptance: Add these to LocalVoice --check-readback, next to the cancel checks (ReadbackChecks.swift:945-1009), using SyntheticNarrationRecorder(seconds: 1.0, peak: -10).

(a) Self-stop with the callback:

  • Start a take and wait for recordingElapsed == 1.0.
  • Set take.reportedTime = 0 (a stopped AVAudioRecorder), then sleep at least 300 ms so several meter ticks run.
  • Assert recordingElapsed is still 1.0 (this guards against the race).
  • Call model.narrationRecorderEnded(take!).
  • Assert: !isRecording; the section is .queued with audio == dir/narration.wav matching take.written; no narration-pending file remains; onNarrationNotHeard did not fire; the notice names the microphone stopping. After another 300 ms, recordingElapsed and the section are unchanged.

(b) Self-stop with no callback, then Stop:

  • Same setup, but call stopNarration() instead.
  • Assert the take is kept, not "No clear speech was captured".

(c) Long self-stopped take, then Cancel:

  • reportedTime 74, then 0, then several ticks.
  • Assert requestCancelNarration() still asks first.

(d) A late callback after a normal Stop:

  • Call narrationRecorderEnded with the old take after Stop, and again after a new

Value: 2 of 5 · Grammar: Quality · Risk: Low to medium.

  1. The trigger itself (how often AVAudioRecorder stops on its own on macOS) is unproven natively. The fix is sound whether or not it happens; only its frequency is uncertain.
  2. The candidate as written (callback plus max in keepNarration only) still deletes the take whenever a meter tick lands before the callback, and on every Stop without a callback. The monotonic meter change is required.
  3. The identity guard is required. AVAudioRecorder also calls audioRecorderDidFinishRecor

Refs #63


Build-ready record from round four of the overnight lead pass, verified by an independent skeptic per item. Closes only after native acceptance.

Dominant language
Swift
Stars
0
Forks
1
Avg merge
8h 5m
Merged PRs (30d)
215

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Ship-Work/workbench

All issues in Ship-Work/workbench

Similar issues

More Swift issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.