Keep the front where the dictation started on macOS

Recording goes through ffmpeg's avfoundation input, and opening a capture
session there brings the process that did it to the front. ffmpeg is a
child of Dikte with no bundle of its own, so macOS credits the move to
Dikte: the window the user was dictating into goes inactive, its caret
stops, its title bar greys out, and the Cmd+V at the end of the run lands
somewhere other than the document it was meant for. Measured with a
TextEdit document in front:

    press the shortcut          front = TextEdit
    recorder.start returns      front = TextEdit
    89 ms later                 front = Dikte

Nothing about the capture session can be asked not to do this. Starting
ffmpeg in its own session, and clearing __CFBundleIdentifier from its
environment, were both tried and both measured to make no difference, so
it is undone instead: the application in front is noted before the
indicator goes up, and a short watch puts it back the moment Dikte takes
the front. Measured at 99 ms from the moment it is taken, against the
title bar staying grey for the whole dictation before. All three ways in
do it, dictation, agent and meeting, since all three open the same
capture.

The indicator had a share of the same problem and needed AppKit for it
too, so mac_window.py carries both. An NSPanel is hidden by the system
the moment its application stops being the active one, which for a
dictation indicator is immediately, and ordering one to the front brings
its application with it unless it carries the nonactivating style bit.
Neither is reachable through Qt.

The runtime is loaded in _appkit() rather than at import, the way paste.py
loads its frameworks in _macos_api(), so the module imports on a machine
with no AppKit and the tests below run there as well: 52 new tests, none
of them skipped anywhere.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Gökhan
2026-08-16 17:37:31 +03:00
co-authored by Claude Opus 5
parent 569fe7cea8
commit 45b18722be
8 changed files with 679 additions and 15 deletions
+13 -3
View File
@@ -32,7 +32,8 @@ class Chain(DikteTest):
transcript="uh, book it for Thursday",
cleaned="Book it for Thursday.",
cleanup_error=None, answer=("Booked.", ""), rms=None,
clipboard=b"what was there before", paste_error=None):
clipboard=b"what was there before", paste_error=None,
focus=None):
pipeline = worker.Pipeline(self.conf)
done, failures, stages, cancels = [], [], [], []
pipeline.finished.connect(lambda *args: done.append(args))
@@ -60,7 +61,8 @@ class Chain(DikteTest):
"copy": copy, "copy_bytes": copy_bytes, "press": press,
"read_clipboard": read_clipboard}
pipeline._work(self.wav, duration,
self.rms if rms is None else rms, ask, paste_override)
self.rms if rms is None else rms, ask, paste_override,
focus)
return {"done": done, "failures": failures, "stages": stages,
"cancelled": cancels, **calls}
@@ -72,7 +74,15 @@ class Chain(DikteTest):
self.assertEqual(run["done"][0],
("uh, book it for Thursday", "Book it for Thursday.", ""))
run["copy"].assert_called_once_with("Book it for Thursday.")
run["press"].assert_called_once_with(self.conf["paste_shortcut"])
run["press"].assert_called_once_with(self.conf["paste_shortcut"],
focus=None)
def test_the_paste_is_told_where_the_dictation_started(self):
"""Whoever was in front when the recording began is where the keys are
meant to go, and the press is the only part that can act on it."""
run = self.run_chain(focus=4242)
run["press"].assert_called_once_with(self.conf["paste_shortcut"],
focus=4242)
def test_the_stages_are_named_as_they_happen(self):
run = self.run_chain()