A terminal panel beside an agent chat

A real shell on the chat's own connection, opened and closed like the
inspector and never beside it. The modes govern the model; what a person
types is theirs, since they hold the credential and could open the same
shell with an ssh client. The model cannot see the panel -- a button
copies the output you choose into the composer.

The session outlives the socket: closing the panel leaves a build
running, and coming back reattaches with the scrollback. Two tabs share
one shell and the smaller window decides the size. It ends on an idle
timeout, on deleting the chat, on disabling, moving or deleting the
connection, and on a restart -- which says why rather than quietly
opening a fresh shell that has lost the working directory.

The nginx template's `Connection ""` is right for SSE and fails every
WebSocket handshake, so `location /` now uses a `map $http_upgrade`;
update.sh grows a drift check for it, because the only symptom on a
stale vhost is a panel that cannot connect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-02 01:44:07 +02:00
parent a1824681ae
commit 47791a88c7
37 changed files with 2889 additions and 31 deletions
+5
View File
@@ -36,6 +36,7 @@ from lembas.services import prompts as prompts_service
from lembas.services import settings_store, sse
from lembas.services import tools as tools_service
from lembas.services.agent import policy as agent_policy
from lembas.services.agent import terminal as terminal_service
from lembas.services.markdown import escape_text, render_markdown
from lembas.web.templating import render, templates
@@ -997,6 +998,10 @@ def _clean_params(**submitted: str | None) -> dict[str, float | int | None]:
@router.delete("/{chat_id}", dependencies=[Depends(require_permission("chat.delete"))])
async def delete_chat(db: Db, user: RequiredUser, chat_id: str) -> Response:
chat = _owned_chat(db, chat_id, user.id)
# Before the row goes: a terminal is keyed on the chat id, so afterwards
# there would be nothing left to find it by and a shell would sit open on
# somebody's machine until the idle timeout noticed.
await terminal_service.close_chat(chat_id)
db.delete(chat)
db.commit()