Fix Hermes LLM GUI startup and layout

This commit is contained in:
Eskimue
2026-07-21 15:50:32 +02:00
parent 7502c47bcd
commit 1677fcac43
+183 -39
View File
@@ -197,18 +197,21 @@ class HermesLLMGUI:
body = ttk.PanedWindow(self.root, orient=tk.VERTICAL) body = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
body.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6)) body.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))
upper = ttk.Frame(body, padding=2) top_area = ttk.PanedWindow(body, orient=tk.VERTICAL)
body.add(top_area, weight=5)
upper = ttk.Frame(top_area, padding=2)
upper.columnconfigure(0, weight=1) upper.columnconfigure(0, weight=1)
upper.columnconfigure(1, weight=1) upper.columnconfigure(1, weight=1)
body.add(upper, weight=3) top_area.add(upper, weight=4)
self._build_llm_panel(upper) self._build_llm_panel(upper)
self._build_hermes_panel(upper) self._build_hermes_panel(upper)
lower = ttk.Frame(body, padding=2) lower = ttk.Frame(top_area, padding=2)
lower.columnconfigure(0, weight=1) lower.columnconfigure(0, weight=1)
lower.rowconfigure(0, weight=1) lower.rowconfigure(0, weight=1)
body.add(lower, weight=2) top_area.add(lower, weight=3)
lower_tabs = ttk.Notebook(lower) lower_tabs = ttk.Notebook(lower)
lower_tabs.grid(row=0, column=0, sticky=tk.NSEW) lower_tabs.grid(row=0, column=0, sticky=tk.NSEW)
@@ -216,27 +219,31 @@ class HermesLLMGUI:
actions_tab = ttk.Frame(lower_tabs, padding=4) actions_tab = ttk.Frame(lower_tabs, padding=4)
chat_tab = ttk.Frame(lower_tabs, padding=4) chat_tab = ttk.Frame(lower_tabs, padding=4)
lower_tabs.add(actions_tab, text="Actions") lower_tabs.add(actions_tab, text="Actions")
lower_tabs.add(chat_tab, text="Hermes Chat") lower_tabs.add(chat_tab, text="Chat")
self._build_actions(actions_tab) self._build_actions(actions_tab)
self._build_chat_tab(chat_tab) self._build_chat_tab(chat_tab)
log_frame = ttk.LabelFrame(self.root, text="Output", padding=4) log_frame = ttk.LabelFrame(body, text="Output", padding=4)
log_frame.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6)) body.add(log_frame, weight=4)
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
self.log = scrolledtext.ScrolledText( self.log = scrolledtext.ScrolledText(
log_frame, log_frame,
font=("Consolas", 9), font=("Consolas", 10),
bg="#1e1e1e", bg="#1e1e1e",
fg="#d4d4d4", fg="#d4d4d4",
insertbackground="white", insertbackground="white",
wrap=tk.WORD, wrap=tk.WORD,
state=tk.DISABLED, state=tk.DISABLED,
height=18,
) )
self.log.pack(fill=tk.BOTH, expand=True) self.log.grid(row=0, column=0, sticky=tk.NSEW)
self.log.tag_config("error", foreground="#f48771") self.log.tag_config("error", foreground="#f48771")
self.log.tag_config("ok", foreground="#89d185") self.log.tag_config("ok", foreground="#89d185")
self.log.tag_config("info", foreground="#9cdcfe") self.log.tag_config("info", foreground="#9cdcfe")
self.root.after(100, lambda: self._set_initial_pane_sizes(body, top_area))
def _build_connection_bar(self, parent): def _build_connection_bar(self, parent):
frame = ttk.LabelFrame(parent, text="SSH connection", padding=6) frame = ttk.LabelFrame(parent, text="SSH connection", padding=6)
@@ -292,15 +299,20 @@ class HermesLLMGUI:
ttk.Label(frame, text="Status:").grid(row=len(rows), column=0, sticky=tk.W, padx=4, pady=(8, 3)) ttk.Label(frame, text="Status:").grid(row=len(rows), column=0, sticky=tk.W, padx=4, pady=(8, 3))
self.llm_status_label = ttk.Label(frame, textvariable=self.v_llm_status, foreground="#555") self.llm_status_label = ttk.Label(frame, textvariable=self.v_llm_status, foreground="#555")
self.llm_status_label.grid(row=len(rows), column=1, sticky=tk.W, padx=4, pady=(8, 3)) self.llm_status_label.grid(row=len(rows), column=1, sticky=tk.W, padx=4, pady=(8, 3))
self.llm_status_label.bind("<Double-Button-1>", lambda _event: self._copy_status("LLM", self.v_llm_status.get()))
buttons = ttk.Frame(frame) buttons = ttk.Frame(frame)
buttons.grid(row=len(rows) + 1, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(8, 0)) buttons.grid(row=len(rows) + 1, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(8, 0))
tip(ttk.Button(buttons, text="Start LLM", command=self._start_llm), "Starts the remote llama-server process.").pack( self.start_llm_button = tip(
side=tk.LEFT, padx=(0, 6) ttk.Button(buttons, text="Start LLM", command=self._start_llm),
"Starts the remote llama-server process.",
) )
tip(ttk.Button(buttons, text="Stop LLM", command=self._stop_llm), "Stops the remote llama-server process.").pack( self.start_llm_button.pack(side=tk.LEFT, padx=(0, 6))
side=tk.LEFT, padx=6 self.stop_llm_button = tip(
ttk.Button(buttons, text="Stop LLM", command=self._stop_llm),
"Stops the remote llama-server process.",
) )
self.stop_llm_button.pack(side=tk.LEFT, padx=6)
tip( tip(
ttk.Button(buttons, text="LLM status", command=self._refresh_llm_status), ttk.Button(buttons, text="LLM status", command=self._refresh_llm_status),
"Checks the current process state and whether the HTTP endpoint is healthy.", "Checks the current process state and whether the HTTP endpoint is healthy.",
@@ -342,6 +354,10 @@ class HermesLLMGUI:
ttk.Label(frame, text="Status:").grid(row=len(rows) + 1, column=0, sticky=tk.W, padx=4, pady=(8, 3)) ttk.Label(frame, text="Status:").grid(row=len(rows) + 1, column=0, sticky=tk.W, padx=4, pady=(8, 3))
self.hermes_status_label = ttk.Label(frame, textvariable=self.v_hermes_status, foreground="#555") self.hermes_status_label = ttk.Label(frame, textvariable=self.v_hermes_status, foreground="#555")
self.hermes_status_label.grid(row=len(rows) + 1, column=1, sticky=tk.W, padx=4, pady=(8, 3)) self.hermes_status_label.grid(row=len(rows) + 1, column=1, sticky=tk.W, padx=4, pady=(8, 3))
self.hermes_status_label.bind(
"<Double-Button-1>",
lambda _event: self._copy_status("Hermes", self.v_hermes_status.get()),
)
buttons = ttk.Frame(frame) buttons = ttk.Frame(frame)
buttons.grid(row=len(rows) + 2, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(8, 0)) buttons.grid(row=len(rows) + 2, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(8, 0))
@@ -425,13 +441,18 @@ class HermesLLMGUI:
buttons = ttk.Frame(parent) buttons = ttk.Frame(parent)
buttons.grid(row=1, column=0, sticky=tk.W, pady=(0, 6)) buttons.grid(row=1, column=0, sticky=tk.W, pady=(0, 6))
tip( tip(
ttk.Button(buttons, text="Send to Hermes", command=self._send_chat_message), ttk.Button(buttons, text="Send", command=self._send_chat_message),
"Sends the current chat input using the selected mode.", "Sends the current chat input using the selected mode.",
).pack(side=tk.LEFT, padx=(0, 6)) ).pack(side=tk.LEFT, padx=(0, 6))
tip( tip(
ttk.Button(buttons, text="Clear chat", command=self._clear_chat_history), ttk.Button(buttons, text="New chat", command=self._clear_chat_history),
"Clears only the chat tab history in this GUI.", "Starts a new local chat session in this GUI.",
).pack(side=tk.LEFT, padx=6) ).pack(side=tk.LEFT, padx=6)
ttk.Label(
buttons,
text="Enter = senden, Shift+Enter = Zeilenumbruch",
foreground="#555",
).pack(side=tk.LEFT, padx=(12, 0))
history_frame = ttk.LabelFrame(parent, text="Conversation", padding=4) history_frame = ttk.LabelFrame(parent, text="Conversation", padding=4)
history_frame.grid(row=2, column=0, sticky=tk.NSEW, pady=(0, 6)) history_frame.grid(row=2, column=0, sticky=tk.NSEW, pady=(0, 6))
@@ -446,21 +467,36 @@ class HermesLLMGUI:
height=12, height=12,
) )
self.chat_text.grid(row=0, column=0, sticky=tk.NSEW) self.chat_text.grid(row=0, column=0, sticky=tk.NSEW)
self.chat_text.tag_config("chat_user", foreground="#0b57d0", spacing1=8, spacing3=10)
self.chat_text.tag_config("chat_assistant", foreground="#188038", spacing1=8, spacing3=10)
self.chat_text.tag_config("chat_error", foreground="#c5221f", spacing1=8, spacing3=10)
self.chat_text.tag_config("chat_meta", foreground="#666666", spacing1=4, spacing3=8)
input_frame = ttk.Frame(parent) input_frame = ttk.Frame(parent)
input_frame.grid(row=3, column=0, sticky=tk.EW) input_frame.grid(row=3, column=0, sticky=tk.EW)
input_frame.columnconfigure(0, weight=1) input_frame.columnconfigure(0, weight=1)
entry = ttk.Entry(input_frame, textvariable=self.v_chat_input) self.chat_input = tk.Text(input_frame, height=4, wrap=tk.WORD, font=("Segoe UI", 10))
entry.grid(row=0, column=0, sticky=tk.EW, padx=(0, 6)) self.chat_input.grid(row=0, column=0, sticky=tk.EW, padx=(0, 6))
entry.bind("<Return>", lambda _event: self._send_chat_message()) self.chat_input.bind("<Return>", self._on_chat_return)
tip( self.chat_input.bind("<Shift-Return>", self._on_chat_shift_return)
self.chat_send_button = tip(
ttk.Button(input_frame, text="Send", command=self._send_chat_message), ttk.Button(input_frame, text="Send", command=self._send_chat_message),
"Submits the current message to Hermes.", "Submits the current message using the selected chat mode.",
).grid(row=0, column=1) )
self.chat_send_button.grid(row=0, column=1, sticky=tk.N)
self._append_chat_meta("Neuer Chat gestartet.")
def _submit(self, coro): def _submit(self, coro):
return asyncio.run_coroutine_threadsafe(coro, self._loop) return asyncio.run_coroutine_threadsafe(coro, self._loop)
def _set_initial_pane_sizes(self, outer_pane: ttk.PanedWindow, top_pane: ttk.PanedWindow):
try:
total = max(self.root.winfo_height(), 760)
outer_pane.sashpos(0, int(total * 0.62))
top_pane.sashpos(0, int(total * 0.36))
except Exception:
pass
def _make_ssh(self) -> AsyncSSH: def _make_ssh(self) -> AsyncSSH:
return AsyncSSH( return AsyncSSH(
self.v_host.get().strip(), self.v_host.get().strip(),
@@ -533,21 +569,29 @@ class HermesLLMGUI:
return f""" return f"""
set -e set -e
mkdir -p {shlex.quote(os.path.dirname(log_path))} {shlex.quote(os.path.dirname(pid_path))} mkdir -p {shlex.quote(os.path.dirname(log_path))} {shlex.quote(os.path.dirname(pid_path))}
pkill -f "llama-server.*{model}" || true if [ -f {shlex.quote(pid_path)} ]; then
oldpid=$(cat {shlex.quote(pid_path)})
if ps -p "$oldpid" > /dev/null 2>&1; then
kill "$oldpid" 2>/dev/null || true
sleep 1
fi
rm -f {shlex.quote(pid_path)}
fi
pkill -x llama-server 2>/dev/null || true
cd {shlex.quote(workdir)} cd {shlex.quote(workdir)}
nohup ./build/bin/llama-server \\ setsid nohup ./build/bin/llama-server \\
-m {shlex.quote(model)} \\ -m {shlex.quote(model)} \\
-a {shlex.quote(alias)} \\ -a {shlex.quote(alias)} \\
-ngl 0 -t {threads} -c {ctx} --host {shlex.quote(bind_host)} --port {port} \\ -ngl 0 -t {threads} -c {ctx} --host {shlex.quote(bind_host)} --port {port} \\
> {shlex.quote(log_path)} 2>&1 & < /dev/null >> {shlex.quote(log_path)} 2>&1 &
echo $! > {shlex.quote(pid_path)} pid=$!
echo "$pid" > {shlex.quote(pid_path)}
sleep 3 sleep 3
cat {shlex.quote(pid_path)} cat {shlex.quote(pid_path)}
""" """
def _llm_stop_script(self) -> str: def _llm_stop_script(self) -> str:
pid_path = self.v_llm_pid.get().strip() pid_path = self.v_llm_pid.get().strip()
model = self.v_llm_model.get().strip()
return f""" return f"""
set -e set -e
if [ -f {shlex.quote(pid_path)} ]; then if [ -f {shlex.quote(pid_path)} ]; then
@@ -555,12 +599,13 @@ if [ -f {shlex.quote(pid_path)} ]; then
kill "$pid" 2>/dev/null || true kill "$pid" 2>/dev/null || true
rm -f {shlex.quote(pid_path)} rm -f {shlex.quote(pid_path)}
fi fi
pkill -f "llama-server.*{model}" || true pkill -x llama-server 2>/dev/null || true
""" """
def _llm_status_script(self) -> str: def _llm_status_script(self) -> str:
pid_path = self.v_llm_pid.get().strip() pid_path = self.v_llm_pid.get().strip()
port = int(self.v_llm_port.get()) port = int(self.v_llm_port.get())
host = self.v_host.get().strip()
return f""" return f"""
if [ -f {shlex.quote(pid_path)} ]; then if [ -f {shlex.quote(pid_path)} ]; then
pid=$(cat {shlex.quote(pid_path)}) pid=$(cat {shlex.quote(pid_path)})
@@ -568,13 +613,14 @@ if [ -f {shlex.quote(pid_path)} ]; then
echo "running: pid=$pid" echo "running: pid=$pid"
ps -p "$pid" -o pid=,etime=,cmd= ps -p "$pid" -o pid=,etime=,cmd=
else else
echo "stale pid file: $pid" rm -f {shlex.quote(pid_path)}
echo "not running (removed stale pid: $pid)"
fi fi
else else
echo "not running" echo "not running"
fi fi
printf '%s\\n' '--- health ---' printf '%s\\n' '--- health ---'
curl -s http://127.0.0.1:{port}/health || true curl -s http://{host}:{port}/health || true
""" """
def _hermes_start_script(self) -> str: def _hermes_start_script(self) -> str:
@@ -594,8 +640,13 @@ if [ -f {shlex.quote(pid_path)} ]; then
rm -f {shlex.quote(pid_path)} rm -f {shlex.quote(pid_path)}
fi fi
. {shlex.quote(posixpath.join(venv, "bin", "activate"))} . {shlex.quote(posixpath.join(venv, "bin", "activate"))}
nohup hermes serve --host {shlex.quote(bind_host)} --port {port}{extra_suffix} > {shlex.quote(log_path)} 2>&1 & setsid bash -lc {shlex.quote(
echo $! > {shlex.quote(pid_path)} f". {shlex.quote(posixpath.join(venv, 'bin', 'activate'))} && "
+ f"hermes serve --host {shlex.quote(bind_host)} --port {port}{extra_suffix} "
+ f"< /dev/null >> {shlex.quote(log_path)} 2>&1"
)} &
pid=$!
echo "$pid" > {shlex.quote(pid_path)}
sleep 3 sleep 3
cat {shlex.quote(pid_path)} cat {shlex.quote(pid_path)}
""" """
@@ -634,11 +685,12 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
def _start_llm(self): def _start_llm(self):
self._log("[info] starting remote llama.cpp server\n", "info") self._log("[info] starting remote llama.cpp server\n", "info")
self._submit(self._async_run_script(self._llm_start_script(), "LLM started", self._refresh_llm_status)) self._set_llm_starting(True)
self._submit(self._async_start_llm())
def _stop_llm(self): def _stop_llm(self):
self._log("[info] stopping remote llama.cpp server\n", "info") self._log("[info] stopping remote llama.cpp server\n", "info")
self._submit(self._async_run_script(self._llm_stop_script(), "LLM stopped", self._refresh_llm_status)) self._submit(self._async_stop_llm())
def _start_hermes(self): def _start_hermes(self):
self._log("[info] starting remote Hermes backend\n", "info") self._log("[info] starting remote Hermes backend\n", "info")
@@ -658,14 +710,14 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
async def _async_start_both(self): async def _async_start_both(self):
self._log("[info] starting LLM first\n", "info") self._log("[info] starting LLM first\n", "info")
await self._async_run_script(self._llm_start_script(), "LLM started") await self._async_start_llm()
self._log("[info] starting Hermes next\n", "info") self._log("[info] starting Hermes next\n", "info")
await self._async_run_script(self._hermes_start_script(), "Hermes started") await self._async_run_script(self._hermes_start_script(), "Hermes started")
self.root.after(0, self._refresh_all_status) self.root.after(0, self._refresh_all_status)
async def _async_stop_both(self): async def _async_stop_both(self):
await self._async_run_script(self._hermes_stop_script(), "Hermes stopped") await self._async_run_script(self._hermes_stop_script(), "Hermes stopped")
await self._async_run_script(self._llm_stop_script(), "LLM stopped") await self._async_stop_llm()
self.root.after(0, self._refresh_all_status) self.root.after(0, self._refresh_all_status)
def _refresh_llm_status(self): def _refresh_llm_status(self):
@@ -697,6 +749,47 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
finally: finally:
await ssh.close() await ssh.close()
async def _async_start_llm(self):
ssh = self._make_ssh()
try:
await ssh.connect()
result = await ssh.exec(f"bash -lc {shlex.quote(self._llm_start_script())}")
if result.strip():
self._log(result if result.endswith("\n") else result + "\n", "info")
llm_port = int(self.v_llm_port.get())
llm_host = self.v_host.get().strip()
health_cmd = "bash -lc " + shlex.quote(f"curl -s http://{llm_host}:{llm_port}/health")
for _ in range(20):
health_output = await ssh.exec(health_cmd)
if '"status":"ok"' in health_output.replace(" ", ""):
self._log(f"[ok] LLM started and healthy on {llm_host}:{llm_port}\n", "ok")
self.root.after(0, lambda: self._set_llm_status("running"))
return
await asyncio.sleep(1)
self._log(
f"[error] LLM start command ran, but no healthy endpoint appeared on {llm_host}:{llm_port}\n",
"error",
)
log_output = await ssh.exec(
f"bash -lc {shlex.quote(f'tail -n 60 {shlex.quote(self.v_llm_log.get().strip())}')}"
)
if log_output.strip():
self._log(log_output if log_output.endswith("\n") else log_output + "\n", "error")
self.root.after(0, lambda: self._set_llm_status("not running"))
except Exception as exc:
self._log(f"[error] LLM start failed: {exc}\n", "error")
self.root.after(0, lambda: self._set_llm_status("not running"))
finally:
await ssh.close()
self.root.after(0, lambda: self._set_llm_starting(False))
self.root.after(0, self._refresh_llm_status)
async def _async_stop_llm(self):
await self._async_run_script(self._llm_stop_script(), "LLM stopped", self._refresh_llm_status)
def _tail_llm_log(self): def _tail_llm_log(self):
self._submit(self._async_tail_log(self.v_llm_log.get().strip(), "LLM")) self._submit(self._async_tail_log(self.v_llm_log.get().strip(), "LLM"))
@@ -723,10 +816,13 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
self._submit(self._async_test_chat()) self._submit(self._async_test_chat())
def _send_chat_message(self): def _send_chat_message(self):
text = self.v_chat_input.get().strip() if hasattr(self, "chat_send_button") and str(self.chat_send_button["state"]) == "disabled":
return
text = self.chat_input.get("1.0", tk.END).strip()
if not text: if not text:
return return
self.v_chat_input.set("") self.chat_input.delete("1.0", tk.END)
self._set_chat_busy(True)
self._append_chat_message("You", text) self._append_chat_message("You", text)
self._chat_history.append(("user", text)) self._chat_history.append(("user", text))
if self.v_chat_mode.get() == "hermes": if self.v_chat_mode.get() == "hermes":
@@ -739,13 +835,50 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
self.chat_text.config(state=tk.NORMAL) self.chat_text.config(state=tk.NORMAL)
self.chat_text.delete("1.0", tk.END) self.chat_text.delete("1.0", tk.END)
self.chat_text.config(state=tk.DISABLED) self.chat_text.config(state=tk.DISABLED)
if hasattr(self, "chat_input"):
self.chat_input.delete("1.0", tk.END)
self._append_chat_meta("Neuer Chat gestartet.")
def _append_chat_message(self, speaker: str, text: str): def _append_chat_message(self, speaker: str, text: str):
tag = "chat_assistant"
if speaker == "You":
tag = "chat_user"
elif "Error" in speaker:
tag = "chat_error"
self.chat_text.config(state=tk.NORMAL) self.chat_text.config(state=tk.NORMAL)
self.chat_text.insert(tk.END, f"{speaker}:\n{text.strip()}\n\n") self.chat_text.insert(tk.END, f"{speaker}:\n", tag)
self.chat_text.insert(tk.END, f"{text.strip()}\n\n")
self.chat_text.see(tk.END) self.chat_text.see(tk.END)
self.chat_text.config(state=tk.DISABLED) self.chat_text.config(state=tk.DISABLED)
def _append_chat_meta(self, text: str):
self.chat_text.config(state=tk.NORMAL)
self.chat_text.insert(tk.END, f"{text.strip()}\n\n", "chat_meta")
self.chat_text.see(tk.END)
self.chat_text.config(state=tk.DISABLED)
def _copy_status(self, label: str, value: str):
text = f"{label} status: {value or 'unknown'}"
self.root.clipboard_clear()
self.root.clipboard_append(text)
self._log(f"[info] copied to clipboard: {text}\n", "info")
def _set_chat_busy(self, busy: bool):
if hasattr(self, "chat_send_button"):
self.chat_send_button.configure(state=tk.DISABLED if busy else tk.NORMAL)
if hasattr(self, "chat_input"):
self.chat_input.configure(state=tk.DISABLED if busy else tk.NORMAL)
if not busy:
self.chat_input.focus_set()
def _on_chat_return(self, _event):
self._send_chat_message()
return "break"
def _on_chat_shift_return(self, _event):
self.chat_input.insert(tk.INSERT, "\n")
return "break"
def _set_llm_status(self, text: str): def _set_llm_status(self, text: str):
self.v_llm_status.set(text) self.v_llm_status.set(text)
color = "#555" color = "#555"
@@ -756,6 +889,15 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
color = "#c5221f" color = "#c5221f"
self.llm_status_label.configure(foreground=color) self.llm_status_label.configure(foreground=color)
def _set_llm_starting(self, starting: bool):
if hasattr(self, "start_llm_button"):
self.start_llm_button.configure(
text="Starting..." if starting else "Start LLM",
state=tk.DISABLED if starting else tk.NORMAL,
)
if hasattr(self, "stop_llm_button"):
self.stop_llm_button.configure(state=tk.DISABLED if starting else tk.NORMAL)
def _set_hermes_status(self, text: str): def _set_hermes_status(self, text: str):
self.v_hermes_status.set(text) self.v_hermes_status.set(text)
color = "#555" color = "#555"
@@ -917,6 +1059,7 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
self._log(f"[error] Hermes chat failed: {exc}\n", "error") self._log(f"[error] Hermes chat failed: {exc}\n", "error")
finally: finally:
await ssh.close() await ssh.close()
self.root.after(0, lambda: self._set_chat_busy(False))
async def _async_send_direct_llm_chat_message(self): async def _async_send_direct_llm_chat_message(self):
ssh = self._make_ssh() ssh = self._make_ssh()
@@ -979,6 +1122,7 @@ curl -sI http://127.0.0.1:{port} | head -1 || true
self._log(f"[error] LLM chat failed: {exc}\n", "error") self._log(f"[error] LLM chat failed: {exc}\n", "error")
finally: finally:
await ssh.close() await ssh.close()
self.root.after(0, lambda: self._set_chat_busy(False))
async def _async_run_script(self, script: str, ok_message: str = "", callback=None): async def _async_run_script(self, script: str, ok_message: str = "", callback=None):
ssh = self._make_ssh() ssh = self._make_ssh()