diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 72b2ada..2c891c2 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -569,6 +569,7 @@ class SettingsWindow(QDialog): _models_loaded = pyqtSignal(list, str, str) _transcribe_models_loaded = pyqtSignal(list, str) _codex_models_loaded = pyqtSignal(list) + _opencode_models_loaded = pyqtSignal(list) # Which key was tested, whether it worked, and what to write under it. _test_done = pyqtSignal(str, bool, str) # The release that was found, or None, and what went wrong instead. @@ -628,6 +629,7 @@ class SettingsWindow(QDialog): self._models_loaded.connect(self._on_models_loaded) self._transcribe_models_loaded.connect(self._on_transcribe_models_loaded) self._codex_models_loaded.connect(self._on_codex_models_loaded) + self._opencode_models_loaded.connect(self._on_opencode_models_loaded) self._test_done.connect(self._on_test_done) self._update_checked.connect(self._on_update_checked) self.transcriber.progress.connect(self._on_file_progress) @@ -639,6 +641,7 @@ class SettingsWindow(QDialog): self.meetings.failed.connect(self._on_minutes_failed) self._load() self._load_codex_models() + self._load_opencode_models() # Connected after the load, so that filling the boxes in is not taken # for the user ticking them. self.file_timestamps.toggled.connect(self._remember_file_choices) @@ -2080,6 +2083,37 @@ class SettingsWindow(QDialog): combo.addItem(name, name) combo.setCurrentText(current) + def _load_opencode_models(self): + """Ask OpenCode Go for its catalog of the day, off the interface thread. + + The same courtesy Codex gets: the built-in list is only a starting + point, so the boxes are refreshed from the source as the window opens. + Skipped without a key, so a machine that never touched OpenCode Go + sends it nothing; the Fetch button stays for a key typed in just now. + """ + key = self.conf.opencode_key() + if not key: + return + + def work(): + try: + found = api.openai_models(key, self.conf["opencode_base_url"], + "OpenCode Go") + except api.ApiError: + # The window is only opening; the Test button says what failed. + return + if found: + self._opencode_models_loaded.emit(found) + + threading.Thread(target=work, daemon=True).start() + + def _on_opencode_models_loaded(self, models): + for combo in (self.cleanup_opencode_model, self.assistant_opencode_model): + current = combo.currentText() + combo.clear() + combo.addItems(models) + combo.setCurrentText(current) + def _test_openai(self): key, base = self._typed_key("openai") self._test_key("openai", lambda: t( diff --git a/tests/test_ui.py b/tests/test_ui.py index 0ed35ab..4f59318 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -135,6 +135,8 @@ class Settings(DikteTest): "_load_transcribe_models")) self.enterContext(mock.patch.object(settings_ui.SettingsWindow, "_load_codex_models")) + self.enterContext(mock.patch.object(settings_ui.SettingsWindow, + "_load_opencode_models")) # The local model boxes fetch their own list the moment they are shown, # from a thread, which is nobody's test failing but a real request. self.enterContext(mock.patch.object(settings_ui.LocalModelBox, @@ -285,6 +287,20 @@ class Settings(DikteTest): self.assertEqual(window.cleanup_codex_model.currentText(), "my-own-model") + def test_opencode_answering_refills_both_of_its_boxes(self): + """The catalog fetched at open replaces the built-in list in the + cleanup and agent boxes alike, and neither loses what was picked.""" + conf = self.config(cleanup_opencode_model="my-own-model") + window = self.window(conf) + window._on_opencode_models_loaded(["glm-9", "kimi-k9"]) + for combo in (window.cleanup_opencode_model, + window.assistant_opencode_model): + with self.subTest(combo=combo.objectName() or "combo"): + offered = [combo.itemText(i) for i in range(combo.count())] + self.assertEqual(offered, ["glm-9", "kimi-k9"]) + self.assertEqual(window.cleanup_opencode_model.currentText(), + "my-own-model") + def test_a_fetched_opencode_list_lands_in_opencode_s_own_box(self): """The OpenRouter and meeting boxes are not refilled by another provider's catalog, and the picked model survives the refill.""" @@ -1026,7 +1042,9 @@ class MeetingSources(DikteTest): mock.patch.object(settings_ui.SettingsWindow, "_load_transcribe_models"), \ mock.patch.object(settings_ui.SettingsWindow, - "_load_codex_models"): + "_load_codex_models"), \ + mock.patch.object(settings_ui.SettingsWindow, + "_load_opencode_models"): window = settings_ui.SettingsWindow(cfg.Config()) self.addCleanup(window.deleteLater) self.addCleanup(window.close) @@ -1058,6 +1076,8 @@ class LocalModels(DikteTest): # And one with Codex on it would ask it for its model list. self.enterContext(mock.patch.object(settings_ui.SettingsWindow, "_load_codex_models")) + self.enterContext(mock.patch.object(settings_ui.SettingsWindow, + "_load_opencode_models")) def window(self, conf): window = settings_ui.SettingsWindow(conf)