mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix(tui): keep /title session names in sync
Route TUI /title through session.title RPC and queue titles when the session DB row is still initializing, so renamed sessions reliably appear in /resume and browse flows.
This commit is contained in:
@@ -258,6 +258,68 @@ 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 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 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_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
|
||||||
|
|
||||||
|
|||||||
@@ -1530,6 +1530,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 +1568,13 @@ 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:
|
||||||
|
if db.set_session_title(key, pending_title):
|
||||||
|
session["pending_title"] = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
session["agent"] = agent
|
session["agent"] = agent
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1736,12 +1744,24 @@ 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:
|
||||||
|
return _ok(
|
||||||
|
rid,
|
||||||
|
{
|
||||||
|
"title": db.get_session_title(key) or session.get("pending_title") or "",
|
||||||
|
"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, 4007, "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})
|
||||||
|
session["pending_title"] = title
|
||||||
|
return _ok(rid, {"pending": True, "title": title})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _err(rid, 5007, str(e))
|
return _err(rid, 5007, str(e))
|
||||||
|
|
||||||
|
|||||||
@@ -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