Merge pull request #16625 from NousResearch/bb/fix-tui-title-session-sync
fix(tui): keep /title session names in sync
This commit is contained in:
@@ -3294,6 +3294,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
chat_topic = self._get_effective_topic(message.channel, is_thread=is_thread)
|
chat_topic = self._get_effective_topic(message.channel, is_thread=is_thread)
|
||||||
|
|
||||||
# Build source
|
# Build source
|
||||||
|
guild = getattr(message, "guild", None)
|
||||||
source = self.build_source(
|
source = self.build_source(
|
||||||
chat_id=str(effective_channel.id),
|
chat_id=str(effective_channel.id),
|
||||||
chat_name=chat_name,
|
chat_name=chat_name,
|
||||||
@@ -3303,7 +3304,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
chat_topic=chat_topic,
|
chat_topic=chat_topic,
|
||||||
is_bot=getattr(message.author, "bot", False),
|
is_bot=getattr(message.author, "bot", False),
|
||||||
guild_id=str(message.guild.id) if message.guild else None,
|
guild_id=str(guild.id) if guild else None,
|
||||||
parent_chat_id=parent_channel_id,
|
parent_chat_id=parent_channel_id,
|
||||||
message_id=str(message.id),
|
message_id=str(message.id),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pkgs.buildNpmPackage (npm // {
|
|||||||
inherit src npmDeps version;
|
inherit src npmDeps version;
|
||||||
|
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
|
npmFlags = [ "--legacy-peer-deps" ];
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|||||||
@@ -100,20 +100,36 @@ def test_session_resume_uses_parent_lineage_for_display(monkeypatch):
|
|||||||
|
|
||||||
def get_messages_as_conversation(self, target, include_ancestors=False):
|
def get_messages_as_conversation(self, target, include_ancestors=False):
|
||||||
captured.setdefault("history_calls", []).append((target, include_ancestors))
|
captured.setdefault("history_calls", []).append((target, include_ancestors))
|
||||||
return [
|
return (
|
||||||
{"role": "user", "content": "root prompt"},
|
[
|
||||||
{"role": "assistant", "content": "root answer"},
|
{"role": "user", "content": "root prompt"},
|
||||||
] if include_ancestors else [{"role": "user", "content": "tip prompt"}]
|
{"role": "assistant", "content": "root answer"},
|
||||||
|
]
|
||||||
|
if include_ancestors
|
||||||
|
else [{"role": "user", "content": "tip prompt"}]
|
||||||
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(server, "_get_db", lambda: FakeDB())
|
monkeypatch.setattr(server, "_get_db", lambda: FakeDB())
|
||||||
monkeypatch.setattr(server, "_enable_gateway_prompts", lambda: None)
|
monkeypatch.setattr(server, "_enable_gateway_prompts", lambda: None)
|
||||||
monkeypatch.setattr(server, "_set_session_context", lambda target: [])
|
monkeypatch.setattr(server, "_set_session_context", lambda target: [])
|
||||||
monkeypatch.setattr(server, "_clear_session_context", lambda tokens: None)
|
monkeypatch.setattr(server, "_clear_session_context", lambda tokens: None)
|
||||||
monkeypatch.setattr(server, "_make_agent", lambda *args, **kwargs: types.SimpleNamespace(model="test"))
|
monkeypatch.setattr(
|
||||||
monkeypatch.setattr(server, "_session_info", lambda agent: {"model": "test", "tools": {}, "skills": {}})
|
server,
|
||||||
monkeypatch.setattr(server, "_init_session", lambda sid, key, agent, history, cols=80: None)
|
"_make_agent",
|
||||||
|
lambda *args, **kwargs: types.SimpleNamespace(model="test"),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
server,
|
||||||
|
"_session_info",
|
||||||
|
lambda agent: {"model": "test", "tools": {}, "skills": {}},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
server, "_init_session", lambda sid, key, agent, history, cols=80: None
|
||||||
|
)
|
||||||
|
|
||||||
resp = server.handle_request({"id": "1", "method": "session.resume", "params": {"session_id": "tip"}})
|
resp = server.handle_request(
|
||||||
|
{"id": "1", "method": "session.resume", "params": {"session_id": "tip"}}
|
||||||
|
)
|
||||||
|
|
||||||
assert resp["result"]["messages"] == [
|
assert resp["result"]["messages"] == [
|
||||||
{"role": "user", "text": "root prompt"},
|
{"role": "user", "text": "root prompt"},
|
||||||
@@ -258,6 +274,307 @@ def _session(agent=None, **extra):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_queues_when_db_row_not_ready(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_session(self, _key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_session_title(self, _key, _title):
|
||||||
|
return False
|
||||||
|
|
||||||
|
server._sessions["sid"] = _session(pending_title=None)
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||||
|
try:
|
||||||
|
set_resp = server.handle_request(
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"method": "session.title",
|
||||||
|
"params": {"session_id": "sid", "title": "queued title"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert set_resp["result"]["pending"] is True
|
||||||
|
assert set_resp["result"]["title"] == "queued title"
|
||||||
|
assert server._sessions["sid"]["pending_title"] == "queued title"
|
||||||
|
|
||||||
|
get_resp = server.handle_request(
|
||||||
|
{"id": "2", "method": "session.title", "params": {"session_id": "sid"}}
|
||||||
|
)
|
||||||
|
assert get_resp["result"]["title"] == "queued title"
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_clears_pending_after_persist(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def __init__(self):
|
||||||
|
self.title = "old"
|
||||||
|
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_session(self, _key):
|
||||||
|
return {"id": _key, "title": self.title}
|
||||||
|
|
||||||
|
def set_session_title(self, _key, title):
|
||||||
|
self.title = title
|
||||||
|
return True
|
||||||
|
|
||||||
|
db = _FakeDB()
|
||||||
|
server._sessions["sid"] = _session(pending_title="stale")
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: db)
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"method": "session.title",
|
||||||
|
"params": {"session_id": "sid", "title": "fresh"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp["result"]["pending"] is False
|
||||||
|
assert resp["result"]["title"] == "fresh"
|
||||||
|
assert server._sessions["sid"]["pending_title"] is None
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_does_not_queue_noop_when_row_exists(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def __init__(self):
|
||||||
|
self.title = "same title"
|
||||||
|
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_session(self, _key):
|
||||||
|
return {"id": _key, "title": self.title}
|
||||||
|
|
||||||
|
def set_session_title(self, _key, _title):
|
||||||
|
# Simulate sqlite UPDATE rowcount==0 for no-op update.
|
||||||
|
return False
|
||||||
|
|
||||||
|
server._sessions["sid"] = _session(pending_title="stale")
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"method": "session.title",
|
||||||
|
"params": {"session_id": "sid", "title": "same title"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp["result"]["pending"] is False
|
||||||
|
assert resp["result"]["title"] == "same title"
|
||||||
|
assert server._sessions["sid"]["pending_title"] is None
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_get_falls_back_to_pending_when_db_read_throws(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
raise RuntimeError("db temporarily locked")
|
||||||
|
|
||||||
|
server._sessions["sid"] = _session(pending_title="queued title")
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{"id": "1", "method": "session.title", "params": {"session_id": "sid"}}
|
||||||
|
)
|
||||||
|
assert resp["result"]["title"] == "queued title"
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_get_retries_persist_for_pending_title(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def __init__(self):
|
||||||
|
self.title = ""
|
||||||
|
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def set_session_title(self, _key, title):
|
||||||
|
self.title = title
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_session(self, _key):
|
||||||
|
return {"id": _key, "title": self.title}
|
||||||
|
|
||||||
|
db = _FakeDB()
|
||||||
|
server._sessions["sid"] = _session(pending_title="queued title")
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: db)
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{"id": "1", "method": "session.title", "params": {"session_id": "sid"}}
|
||||||
|
)
|
||||||
|
assert resp["result"]["title"] == "queued title"
|
||||||
|
assert server._sessions["sid"]["pending_title"] is None
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_get_retries_pending_even_when_db_has_title(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def __init__(self):
|
||||||
|
self.title = "auto title"
|
||||||
|
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def set_session_title(self, _key, title):
|
||||||
|
self.title = title
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_session(self, _key):
|
||||||
|
return {"id": _key, "title": self.title}
|
||||||
|
|
||||||
|
db = _FakeDB()
|
||||||
|
server._sessions["sid"] = _session(pending_title="queued title")
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: db)
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{"id": "1", "method": "session.title", "params": {"session_id": "sid"}}
|
||||||
|
)
|
||||||
|
assert resp["result"]["title"] == "queued title"
|
||||||
|
assert server._sessions["sid"]["pending_title"] is None
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_rejects_empty_title_with_specific_error_code(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
server._sessions["sid"] = _session()
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"method": "session.title",
|
||||||
|
"params": {"session_id": "sid", "title": " "},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert "error" in resp
|
||||||
|
assert resp["error"]["code"] == 4021
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_set_maps_valueerror_to_user_error(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_session(self, _key):
|
||||||
|
return {"id": _key}
|
||||||
|
|
||||||
|
def set_session_title(self, _key, _title):
|
||||||
|
raise ValueError("Title already in use")
|
||||||
|
|
||||||
|
server._sessions["sid"] = _session()
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"method": "session.title",
|
||||||
|
"params": {"session_id": "sid", "title": "dup"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert "error" in resp
|
||||||
|
assert resp["error"]["code"] == 4022
|
||||||
|
assert "already in use" in resp["error"]["message"]
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_title_set_errors_when_row_lookup_fails_after_noop(monkeypatch):
|
||||||
|
class _FakeDB:
|
||||||
|
def get_session_title(self, _key):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_session(self, _key):
|
||||||
|
raise RuntimeError("row lookup failed")
|
||||||
|
|
||||||
|
def set_session_title(self, _key, _title):
|
||||||
|
return False
|
||||||
|
|
||||||
|
server._sessions["sid"] = _session()
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||||
|
try:
|
||||||
|
resp = server.handle_request(
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"method": "session.title",
|
||||||
|
"params": {"session_id": "sid", "title": "fresh"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert "error" in resp
|
||||||
|
assert resp["error"]["code"] == 5007
|
||||||
|
assert "row lookup failed" in resp["error"]["message"]
|
||||||
|
finally:
|
||||||
|
server._sessions.pop("sid", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_session_create_drops_pending_title_on_valueerror(monkeypatch):
|
||||||
|
unblock_agent = threading.Event()
|
||||||
|
|
||||||
|
class _FakeWorker:
|
||||||
|
def __init__(self, key, model):
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
class _FakeAgent:
|
||||||
|
model = "x"
|
||||||
|
provider = "openrouter"
|
||||||
|
base_url = ""
|
||||||
|
api_key = ""
|
||||||
|
|
||||||
|
class _FakeDB:
|
||||||
|
def create_session(self, _key, source="tui", model=None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_session_title(self, _key, _title):
|
||||||
|
raise ValueError("Title already in use")
|
||||||
|
|
||||||
|
def _make_agent(_sid, _key):
|
||||||
|
unblock_agent.wait(timeout=2.0)
|
||||||
|
return _FakeAgent()
|
||||||
|
|
||||||
|
monkeypatch.setattr(server, "_make_agent", _make_agent)
|
||||||
|
monkeypatch.setattr(server, "_SlashWorker", _FakeWorker)
|
||||||
|
monkeypatch.setattr(server, "_get_db", lambda: _FakeDB())
|
||||||
|
monkeypatch.setattr(server, "_session_info", lambda _a: {"model": "x"})
|
||||||
|
monkeypatch.setattr(server, "_probe_credentials", lambda _a: None)
|
||||||
|
monkeypatch.setattr(server, "_wire_callbacks", lambda _sid: None)
|
||||||
|
monkeypatch.setattr(server, "_emit", lambda *a, **kw: None)
|
||||||
|
|
||||||
|
import tools.approval as _approval
|
||||||
|
|
||||||
|
monkeypatch.setattr(_approval, "register_gateway_notify", lambda key, cb: None)
|
||||||
|
monkeypatch.setattr(_approval, "load_permanent_allowlist", lambda: None)
|
||||||
|
|
||||||
|
resp = server.handle_request({"id": "1", "method": "session.create", "params": {"cols": 80}})
|
||||||
|
sid = resp["result"]["session_id"]
|
||||||
|
session = server._sessions[sid]
|
||||||
|
session["pending_title"] = "duplicate title"
|
||||||
|
unblock_agent.set()
|
||||||
|
session["agent_ready"].wait(timeout=2.0)
|
||||||
|
|
||||||
|
assert session["pending_title"] is None
|
||||||
|
server._sessions.pop(sid, None)
|
||||||
|
|
||||||
|
|
||||||
def test_config_set_yolo_toggles_session_scope():
|
def test_config_set_yolo_toggles_session_scope():
|
||||||
from tools.approval import clear_session, is_session_yolo_enabled
|
from tools.approval import clear_session, is_session_yolo_enabled
|
||||||
|
|
||||||
@@ -1798,6 +2115,7 @@ def test_session_create_continues_when_state_db_is_unavailable(monkeypatch):
|
|||||||
monkeypatch.setattr(server, "_emit", lambda *a, **kw: emits.append(a))
|
monkeypatch.setattr(server, "_emit", lambda *a, **kw: emits.append(a))
|
||||||
|
|
||||||
import tools.approval as _approval
|
import tools.approval as _approval
|
||||||
|
|
||||||
monkeypatch.setattr(_approval, "register_gateway_notify", lambda key, cb: None)
|
monkeypatch.setattr(_approval, "register_gateway_notify", lambda key, cb: None)
|
||||||
monkeypatch.setattr(_approval, "load_permanent_allowlist", lambda: None)
|
monkeypatch.setattr(_approval, "load_permanent_allowlist", lambda: None)
|
||||||
|
|
||||||
@@ -1905,6 +2223,7 @@ def test_model_options_propagates_list_exception(monkeypatch):
|
|||||||
# prompt.submit — auto-title
|
# prompt.submit — auto-title
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class _ImmediateThread:
|
class _ImmediateThread:
|
||||||
"""Runs the target callable synchronously so assertions can follow."""
|
"""Runs the target callable synchronously so assertions can follow."""
|
||||||
|
|
||||||
@@ -1919,7 +2238,9 @@ def test_prompt_submit_auto_titles_session_on_complete(monkeypatch):
|
|||||||
"""maybe_auto_title is called after a successful (complete) prompt."""
|
"""maybe_auto_title is called after a successful (complete) prompt."""
|
||||||
|
|
||||||
class _Agent:
|
class _Agent:
|
||||||
def run_conversation(self, prompt, conversation_history=None, stream_callback=None):
|
def run_conversation(
|
||||||
|
self, prompt, conversation_history=None, stream_callback=None
|
||||||
|
):
|
||||||
return {
|
return {
|
||||||
"final_response": "Rome was founded in 753 BC.",
|
"final_response": "Rome was founded in 753 BC.",
|
||||||
"messages": [
|
"messages": [
|
||||||
@@ -1955,7 +2276,9 @@ def test_prompt_submit_skips_auto_title_when_interrupted(monkeypatch):
|
|||||||
"""maybe_auto_title must NOT be called when the agent was interrupted."""
|
"""maybe_auto_title must NOT be called when the agent was interrupted."""
|
||||||
|
|
||||||
class _Agent:
|
class _Agent:
|
||||||
def run_conversation(self, prompt, conversation_history=None, stream_callback=None):
|
def run_conversation(
|
||||||
|
self, prompt, conversation_history=None, stream_callback=None
|
||||||
|
):
|
||||||
return {
|
return {
|
||||||
"final_response": "partial answer",
|
"final_response": "partial answer",
|
||||||
"interrupted": True,
|
"interrupted": True,
|
||||||
@@ -1985,7 +2308,9 @@ def test_prompt_submit_skips_auto_title_when_response_empty(monkeypatch):
|
|||||||
"""maybe_auto_title must NOT be called when the agent returns an empty reply."""
|
"""maybe_auto_title must NOT be called when the agent returns an empty reply."""
|
||||||
|
|
||||||
class _Agent:
|
class _Agent:
|
||||||
def run_conversation(self, prompt, conversation_history=None, stream_callback=None):
|
def run_conversation(
|
||||||
|
self, prompt, conversation_history=None, stream_callback=None
|
||||||
|
):
|
||||||
return {
|
return {
|
||||||
"final_response": "",
|
"final_response": "",
|
||||||
"messages": [],
|
"messages": [],
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ def _dockerfile_instructions(dockerfile_text: str) -> list[str]:
|
|||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
current = f"{current} {line.removesuffix('\\').strip()}".strip()
|
continued = line.removesuffix("\\").strip()
|
||||||
|
current = f"{current} {continued}".strip()
|
||||||
if not line.endswith("\\"):
|
if not line.endswith("\\"):
|
||||||
instructions.append(current)
|
instructions.append(current)
|
||||||
current = ""
|
current = ""
|
||||||
|
|||||||
@@ -759,8 +759,11 @@ def _apply_model_switch(sid: str, session: dict, raw_input: str) -> dict:
|
|||||||
custom_provs = None
|
custom_provs = None
|
||||||
try:
|
try:
|
||||||
from hermes_cli.config import get_compatible_custom_providers, load_config
|
from hermes_cli.config import get_compatible_custom_providers, load_config
|
||||||
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
user_provs = [{"provider": k, **v} for k, v in (cfg.get("providers") or {}).items()]
|
user_provs = [
|
||||||
|
{"provider": k, **v} for k, v in (cfg.get("providers") or {}).items()
|
||||||
|
]
|
||||||
custom_provs = get_compatible_custom_providers(cfg)
|
custom_provs = get_compatible_custom_providers(cfg)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -918,7 +921,10 @@ def _probe_config_health(cfg: dict) -> str:
|
|||||||
def _session_info(agent) -> dict:
|
def _session_info(agent) -> dict:
|
||||||
reasoning_config = getattr(agent, "reasoning_config", None)
|
reasoning_config = getattr(agent, "reasoning_config", None)
|
||||||
reasoning_effort = ""
|
reasoning_effort = ""
|
||||||
if isinstance(reasoning_config, dict) and reasoning_config.get("enabled") is not False:
|
if (
|
||||||
|
isinstance(reasoning_config, dict)
|
||||||
|
and reasoning_config.get("enabled") is not False
|
||||||
|
):
|
||||||
reasoning_effort = str(reasoning_config.get("effort", "") or "")
|
reasoning_effort = str(reasoning_config.get("effort", "") or "")
|
||||||
service_tier = getattr(agent, "service_tier", None) or ""
|
service_tier = getattr(agent, "service_tier", None) or ""
|
||||||
info: dict = {
|
info: dict = {
|
||||||
@@ -1042,7 +1048,11 @@ def _on_tool_start(sid: str, tool_call_id: str, name: str, args: dict):
|
|||||||
if _tool_progress_enabled(sid):
|
if _tool_progress_enabled(sid):
|
||||||
# tool.complete is the source of truth for todos (full list from the
|
# tool.complete is the source of truth for todos (full list from the
|
||||||
# tool result). args.todos here may be a partial merge update.
|
# tool result). args.todos here may be a partial merge update.
|
||||||
_emit("tool.start", sid, {"tool_id": tool_call_id, "name": name, "context": _tool_ctx(name, args)})
|
_emit(
|
||||||
|
"tool.start",
|
||||||
|
sid,
|
||||||
|
{"tool_id": tool_call_id, "name": name, "context": _tool_ctx(name, args)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result: str):
|
def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result: str):
|
||||||
@@ -1530,6 +1540,7 @@ def _(rid, params: dict) -> dict:
|
|||||||
"history_lock": threading.Lock(),
|
"history_lock": threading.Lock(),
|
||||||
"history_version": 0,
|
"history_version": 0,
|
||||||
"image_counter": 0,
|
"image_counter": 0,
|
||||||
|
"pending_title": None,
|
||||||
"running": False,
|
"running": False,
|
||||||
"session_key": key,
|
"session_key": key,
|
||||||
"show_reasoning": _load_show_reasoning(),
|
"show_reasoning": _load_show_reasoning(),
|
||||||
@@ -1567,6 +1578,42 @@ def _(rid, params: dict) -> dict:
|
|||||||
db = _get_db()
|
db = _get_db()
|
||||||
if db is not None:
|
if db is not None:
|
||||||
db.create_session(key, source="tui", model=_resolve_model())
|
db.create_session(key, source="tui", model=_resolve_model())
|
||||||
|
pending_title = (session.get("pending_title") or "").strip()
|
||||||
|
if pending_title:
|
||||||
|
try:
|
||||||
|
title_applied = db.set_session_title(key, pending_title)
|
||||||
|
if title_applied:
|
||||||
|
session["pending_title"] = None
|
||||||
|
else:
|
||||||
|
existing_row = db.get_session(key)
|
||||||
|
existing_title = (
|
||||||
|
(existing_row or {}).get("title") or ""
|
||||||
|
).strip()
|
||||||
|
if existing_title == pending_title:
|
||||||
|
session["pending_title"] = None
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Pending title still queued for session %s (wanted=%r, current=%r)",
|
||||||
|
sid,
|
||||||
|
pending_title,
|
||||||
|
existing_title,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
# Queued title can become invalid/duplicate between queue time
|
||||||
|
# and DB row creation. Drop the queue and log the reason so
|
||||||
|
# future /title reads don't surface a stuck pending value.
|
||||||
|
session["pending_title"] = None
|
||||||
|
logger.info(
|
||||||
|
"Dropping pending title for session %s: %s",
|
||||||
|
sid,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to apply pending title for session %s",
|
||||||
|
sid,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
session["agent"] = agent
|
session["agent"] = agent
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1706,7 +1753,9 @@ def _(rid, params: dict) -> dict:
|
|||||||
try:
|
try:
|
||||||
db.reopen_session(target)
|
db.reopen_session(target)
|
||||||
history = db.get_messages_as_conversation(target)
|
history = db.get_messages_as_conversation(target)
|
||||||
display_history = db.get_messages_as_conversation(target, include_ancestors=True)
|
display_history = db.get_messages_as_conversation(
|
||||||
|
target, include_ancestors=True
|
||||||
|
)
|
||||||
messages = _history_to_messages(display_history)
|
messages = _history_to_messages(display_history)
|
||||||
tokens = _set_session_context(target)
|
tokens = _set_session_context(target)
|
||||||
try:
|
try:
|
||||||
@@ -1736,12 +1785,57 @@ def _(rid, params: dict) -> dict:
|
|||||||
db = _get_db()
|
db = _get_db()
|
||||||
if db is None:
|
if db is None:
|
||||||
return _db_unavailable_error(rid, code=5007)
|
return _db_unavailable_error(rid, code=5007)
|
||||||
title, key = params.get("title", ""), session["session_key"]
|
key = session["session_key"]
|
||||||
|
if "title" not in params:
|
||||||
|
fallback = session.get("pending_title") or ""
|
||||||
|
try:
|
||||||
|
resolved_title = db.get_session_title(key) or ""
|
||||||
|
if fallback:
|
||||||
|
if db.set_session_title(key, fallback):
|
||||||
|
session["pending_title"] = None
|
||||||
|
resolved_title = fallback
|
||||||
|
else:
|
||||||
|
existing_row = db.get_session(key)
|
||||||
|
existing_title = ((existing_row or {}).get("title") or "").strip()
|
||||||
|
if existing_title == fallback:
|
||||||
|
session["pending_title"] = None
|
||||||
|
resolved_title = fallback
|
||||||
|
elif not resolved_title:
|
||||||
|
resolved_title = fallback
|
||||||
|
elif resolved_title:
|
||||||
|
session["pending_title"] = None
|
||||||
|
except Exception:
|
||||||
|
resolved_title = fallback
|
||||||
|
return _ok(
|
||||||
|
rid,
|
||||||
|
{
|
||||||
|
"title": resolved_title,
|
||||||
|
"session_key": key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
title = (params.get("title", "") or "").strip()
|
||||||
if not title:
|
if not title:
|
||||||
return _ok(rid, {"title": db.get_session_title(key) or "", "session_key": key})
|
return _err(rid, 4021, "title required")
|
||||||
try:
|
try:
|
||||||
db.set_session_title(key, title)
|
if db.set_session_title(key, title):
|
||||||
return _ok(rid, {"title": title})
|
session["pending_title"] = None
|
||||||
|
return _ok(rid, {"pending": False, "title": title})
|
||||||
|
# rowcount == 0 can mean "same value" as well as "missing row".
|
||||||
|
# Queue only when the session row truly does not exist yet.
|
||||||
|
existing_row = db.get_session(key)
|
||||||
|
if existing_row:
|
||||||
|
session["pending_title"] = None
|
||||||
|
return _ok(
|
||||||
|
rid,
|
||||||
|
{
|
||||||
|
"pending": False,
|
||||||
|
"title": (existing_row.get("title") or title),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
session["pending_title"] = title
|
||||||
|
return _ok(rid, {"pending": True, "title": title})
|
||||||
|
except ValueError as e:
|
||||||
|
return _err(rid, 4022, str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _err(rid, 5007, str(e))
|
return _err(rid, 5007, str(e))
|
||||||
|
|
||||||
@@ -1761,7 +1855,9 @@ def _(rid, params: dict) -> dict:
|
|||||||
db = _get_db()
|
db = _get_db()
|
||||||
if db is not None and session.get("session_key"):
|
if db is not None and session.get("session_key"):
|
||||||
try:
|
try:
|
||||||
history = db.get_messages_as_conversation(session["session_key"], include_ancestors=True)
|
history = db.get_messages_as_conversation(
|
||||||
|
session["session_key"], include_ancestors=True
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return _ok(
|
return _ok(
|
||||||
@@ -2899,7 +2995,11 @@ def _(rid, params: dict) -> dict:
|
|||||||
|
|
||||||
if key == "mouse":
|
if key == "mouse":
|
||||||
raw = str(value or "").strip().lower()
|
raw = str(value or "").strip().lower()
|
||||||
display = _load_cfg().get("display") if isinstance(_load_cfg().get("display"), dict) else {}
|
display = (
|
||||||
|
_load_cfg().get("display")
|
||||||
|
if isinstance(_load_cfg().get("display"), dict)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
current = bool(display.get("tui_mouse", True))
|
current = bool(display.get("tui_mouse", True))
|
||||||
|
|
||||||
if raw in ("", "toggle"):
|
if raw in ("", "toggle"):
|
||||||
@@ -3763,7 +3863,9 @@ def _details_completion_item(value: str, meta: str = "") -> dict:
|
|||||||
return {"text": value, "display": value, "meta": meta}
|
return {"text": value, "display": value, "meta": meta}
|
||||||
|
|
||||||
|
|
||||||
def _details_root_completion_item(value: str, meta: str, needs_leading_space: bool) -> dict:
|
def _details_root_completion_item(
|
||||||
|
value: str, meta: str, needs_leading_space: bool
|
||||||
|
) -> dict:
|
||||||
return _details_completion_item(
|
return _details_completion_item(
|
||||||
f" {value}" if needs_leading_space else value,
|
f" {value}" if needs_leading_space else value,
|
||||||
meta,
|
meta,
|
||||||
@@ -3778,7 +3880,7 @@ def _details_completions(text: str) -> list[dict] | None:
|
|||||||
if stripped and not "/details".startswith(stripped.lower().split()[0]):
|
if stripped and not "/details".startswith(stripped.lower().split()[0]):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
body = text[len("/details"):]
|
body = text[len("/details") :]
|
||||||
if body.startswith(" "):
|
if body.startswith(" "):
|
||||||
body = body[1:]
|
body = body[1:]
|
||||||
parts = body.split()
|
parts = body.split()
|
||||||
@@ -3789,12 +3891,18 @@ def _details_completions(text: str) -> list[dict] | None:
|
|||||||
if not body or (len(parts) == 0 and has_trailing_space):
|
if not body or (len(parts) == 0 and has_trailing_space):
|
||||||
return [
|
return [
|
||||||
*[
|
*[
|
||||||
_details_root_completion_item(mode, "global mode", not has_trailing_space)
|
_details_root_completion_item(
|
||||||
|
mode, "global mode", not has_trailing_space
|
||||||
|
)
|
||||||
for mode in modes
|
for mode in modes
|
||||||
],
|
],
|
||||||
_details_root_completion_item("cycle", "cycle global mode", not has_trailing_space),
|
_details_root_completion_item(
|
||||||
|
"cycle", "cycle global mode", not has_trailing_space
|
||||||
|
),
|
||||||
*[
|
*[
|
||||||
_details_root_completion_item(section, "section override", not has_trailing_space)
|
_details_root_completion_item(
|
||||||
|
section, "section override", not has_trailing_space
|
||||||
|
)
|
||||||
for section in sections
|
for section in sections
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@@ -3808,9 +3916,7 @@ def _details_completions(text: str) -> list[dict] | None:
|
|||||||
(
|
(
|
||||||
"section override"
|
"section override"
|
||||||
if candidate in sections
|
if candidate in sections
|
||||||
else "cycle global mode"
|
else "cycle global mode" if candidate == "cycle" else "global mode"
|
||||||
if candidate == "cycle"
|
|
||||||
else "global mode"
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for candidate in candidates
|
for candidate in candidates
|
||||||
@@ -3819,7 +3925,10 @@ def _details_completions(text: str) -> list[dict] | None:
|
|||||||
|
|
||||||
if len(parts) == 1 and has_trailing_space and parts[0].lower() in sections:
|
if len(parts) == 1 and has_trailing_space and parts[0].lower() in sections:
|
||||||
return [
|
return [
|
||||||
*[_details_completion_item(mode, f"set {parts[0].lower()}") for mode in modes],
|
*[
|
||||||
|
_details_completion_item(mode, f"set {parts[0].lower()}")
|
||||||
|
for mode in modes
|
||||||
|
],
|
||||||
_details_completion_item("reset", f"clear {parts[0].lower()} override"),
|
_details_completion_item("reset", f"clear {parts[0].lower()} override"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3828,7 +3937,11 @@ def _details_completions(text: str) -> list[dict] | None:
|
|||||||
return [
|
return [
|
||||||
_details_completion_item(
|
_details_completion_item(
|
||||||
candidate,
|
candidate,
|
||||||
f"clear {parts[0].lower()} override" if candidate == "reset" else f"set {parts[0].lower()}",
|
(
|
||||||
|
f"clear {parts[0].lower()} override"
|
||||||
|
if candidate == "reset"
|
||||||
|
else f"set {parts[0].lower()}"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for candidate in (*modes, "reset")
|
for candidate in (*modes, "reset")
|
||||||
if candidate.startswith(prefix) and candidate != prefix
|
if candidate.startswith(prefix) and candidate != prefix
|
||||||
@@ -4712,7 +4825,11 @@ def _(rid, params: dict) -> dict:
|
|||||||
|
|
||||||
return _ok(rid, {"skills": get_available_skills()})
|
return _ok(rid, {"skills": get_available_skills()})
|
||||||
if action == "search":
|
if action == "search":
|
||||||
from tools.skills_hub import GitHubAuth, create_source_router, unified_search
|
from tools.skills_hub import (
|
||||||
|
GitHubAuth,
|
||||||
|
create_source_router,
|
||||||
|
unified_search,
|
||||||
|
)
|
||||||
|
|
||||||
raw = (
|
raw = (
|
||||||
unified_search(
|
unified_search(
|
||||||
|
|||||||
41
ui-tui/package-lock.json
generated
41
ui-tui/package-lock.json
generated
@@ -124,7 +124,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -502,6 +501,31 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
@@ -1676,7 +1700,6 @@
|
|||||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.19.0"
|
"undici-types": "~7.19.0"
|
||||||
}
|
}
|
||||||
@@ -1687,7 +1710,6 @@
|
|||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1698,7 +1720,6 @@
|
|||||||
"integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
|
"integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.58.1",
|
||||||
@@ -1728,7 +1749,6 @@
|
|||||||
"integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
|
"integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.58.1",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.58.1",
|
||||||
@@ -2046,7 +2066,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2449,7 +2468,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
@@ -3185,7 +3203,6 @@
|
|||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3317,7 +3334,6 @@
|
|||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -4226,7 +4242,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz",
|
||||||
"integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==",
|
"integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"type-fest": "^4.18.2"
|
"type-fest": "^4.18.2"
|
||||||
@@ -5663,7 +5678,6 @@
|
|||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5773,7 +5787,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||||
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6598,7 +6611,6 @@
|
|||||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "~0.27.0",
|
"esbuild": "~0.27.0",
|
||||||
"get-tsconfig": "^4.7.5"
|
"get-tsconfig": "^4.7.5"
|
||||||
@@ -6725,7 +6737,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6835,7 +6846,6 @@
|
|||||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
@@ -7251,7 +7261,6 @@
|
|||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -397,6 +397,34 @@ describe('createSlashHandler', () => {
|
|||||||
expect(rpc).not.toHaveBeenCalled()
|
expect(rpc).not.toHaveBeenCalled()
|
||||||
expect(ctx.transcript.sys).toHaveBeenCalledWith('no active session — nothing to save')
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('no active session — nothing to save')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('/title <name> uses session.title RPC and bypasses slash.exec', async () => {
|
||||||
|
patchUiState({ sid: 'sid-abc' })
|
||||||
|
const rpc = vi.fn(() => Promise.resolve({ pending: false, title: 'my title' }))
|
||||||
|
const ctx = buildCtx({ gateway: { ...buildGateway(), rpc } })
|
||||||
|
|
||||||
|
createSlashHandler(ctx)('/title my title')
|
||||||
|
|
||||||
|
expect(rpc).toHaveBeenCalledWith('session.title', { session_id: 'sid-abc', title: 'my title' })
|
||||||
|
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('session title set: my title')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('/title with no args fetches and displays the current title', async () => {
|
||||||
|
patchUiState({ sid: 'sid-abc' })
|
||||||
|
const rpc = vi.fn(() => Promise.resolve({ title: 'demo title' }))
|
||||||
|
const ctx = buildCtx({ gateway: { ...buildGateway(), rpc } })
|
||||||
|
|
||||||
|
createSlashHandler(ctx)('/title')
|
||||||
|
|
||||||
|
expect(rpc).toHaveBeenCalledWith('session.title', { session_id: 'sid-abc' })
|
||||||
|
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(ctx.transcript.sys).toHaveBeenCalledWith('title: demo title')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const buildCtx = (overrides: Partial<Ctx> = {}): Ctx => ({
|
const buildCtx = (overrides: Partial<Ctx> = {}): Ctx => ({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
ConfigGetValueResponse,
|
ConfigGetValueResponse,
|
||||||
ConfigSetResponse,
|
ConfigSetResponse,
|
||||||
SessionSaveResponse,
|
SessionSaveResponse,
|
||||||
|
SessionTitleResponse,
|
||||||
SessionSteerResponse,
|
SessionSteerResponse,
|
||||||
SessionUndoResponse
|
SessionUndoResponse
|
||||||
} from '../../../gatewayTypes.js'
|
} from '../../../gatewayTypes.js'
|
||||||
@@ -151,6 +152,47 @@ export const coreCommands: SlashCommand[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
help: 'set or show current session title',
|
||||||
|
name: 'title',
|
||||||
|
run: (arg, ctx) => {
|
||||||
|
if (!ctx.sid) {
|
||||||
|
return ctx.transcript.sys('no active session')
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = arg.trim()
|
||||||
|
|
||||||
|
if (!arg) {
|
||||||
|
ctx.gateway
|
||||||
|
.rpc<SessionTitleResponse>('session.title', { session_id: ctx.sid })
|
||||||
|
.then(
|
||||||
|
ctx.guarded<SessionTitleResponse>(r => {
|
||||||
|
const current = (r?.title ?? '').trim()
|
||||||
|
ctx.transcript.sys(current ? `title: ${current}` : 'no title set')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(ctx.guardedErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return ctx.transcript.sys('usage: /title <your session title>')
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.gateway
|
||||||
|
.rpc<SessionTitleResponse>('session.title', { session_id: ctx.sid, title })
|
||||||
|
.then(
|
||||||
|
ctx.guarded<SessionTitleResponse>(r => {
|
||||||
|
const next = (r?.title ?? title).trim()
|
||||||
|
const suffix = r?.pending ? ' (queued while session initializes)' : ''
|
||||||
|
ctx.transcript.sys(`session title set: ${next}${suffix}`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(ctx.guardedErr)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
help: 'toggle compact transcript',
|
help: 'toggle compact transcript',
|
||||||
name: 'compact',
|
name: 'compact',
|
||||||
|
|||||||
@@ -119,6 +119,12 @@ export interface SessionListResponse {
|
|||||||
sessions?: SessionListItem[]
|
sessions?: SessionListItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionTitleResponse {
|
||||||
|
pending?: boolean
|
||||||
|
session_key?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionSaveResponse {
|
export interface SessionSaveResponse {
|
||||||
file?: string
|
file?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user