diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 19:17:22 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 19:17:22 +0200 |
| commit | 728d15912899d9c66838f0937931f61d75d217d4 (patch) | |
| tree | 6e218ba3adc540be47654f25695ad5aca0b0bb1f | |
| parent | 6ae195baa09bc217d51f3fb0a38312dfdcc85fdb (diff) | |
| download | llamachat-728d15912899d9c66838f0937931f61d75d217d4.tar.gz llamachat-728d15912899d9c66838f0937931f61d75d217d4.zip | |
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-08-25-skills-design.md | 5 | ||||
| -rw-r--r-- | llamachat/ui.py | 8 | ||||
| -rwxr-xr-x | test_llamachat.py | 64 |
5 files changed, 78 insertions, 14 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8fb42..c4f6473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,10 +76,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Skills. Instruction files read from `~/.agents/skills/*/SKILL.md` can be loaded into the model's context on demand: the model calls a `load_skill` tool, or you type `/skill-name` or mention a skill's name. Loaded skills - stay for the whole session in chat mode, drop after the reply in one-shot - mode, and show as removable chips. The backend tool loop now dispatches - between `web_search` and `load_skill`; `max_searches` caps total tool - rounds per turn. + show as removable chips and stay loaded for the whole session in chat + mode; one-shot mode keeps them in memory until New or the chip is clicked, + never in history. The backend tool loop now dispatches between `web_search` + and `load_skill`; `max_searches` caps total tool rounds per turn. ### Fixed @@ -46,7 +46,8 @@ persistent process and toggles like a scratchpad from a Hyprland keybind. loaded into the model's context on demand. The model can call a `load_skill` tool when a task matches one; you can type `/skill-name` or mention a skill's name. Loaded skills show as removable chips, stay for - the whole session in chat mode, and drop after the reply in one-shot mode. + the whole session in chat mode, and in one-shot mode stay in memory until + New or the chip is clicked (never written to history). - **Tray icon** for show/hide/quit, hosted by waybar's tray module. Not in this version: RAG or embedding search over history, multi-user @@ -491,7 +492,9 @@ A skill is loaded into the model's context on demand, three ways: - You mention a skill's name anywhere in the message. Loaded skills show as removable chips above the input. They stay loaded for -the whole session in chat mode and drop after the reply in one-shot mode. +the whole session in chat mode. One-shot mode keeps them in memory (never in +history) until New is pressed or the chip is clicked, so a skill loaded in a +scratch window stays active for the rest of it. `max_searches` caps the total tool rounds per turn here too, so skills load and searches share the same per-turn budget. diff --git a/docs/superpowers/specs/2026-08-25-skills-design.md b/docs/superpowers/specs/2026-08-25-skills-design.md index 20a3c0f..99baee6 100644 --- a/docs/superpowers/specs/2026-08-25-skills-design.md +++ b/docs/superpowers/specs/2026-08-25-skills-design.md @@ -13,8 +13,9 @@ with `name` and `description`, then the instructions. This adds a skills subsystem: llamachat reads that directory, lets the model load a skill on demand through a tool call, and lets the user load one by typing `/name` or mentioning the name naturally. A loaded skill's text stays -in context for the whole conversation in chat mode, and for the single reply -in one-shot mode. +in context for the whole conversation in chat mode. One-shot mode keeps it +in memory (never in history) until New or the chip is clicked, so a skill +loaded in a scratch window stays active for the rest of it. ## Skills directory and format diff --git a/llamachat/ui.py b/llamachat/ui.py index 58c0156..db705c3 100644 --- a/llamachat/ui.py +++ b/llamachat/ui.py @@ -1301,8 +1301,8 @@ class ChatWindow(QMainWindow): """Record a skill as loaded; True when it was not already present. Persisted to the session in chat mode so reopening the conversation - restores it. One-shot mode keeps the set in memory only; it is - cleared when the reply finishes. + restores it. One-shot mode keeps the set in memory only: the chip + stays until New is pressed or the chip is clicked. """ if not self.cfg.skills_enabled or name in self.loaded_skills: return False @@ -1590,10 +1590,6 @@ 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, diff --git a/test_llamachat.py b/test_llamachat.py index 71a2c35..8fb7a0e 100755 --- a/test_llamachat.py +++ b/test_llamachat.py @@ -3773,6 +3773,69 @@ def test_skill_parts(): print("ok skill parts") +def test_oneshot_skill_persists_until_new(): + """A loaded skill survives a one-shot reply and is cleared by New. + + One-shot used to drop skills when the reply finished, which made the + loaded-skill chip flash and vanish. The chip is the indicator that a + skill is active, so it must stay until the user clears it or starts a + new conversation. + """ + os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + from PySide6.QtWidgets import QApplication + + from llamachat import backend as _backend + from llamachat import config as _config + from llamachat import models as _models + from llamachat.ui import ChatWindow + + app = QApplication.instance() or QApplication([]) + + with tempfile.TemporaryDirectory() as tmp: + tmp = Path(tmp) + skill_dir = tmp / "skills" + (skill_dir / "handoff" / "SKILL.md").parent.mkdir(parents=True) + (skill_dir / "handoff" / "SKILL.md").write_text( + "---\nname: handoff\ndescription: prepare a session handoff.\n" + "---\n\n# handoff\nWrite the handoff.\n" + ) + + cfg = _config.load(tmp / "config.toml") + cfg.prompts_dir = tmp / "prompts" + cfg.db_path = tmp / "t.db" + cfg.skills_dir = skill_dir + cfg.skills_enabled = True + history = db.History(cfg.db_path) + client = _backend.MultiClient(cfg.providers, cfg.request_timeout) + presets = _config.parse_presets(cfg.presets_path) + store = _models.ModelStore(cfg.models_path) + window = ChatWindow(cfg, history, client, presets, store) + + window.session_id = history.create_session("oneshot", "m") + text = window._load_skills_from_text("prepare a handoff please") + assert text == "prepare a handoff please" + assert "handoff" in window.loaded_skills + assert window.skill_chips, "a loaded skill must show a chip" + + # A finished one-shot reply must keep the skill loaded until New. + window.assistant_message_id = history.add_message( + window.session_id, "assistant", "here is the handoff" + ) + window.assistant_buffer = "here is the handoff" + window._on_stream_finished() + assert "handoff" in window.loaded_skills, "one-shot must keep the skill" + assert window.skill_chips, "the chip must stay after a one-shot reply" + + # New drops the skill and the chip. + window.new_session() + assert window.loaded_skills == [] + assert not window.skill_chips + + window.close() + history.close() + print("ok oneshot skill persists until new") + + if __name__ == "__main__": test_presets() test_real_presets() @@ -3848,4 +3911,5 @@ if __name__ == "__main__": test_skills_db() test_skills_column_migration() test_skill_parts() + test_oneshot_skill_persists_until_new() print("\nall checks passed") |
