Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to this project are documented in this file.
- **`mycat --version` and `mycat -v`.** They print the running version (for example `mycat 0.1.38`) and exit, and `mycat.__version__` now reports the same version as the startup log and the update check, also in a source checkout (PR #152).

### Fixed
- **The chat window comes back after Minimize.** Minimizing the chat panel left it miniaturized. It has no Dock icon of its own because it belongs to the cat window, and the Chat menu treated a minimized window as already open and hid it. The following show never cleared the minimized state, so the panel could not be opened again. Chat now restores it with `showNormal()`, and a minimized panel is no longer moved back beside the cat (branch `fix/chat-window-minimize`).
- **CI runs the tests again.** The workflow installed the newest pytest, and with pytest 9.2.0 every test run under `--forked` exited with status 3, so all 500 tests failed in every pull request while the code was fine (pytest 9.1.1 had passed them on 2026-10-19). `ci.yml` now installs `pytest<9.2` until pytest-forked works with it (branch `fix/ci-pytest-pin`).

## [0.1.38] - 2026-10-19
Expand Down
39 changes: 29 additions & 10 deletions mycat/llm_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,23 @@ def toggle_chat(self) -> None:
logger.debug("LLM chat is disabled, ignoring toggle")
return
if self.chat_dialog:
if self.chat_dialog.isVisible():
# A minimized window still reports isVisible() on macOS, and show()
# does not clear WindowMinimized. Treating that as "open" hides it,
# and the next show leaves it miniaturized with no Dock tile of its
# own (the dialog is parented to the cat), so it never returns.
if self.chat_dialog.isVisible() and not self.chat_dialog.isMinimized():
logger.debug("Hiding chat dialog")
self.chat_dialog.hide()
return
logger.debug("Showing existing chat dialog")
self.chat_dialog.show()
self.position_dialog()
self.chat_dialog.raise_()
self.chat_dialog.activateWindow()
self.present_chat()
return

logger.debug("Creating new chat dialog")
dialog = ChatDialog(self)
dialog.show()
dialog.destroyed.connect(self.on_chat_destroyed)
self.chat_dialog = dialog
self.position_dialog()
self.present_chat()

def is_enabled(self) -> bool:
return self.enabled
Expand All @@ -88,12 +88,24 @@ def toggle_enabled(self) -> bool:
logger.info("LLM chat %s", "enabled" if self.enabled else "disabled")
return self.enabled

def present_chat(self) -> None:
"""Show the dialog and clear a stuck minimized state."""
dialog = self.chat_dialog
if dialog is None:
return
dialog.showNormal()
self.position_dialog()
dialog.raise_()
dialog.activateWindow()

def position_dialog(self) -> None:
if not self.chat_dialog:
dialog = self.chat_dialog
# Moving a miniaturized window repositions it while Cocoa still treats
# it as minimized, which is what strands the panel.
if dialog is None or not dialog.isVisible() or dialog.isMinimized():
return
screen_rect = QtGui.QGuiApplication.primaryScreen().availableGeometry()
cat_rect = self.window.geometry()
dialog = self.chat_dialog

dialog_width = dialog.width()
dialog_height = dialog.height()
Expand Down Expand Up @@ -256,11 +268,18 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None:

def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
super().resizeEvent(event)
if not getattr(self, "suspend_anchor", False):
if not self.isMinimized() and not getattr(self, "suspend_anchor", False):
self.controller.position_dialog()

def moveEvent(self, event: QtGui.QMoveEvent) -> None:
super().moveEvent(event)
if not self.isMinimized() and not getattr(self, "suspend_anchor", False):
self.controller.position_dialog()

def changeEvent(self, event: QtCore.QEvent) -> None:
super().changeEvent(event)
if event.type() != QtCore.QEvent.Type.WindowStateChange or self.isMinimized():
return
if not getattr(self, "suspend_anchor", False):
self.controller.position_dialog()

Expand Down
66 changes: 66 additions & 0 deletions tests/test_llm_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Chat window show / minimize behavior."""

from PySide6 import QtCore, QtWidgets

from mycat import llm_prompt, llm_ui
from mycat.llm import LLMContext


class DummyBackend:
def reply(self, user_text: str, system_prompt: str) -> str:
return "ok"


def _attach(qapp, tmp_path, monkeypatch):
history = tmp_path / "history.txt"
history.write_text("", encoding="utf-8")
monkeypatch.setattr(llm_prompt, "ensure_history_file", lambda: history)

host = QtWidgets.QWidget()
host.setGeometry(200, 200, 80, 80)
host.show()
context = LLMContext(
backend_name="ollama",
backend=DummyBackend(),
settings=llm_prompt.LLMSettings(None, "m", "http://127.0.0.1:11434", "m", 5.0, 8),
enabled=True,
)
llm_ui.attach_chat(host, context, enabled=True)
qapp.processEvents()
return host


def test_toggle_restores_a_minimized_chat(qapp, tmp_path, monkeypatch):
host = _attach(qapp, tmp_path, monkeypatch)
host.toggle_llm_chat()
qapp.processEvents()
dialog = host.llm_controller.chat_dialog
assert dialog is not None and dialog.isVisible()

dialog.setWindowState(QtCore.Qt.WindowState.WindowMinimized)
qapp.processEvents()
assert dialog.isMinimized()

host.toggle_llm_chat()
qapp.processEvents()
assert dialog.isVisible()
assert not dialog.isMinimized()

host.toggle_llm_chat()
qapp.processEvents()
assert not dialog.isVisible()


def test_minimized_chat_is_not_dragged_back_onscreen(qapp, tmp_path, monkeypatch):
host = _attach(qapp, tmp_path, monkeypatch)
host.toggle_llm_chat()
qapp.processEvents()
dialog = host.llm_controller.chat_dialog
dialog.setWindowState(QtCore.Qt.WindowState.WindowMinimized)
qapp.processEvents()

calls: list[str] = []
host.llm_controller.position_dialog = lambda: calls.append("pos")
dialog.move(dialog.x() + 40, dialog.y() + 40)
qapp.processEvents()
assert calls == []