2026-05-19 10:35:14 +07:00
|
|
|
import curses
|
2026-05-26 14:03:40 +07:00
|
|
|
import threading
|
2026-06-17 16:00:26 +07:00
|
|
|
from datetime import datetime
|
2026-06-12 14:10:06 +07:00
|
|
|
import config
|
2026-05-19 10:35:14 +07:00
|
|
|
from .render import init_colors, draw
|
|
|
|
|
from .input import handle_key
|
2026-05-26 15:18:16 +07:00
|
|
|
from .agent import log, WELCOME_ART
|
2026-06-17 16:00:26 +07:00
|
|
|
from services.session_manager_neo import NeoSessionManager, NeoSession
|
2026-05-19 10:35:14 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class HendrikTUI:
|
|
|
|
|
def __init__(self, llm_client, tools_definition, TOOLS, TOOL_HANDLERS,
|
|
|
|
|
build_system_prompt, agent_max_iterations):
|
|
|
|
|
self.llm = llm_client
|
|
|
|
|
self.tools_def = tools_definition
|
|
|
|
|
self.TOOLS = TOOLS
|
|
|
|
|
self.TOOL_HANDLERS = TOOL_HANDLERS
|
|
|
|
|
self.build_system_prompt = build_system_prompt
|
|
|
|
|
self.agent_max_iterations = agent_max_iterations
|
|
|
|
|
|
2026-05-26 14:03:40 +07:00
|
|
|
self.messages = None
|
|
|
|
|
self.log = []
|
|
|
|
|
self.input_buffer = [""]
|
|
|
|
|
self.input_line = 0
|
|
|
|
|
self.input_col = 0
|
|
|
|
|
self.scroll = 0
|
|
|
|
|
self.processing = False
|
|
|
|
|
self.running = True
|
|
|
|
|
self.h, self.w = 0, 0
|
|
|
|
|
|
|
|
|
|
self.agent_thread: threading.Thread | None = None
|
|
|
|
|
self.agent_done = threading.Event()
|
2026-05-19 10:35:14 +07:00
|
|
|
|
2026-06-17 16:00:26 +07:00
|
|
|
self.session_mgr = NeoSessionManager()
|
|
|
|
|
self.current_session: NeoSession | None = None
|
|
|
|
|
|
2026-06-16 14:59:40 +07:00
|
|
|
def switch_model(self, item: dict):
|
|
|
|
|
self.llm.base_url = item["base_url"]
|
|
|
|
|
self.llm.model = item["model"]
|
|
|
|
|
self.llm.api_key = item["api_key"]
|
2026-06-17 16:00:26 +07:00
|
|
|
if self.current_session:
|
|
|
|
|
self.session_mgr.update_model_info(
|
|
|
|
|
self.current_session.doc_id, self._model_info()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _model_info(self) -> dict:
|
|
|
|
|
return {
|
|
|
|
|
"provider": config.resolve_provider(self.llm.base_url, self.llm.model),
|
|
|
|
|
"base_url": self.llm.base_url,
|
|
|
|
|
"model": self.llm.model,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def new_session(self):
|
|
|
|
|
name = f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
|
|
|
|
self.current_session = self.session_mgr.create(name, self._model_info())
|
|
|
|
|
self.messages = [{"role": "system", "content": self.build_system_prompt(
|
|
|
|
|
tools_definition=self.tools_def,
|
|
|
|
|
character=config.AGENT_CHARACTER or None,
|
|
|
|
|
skills=config.AGENT_SKILLS.split(",") if config.AGENT_SKILLS else None,
|
|
|
|
|
)}]
|
|
|
|
|
self.log.clear()
|
|
|
|
|
self.scroll = 0
|
|
|
|
|
log(self, "welcome", WELCOME_ART)
|
|
|
|
|
|
|
|
|
|
def switch_session(self, doc_id: int):
|
|
|
|
|
session = self.session_mgr.get(doc_id)
|
|
|
|
|
if not session:
|
|
|
|
|
return
|
|
|
|
|
self.current_session = session
|
|
|
|
|
self.messages = list(session.messages)
|
|
|
|
|
self.log.clear()
|
|
|
|
|
self.scroll = 0
|
|
|
|
|
log(self, "welcome", WELCOME_ART)
|
|
|
|
|
for msg in session.messages:
|
|
|
|
|
if msg["role"] == "user":
|
|
|
|
|
log(self, "user", msg["content"])
|
|
|
|
|
elif msg["role"] == "assistant":
|
|
|
|
|
log(self, "ai", msg["content"])
|
|
|
|
|
elif msg["role"] == "system" and msg.get("content"):
|
|
|
|
|
if len(msg["content"]) > 100:
|
|
|
|
|
continue
|
|
|
|
|
log(self, "system", msg["content"])
|
2026-06-16 14:59:40 +07:00
|
|
|
|
2026-05-19 10:35:14 +07:00
|
|
|
def run(self):
|
|
|
|
|
try:
|
|
|
|
|
curses.wrapper(self._main)
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def _main(self, stdscr):
|
|
|
|
|
curses.use_default_colors()
|
|
|
|
|
init_colors()
|
2026-05-26 14:03:40 +07:00
|
|
|
stdscr.keypad(True)
|
2026-05-19 10:35:14 +07:00
|
|
|
stdscr.refresh()
|
|
|
|
|
|
2026-06-17 16:00:26 +07:00
|
|
|
self.new_session()
|
2026-05-19 10:35:14 +07:00
|
|
|
|
|
|
|
|
while self.running:
|
|
|
|
|
self.h, self.w = stdscr.getmaxyx()
|
|
|
|
|
if self.h < 14 or self.w < 40:
|
|
|
|
|
stdscr.erase()
|
|
|
|
|
stdscr.addstr(0, 0, "Terminal too small (min 40x14)")
|
|
|
|
|
stdscr.refresh()
|
|
|
|
|
stdscr.getch()
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
draw(self, stdscr)
|
2026-05-26 13:54:24 +07:00
|
|
|
curses.curs_set(2)
|
2026-05-26 14:03:40 +07:00
|
|
|
|
|
|
|
|
if self.processing:
|
|
|
|
|
stdscr.timeout(100)
|
|
|
|
|
else:
|
|
|
|
|
stdscr.timeout(-1)
|
|
|
|
|
|
2026-05-19 10:35:14 +07:00
|
|
|
try:
|
|
|
|
|
key = stdscr.getch()
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
break
|
2026-05-26 14:03:40 +07:00
|
|
|
|
2026-05-19 11:38:13 +07:00
|
|
|
handle_key(self, stdscr, key)
|
2026-05-26 14:03:40 +07:00
|
|
|
|
|
|
|
|
if self.agent_done.is_set():
|
|
|
|
|
self.agent_thread.join()
|
|
|
|
|
self.agent_done.clear()
|
|
|
|
|
self.processing = False
|
|
|
|
|
self.agent_thread = None
|