diff options
| -rw-r--r-- | llamachat/ui.py | 33 | ||||
| -rwxr-xr-x | test_llamachat.py | 4 |
2 files changed, 33 insertions, 4 deletions
diff --git a/llamachat/ui.py b/llamachat/ui.py index f29b39c..2d4ded5 100644 --- a/llamachat/ui.py +++ b/llamachat/ui.py @@ -260,6 +260,7 @@ class StreamWorker(QObject): usage_all = Signal(str) # every round's usage block, as a JSON array search_start = Signal(str) search_done = Signal(str) + skill_loaded = Signal(str) finished = Signal() failed = Signal(str) @@ -269,12 +270,14 @@ class StreamWorker(QObject): model: str, messages: list[dict], search_cfg: SearchConfig | None = None, + skills_cfg: backend.SkillsConfig | None = None, ): super().__init__() self.client = client self.model = model self.messages = messages self.search_cfg = search_cfg + self.skills_cfg = skills_cfg self._stop = False self._usage_blocks: list[str] = [] @@ -296,7 +299,7 @@ class StreamWorker(QObject): return try: stream = client.stream_chat( - wire, self.messages, self.search_cfg + wire, self.messages, self.search_cfg, self.skills_cfg ) for kind, piece in stream: if self._stop: @@ -307,6 +310,8 @@ class StreamWorker(QObject): self.search_start.emit(piece) elif kind == "search_done": self.search_done.emit(piece) + elif kind == "skill_loaded": + self.skill_loaded.emit(piece) elif kind == "usage": stats = json.loads(piece) # A searched turn makes several API calls (one per search @@ -603,6 +608,15 @@ class ChatWindow(QMainWindow): top.addWidget(self.search_box) outer.addLayout(top) + # Loaded-skills chips, hidden unless at least one skill is loaded. + self.skills_bar = QWidget() + self.skills_layout = QHBoxLayout(self.skills_bar) + self.skills_layout.setContentsMargins(8, 0, 8, 0) + self.skills_layout.setSpacing(6) + self.skill_chips: dict[str, QPushButton] = {} + self.skills_bar.setVisible(False) + outer.addWidget(self.skills_bar) + # Body: history list beside the transcript and input. self.splitter = QSplitter(Qt.Horizontal) splitter = self.splitter @@ -1298,6 +1312,12 @@ class ChatWindow(QMainWindow): self._refresh_skill_chips() return True + @Slot(str) + def _on_skill_loaded(self, name: str) -> None: + """A skill the model just loaded via its tool, recorded for later.""" + if self._load_skill(name): + self.show_status(f"Loaded skill: {name}") + def _unload_skill(self, name: str) -> None: if name not in self.loaded_skills: return @@ -1451,7 +1471,8 @@ class ChatWindow(QMainWindow): self.thread = QThread(self) self.worker = StreamWorker( - self.client, model, messages, self._search_config() + self.client, model, messages, + self._search_config(), self._skills_config(), ) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) @@ -1459,6 +1480,7 @@ class ChatWindow(QMainWindow): self.worker.reasoning.connect(self._on_reasoning) self.worker.search_start.connect(self._on_search_start) self.worker.search_done.connect(self._on_search_done) + self.worker.skill_loaded.connect(self._on_skill_loaded) self.worker.usage.connect(self._on_usage) self.worker.usage_all.connect(self._on_usage_all) self.worker.finished.connect(self._on_stream_finished) @@ -1567,6 +1589,10 @@ class ChatWindow(QMainWindow): @Slot() def _on_stream_finished(self) -> None: + # One-shot mode: a skill loaded mid-reply is gone when the reply ends. + if self.mode == MODE_ONESHOT: + self.loaded_skills = [] + self._refresh_skill_chips() if self.assistant_message_id is not None: self.history.update_message( self.assistant_message_id, @@ -1868,6 +1894,9 @@ class ChatWindow(QMainWindow): self.prompt_custom = _column(session, "prompt_custom") self.select_prompt(_column(session, "prompt_name")) + self.loaded_skills = _skills_list(_column(session, "skills")) + self._refresh_skill_chips() + self.oneshot_radio.blockSignals(True) self.chat_radio.blockSignals(True) self.oneshot_radio.setChecked(self.mode == MODE_ONESHOT) diff --git a/test_llamachat.py b/test_llamachat.py index 85e69a6..d96b599 100755 --- a/test_llamachat.py +++ b/test_llamachat.py @@ -2157,7 +2157,7 @@ def test_usage_accumulation(): def wire_name(self, model): return model - def stream_chat(self, wire, messages, search_cfg=None): + def stream_chat(self, wire, messages, search_cfg=None, skills_cfg=None): # One usage block per round: a tool round, then the final answer. yield ("usage", '{"prompt_tokens": 100, "completion_tokens": 5,' ' "total_tokens": 105}') @@ -2177,7 +2177,7 @@ def test_usage_accumulation(): # A turn with no usage chunk at all (no include_usage support) yields # an empty array, which the window stores as NULL. class SilentClient(FakeClient): - def stream_chat(self, wire, messages, search_cfg=None): + def stream_chat(self, wire, messages, search_cfg=None, skills_cfg=None): yield ("content", "hi") return |
