From cd419558fd11eea4171354113a3bd1821dc47711 Mon Sep 17 00:00:00 2001 From: kemal Date: Fri, 28 Aug 2026 02:03:51 +0300 Subject: [PATCH 1/5] Do not require claude on PATH when assistant uses a hosted provider --- dikte/cli.py | 2 +- tests/test_cli.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/dikte/cli.py b/dikte/cli.py index c1ef786..37bc87d 100644 --- a/dikte/cli.py +++ b/dikte/cli.py @@ -873,7 +873,7 @@ def cmd_doctor(opts): # Recording, the device list, and KDE's shortcut registry. wanted += ["pw-record", "pactl", "kwriteconfig6"] wanted += ["ffmpeg", - assistant.executable(assistant.provider(conf)) or "claude", + assistant.executable(assistant.provider(conf)), cleanup.executable(cleanup.provider(conf))] programs = {name: shutil.which(name) or "" for name in wanted if name} target = conf.transcribe_target() diff --git a/tests/test_cli.py b/tests/test_cli.py index 67c6f21..c319d4b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -577,6 +577,21 @@ class Doctor(DikteTest): self.run_doctor(as_json=False, cleanup_provider="codex", cleanup_codex_model="gpt-5.4")) + def test_agent_on_hosted_provider_does_not_ask_for_a_cli_program(self): + for provider in ("openrouter", "opencode"): + with self.subTest(provider=provider): + reply = self.run_doctor(assistant_provider=provider) + self.assertEqual(reply["agent"]["provider"], provider) + for cli_name in ("claude", "codex", "agy"): + self.assertNotIn(cli_name, reply["programs"]) + + def test_agent_on_a_cli_asks_for_the_program(self): + for provider, binary in (("claude", "claude"), ("codex", "codex"), ("agy", "agy")): + with self.subTest(provider=provider): + reply = self.run_doctor(assistant_provider=provider) + self.assertEqual(reply["agent"]["provider"], provider) + self.assertIn(binary, reply["programs"]) + class Devices(DikteTest): def test_a_machine_with_nothing_names_its_own_missing_program(self): From 21e28f621e74e4a71eac18cb6997cf0774c04d91 Mon Sep 17 00:00:00 2001 From: kemal Date: Fri, 28 Aug 2026 02:06:22 +0300 Subject: [PATCH 2/5] Allow wheel events on focused boxes when window is inactive --- dikte/settings_ui.py | 4 +++- tests/test_ui.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 56771f8..1bf182e 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -207,7 +207,9 @@ class WheelGuard(QObject): """ def eventFilter(self, box, event): - if event.type() == QEvent.Type.Wheel and not box.hasFocus(): + win = box.window() + focused = box.hasFocus() or (win is not None and win.focusWidget() is box) + if event.type() == QEvent.Type.Wheel and not focused: # Refused rather than swallowed. An unaccepted wheel event carries # on up the parents to the scroll area, so the page still moves. event.ignore() diff --git a/tests/test_ui.py b/tests/test_ui.py index 6273447..038137f 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -223,6 +223,17 @@ class Settings(DikteTest): QApplication.sendEvent(box, self.wheel()) self.assertNotEqual(box.currentIndex(), before) + def test_the_wheel_is_refused_when_another_widget_has_focus(self): + window = self.window(cfg.Config()) + box = window.ui_language + other = window.corner + other.setFocus() + before = box.currentIndex() + rolled = self.wheel() + QApplication.sendEvent(box, rolled) + self.assertEqual(box.currentIndex(), before) + self.assertFalse(rolled.isAccepted()) + def test_a_wrapped_label_keeps_the_room_its_lines_need(self): # The program path shares a row with a button, and a row is measured # before its width is known: the label has to claim the second line back From 6d7c591b7de093c67c895eb94edc398fec00cc5e Mon Sep 17 00:00:00 2001 From: kemal Date: Fri, 28 Aug 2026 02:14:40 +0300 Subject: [PATCH 3/5] Use generic agent label in desktop shortcut name --- dikte/hotkey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dikte/hotkey.py b/dikte/hotkey.py index 91e4c6b..cc793e8 100644 --- a/dikte/hotkey.py +++ b/dikte/hotkey.py @@ -57,7 +57,7 @@ SHORTCUTS = { "Dikte: pause/resume the recording", "pause_shortcut", ""), "cancel": Shortcut("cancel", CANCEL_DESKTOP_ID, "Dikte: discard the recording", "cancel_shortcut", ""), - "ask": Shortcut("ask", ASK_DESKTOP_ID, "Dikte: ask Claude Code", + "ask": Shortcut("ask", ASK_DESKTOP_ID, "Dikte: ask the agent", "assistant_shortcut", ""), "meeting": Shortcut("meeting", MEETING_DESKTOP_ID, "Dikte: start/end a meeting recording", From b46181e001c595c8ce3b0bc79b9168456376c3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20=C3=96mer=20Okyar?= <79448601+catrobe@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:10:07 +0300 Subject: [PATCH 4/5] Describe the binary the settings point at, not the one Dikte found program_path takes the custom path as its second argument, and config.py passes it, but the settings window never did. Happened on this machine: Compiled my own whisper.cpp and llama.cpp with CUDA and pointed the settings at them. The box with no downloaded copy said "Not installed." The box with one said "Downloaded, version b4938." naming a processor build while the CUDA one did the transcribing. LocalModelBox is now handed a callable, so the setting is read when the label is drawn rather than frozen when the window is built. The label reflects that too. A path set by hand now says so, instead of claiming Dikte downloaded something it didn't. --- dikte/i18n.py | 1 + dikte/settings_ui.py | 25 ++++++++++++++++++++----- tests/test_ui.py | 23 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/dikte/i18n.py b/dikte/i18n.py index 5f45805..4b247cd 100644 --- a/dikte/i18n.py +++ b/dikte/i18n.py @@ -806,6 +806,7 @@ TR = { "Local model": "Yerel model", "Not installed.": "Kurulu değil.", "Installed on the system: {path}": "Sistemde kurulu: {path}", + "Using custom build: {path}": "Özel derleme kullanılıyor: {path}", "Download again": "Yeniden indir", "Downloaded, version {version}.": "İndirildi, sürüm {version}.", "Downloaded, version {version}. There was no Vulkan build, " diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 306df4e..cb4a1fd 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -253,9 +253,11 @@ class LocalModelBox(QGroupBox): changed = pyqtSignal() - def __init__(self, program, title, models, model_path, repos=None, parent=None): + def __init__(self, program, title, models, model_path, binary=None, + repos=None, parent=None): super().__init__(title, parent) self.program = program + self._binary = binary # () -> a path set by hand, or "" self._models = models # () -> [hub.Item], or (repo) -> [hub.Item] self._model_path = model_path # (name) -> Path self._repos = repos # None, or () -> [repo id] @@ -417,13 +419,23 @@ class LocalModelBox(QGroupBox): self._fill_repos(self.repository()) self._fetch_models(self.repository()) + def _program_path(self): + return ggml.program_path(self.program, + self._binary() if self._binary else "") + def _show_program(self): - path = ggml.program_path(self.program) + path = self._program_path() if not path: self.program_label.setText(t("Not installed.")) self.install_button.setText(t("Download")) self.install_button.setVisible(True) return + if self._binary and self._binary(): + # Neither a system copy nor one Dikte fetched, and "Downloaded" + # over a build someone made themselves is not true. + self.program_label.setText(t("Using custom build: {path}", path=path)) + self.install_button.setVisible(False) + return # A copy that is here is not a copy that is right. whisper.cpp releases # every few weeks, and a graphics card installed after Dikte was # changes which build this machine should be running; the button was @@ -835,7 +847,7 @@ class LocalModelBox(QGroupBox): repo=self.repository(), cap=ggml.human_size(ggml.GGUF_MAX_BYTES))) elif not name: self.status.setText(t("Nothing downloaded yet.")) - elif here and not ggml.program_path(self.program): + elif here and not self._program_path(): # The model alone runs nothing, and "Ready" over a missing program # reads as though it does. self.status.setText(t("{name} is here, but the program above is " @@ -1191,7 +1203,8 @@ class SettingsWindow(QDialog): self.local_whisper = LocalModelBox( ggml.WHISPER, t("On this machine"), - ggml.whisper_models, ggml.whisper_model_path) + ggml.whisper_models, ggml.whisper_model_path, + binary=lambda: self.conf["local_binary"]) stt_form.addRow(self.local_whisper) self.local_gpu = QCheckBox(t("Use the graphics card")) @@ -1300,7 +1313,9 @@ class SettingsWindow(QDialog): self.local_llm = LocalModelBox( ggml.LLAMA, t("On this machine"), - ggml.llm_quants, ggml.llm_model_path, repos=ggml.llm_repos) + ggml.llm_quants, ggml.llm_model_path, + binary=lambda: self.conf["local_llm_binary"], + repos=ggml.llm_repos) orr_form.addRow(self.local_llm) self.local_llm_gpu = QCheckBox(t("Use the graphics card")) diff --git a/tests/test_ui.py b/tests/test_ui.py index eb751df..f541f32 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -1461,6 +1461,29 @@ class LocalModels(DikteTest): self.assertNotIn("Ready", box.status.text()) self.assertIn("program", box.status.text()) + def test_a_program_set_in_the_settings_is_not_called_downloaded(self): + mine = self.path("my-whisper-server") + mine.write_text("#!/bin/sh\n") + mine.chmod(0o755) + self.patch_attr(ggml.shutil, "which", lambda name: None) + box = self.window(self.config(local_binary=str(mine))).local_whisper + self.assertIn(str(mine), box.program_label.text()) + self.assertFalse(box.install_button.isVisibleTo(box)) + + def test_a_model_over_a_program_set_by_hand_is_ready(self): + # The program is there, it is just named by the settings rather than + # downloaded, and the status line looked past it. + mine = self.path("my-whisper-server") + mine.write_text("#!/bin/sh\n") + mine.chmod(0o755) + self.patch_attr(ggml.shutil, "which", lambda name: None) + path = ggml.whisper_model_path("ggml-small.bin") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"not really a model") + box = self.window(self.config(local_binary=str(mine))).local_whisper + box.load("ggml-small.bin") + self.assertIn("Ready", box.status.text()) + def test_changing_the_publisher_changes_the_model(self): # The model chosen under the old publisher is not published by the new # one. Carried over, it was added back as "not downloaded" and selected From afa53934c21c3fc3f4376e6e585f841132b0b4f6 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Wed, 9 Sep 2026 11:19:03 +0300 Subject: [PATCH 5/5] Test remembered wheel focus in inactive windows --- tests/test_ui.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_ui.py b/tests/test_ui.py index 038137f..a51d2f3 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -14,7 +14,7 @@ from unittest import mock from PyQt6.QtCore import QPoint, QPointF, Qt from PyQt6.QtGui import QWheelEvent -from PyQt6.QtWidgets import QApplication, QMessageBox +from PyQt6.QtWidgets import QApplication, QComboBox, QMessageBox, QSpinBox, QWidget from dikte import audio from dikte import cleanup @@ -223,6 +223,40 @@ class Settings(DikteTest): QApplication.sendEvent(box, self.wheel()) self.assertNotEqual(box.currentIndex(), before) + def test_the_wheel_uses_remembered_focus_in_an_inactive_window(self): + # Keep the window hidden so no desktop activation policy can give it + # keyboard focus. Its remembered focus still selects the wheel target. + for widget_type in (QComboBox, QSpinBox): + with self.subTest(widget=widget_type.__name__): + window = QWidget() + self.addCleanup(window.deleteLater) + box = widget_type(window) + other = QComboBox(window) + if isinstance(box, QComboBox): + box.addItems(["first", "second", "third"]) + box.setCurrentIndex(1) + value = box.currentIndex + else: + box.setValue(5) + value = box.value + box.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + guard = settings_ui.WheelGuard(window) + box.installEventFilter(guard) + box.setFocus() + self.assertFalse(window.isActiveWindow()) + self.assertFalse(box.hasFocus()) + self.assertIs(window.focusWidget(), box) + before = value() + QApplication.sendEvent(box, self.wheel()) + self.assertNotEqual(value(), before) + other.setFocus() + self.assertIs(window.focusWidget(), other) + before = value() + rolled = self.wheel() + QApplication.sendEvent(box, rolled) + self.assertEqual(value(), before) + self.assertFalse(rolled.isAccepted()) + def test_the_wheel_is_refused_when_another_widget_has_focus(self): window = self.window(cfg.Config()) box = window.ui_language