feat: readable server logs, a page to read them, and an agent shell launcher - #17
Merged
Merged
Conversation
Every page that offered a terminal assembled the request itself, so the agent key and the session command could drift apart. They have to travel together: the key is what lets a repeat click focus the tab a profile already has instead of stacking a second shell, and the command is what makes that tab the profile's session rather than the container's prompt. TerminalRequestStore.openAgentShell owns both. The `>_` glyph becomes mc-terminal-icon so the places offering a terminal cannot drift into two different icons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The running container emitted 612 log lines in 48 minutes and 572 of them (93%) were two messages. ContainerInventory reports why it hides a container from the fleet, and the fleet view polls every 10 seconds, so a standing property of a foreign container was re-reported forever — burying the upgrade and terminal failures those warnings exist to surface. They are now reported once per host/container and re-armed only when the container goes away and comes back. Levels were inverted: 33 warn, 4 error, 2 info. Almost no INFO meant no record of what the application did. A deploy is a multi-minute transaction over a volume, bootstrap containers and a pull, and it logged nothing on success or rollback; container removal drops the data volume holding profiles, souls, skills and credentials and logged nothing at all; the MCP Compose lifecycle runs on its own executor, so its failures reached no request thread and no exception advice. That one is now ERROR with the cause attached, because nothing else records it. The console pattern drops the PID, the application name and the `---` separator, all constant in a one-process container, and trades the 40-char logger for the simple class name. Spring's banner, its startup-info lines and the Tomcat/Hikari/servlet-context chatter are off; StartupSummary replaces them with one block naming the port, the Docker endpoint and whether it answered, the database, the stack dir and the key mode. Startup is 11 lines, each carrying information. Separately: the SPA resolver returned classpath:/static/index.html without checking it exists. A resolver must return null when it cannot resolve — ResourceHttpRequestHandler calls lastModified() on whatever comes back, so a build without the Angular bundle answered every unknown path with a 500 and a 45-line trace instead of a 404. The existing test asserted only getFilename(), so it passed against a resource that was not there. The tail rows were duplicated in four places, which is how three of them ended up without a date and why the level filter existed on only one — and there without `info`. One LogView now renders all of them, with a date, the full level set, per-level counts and a remembered height. New Server Logs page over GET /api/server/logs, served from an in-memory ring rather than a second on-disk copy of what stdout already owns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resize frames are SIGWINCH at the far end, and the shell answers each one by redrawing its prompt. The ResizeObserver fired per pointer frame during a drag and fitNow reported unconditionally, so one drag sent ~30 identical resize frames and the prompt accumulated across the input line as `qa > qa > qa >`. Two guards: the observer's bursts collapse into one fit per animation frame, and a fit only reports when the character grid actually changed — most pixel steps of a height drag land on the same rows, and telling the pty about a size it already has costs a redraw for nothing. connect() clears the remembered grid, because a new socket has been told nothing. Measured by the regression test: 31 frames before, 1 after. Also moves the resize handle to the panel's own top edge, spanning the full width, so the handle is the boundary being moved and grabbing it needs no aim. LogView loses its +/- stepper for the same full-width grip on its bottom edge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fitting reflows xterm's buffer and the far end repaints on the SIGWINCH that follows, so doing it per pointer frame meant the app repainted against a buffer that had moved underneath it. The panel now suspends fits for the length of the drag and fits once when it settles, so a drag costs one reflow and one resize instead of roughly thirty. This does not fully fix the duplicated prompt, and the remainder is not ours. Driving /ws/terminal directly against a real container shows bash repainting with `\r\x1b[K\r` — it erases the line first, so any number of resizes overwrite in place. The Hermes REPL (prompt_toolkit) emits no erase-line at all: it moves the cursor back by the row count it believes it occupies and erases with `\x1b[J`. When rows change that count is stale, the cursor lands too low, the erase clears nothing, and the redraw is drawn below the old copy. One resize frame produces two copies of the prompt — measured, not inferred. So the client's remaining job is to send as few resizes as possible, which is what this does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two features. The logging work is the bulk of it; the agent-shell launcher was already in the tree and rides along in its own commit.
Why
The running container emitted 612 log lines in 48 minutes, and 572 of them (93%) were the same two messages.
ContainerInventoryreports why it hides a container from the fleet view — but that describes a standing property of a foreign container, and the fleet view polls every 10s, so it re-fired forever and buried the upgrade and terminal failures those warnings exist to surface.Levels were inverted too — 33 warn, 4 error, 2 info. Almost no INFO meant no record of what the application actually did.
What changed
The flood. Exclusions are reported once, keyed
hostId/name, pruned against the live listing so a container that goes away and returns is reported again. Verified: 12 polls → each exclusion logged once, not 24 times.Missing levels. A deploy is a multi-minute transaction over a volume, bootstrap containers and a pull — it logged nothing on success or rollback. Container removal drops the data volume holding profiles, souls, skills and credentials and logged nothing at all (now WARN, before the fact, naming the volume). The MCP Compose lifecycle runs on its own executor, so its failures reach no request thread and no exception advice — that one is now ERROR with the cause attached, because nothing else records it.
Format. Dropped the PID, application name and
---separator (all constant in a one-process container) and traded the 40-char logger for the simple class name. Spring's banner, startup-info and the Tomcat/Hikari/servlet-context chatter are off;StartupSummaryreplaces them with one block naming the port, the Docker endpoint and whether it answered, the database, the stack dir and the key mode.Startup is 11 lines, each carrying information.
A 500 that should have been a 404. The SPA resolver returned
classpath:/static/index.htmlwithout checking it exists. A resolver must returnnullwhen it cannot resolve —ResourceHttpRequestHandlercallslastModified()on whatever comes back, so a build without the Angular bundle answered every unknown path with a 500 and a 45-line trace at ERROR. The existing test asserted onlygetFilename(), so it passed against a resource that was not there. Now covered both ways.One log renderer. The tail rows were duplicated in four places, which is how three of them ended up with no date and why the level filter existed on only one — and there without
info.LogViewrenders all of them now: date stamp, full level set with per-level counts, and a height that is remembered per placement (buttons + drag grip, reusingPanelHeight).New Server Logs page at
/server-logs, overGET /api/server/logs. Served from an in-memory ring rather than a second on-disk copy of what stdout already owns — what falls out of the ring is still indocker logs.Testing
Notes for review
ApiContractTestcaught/api/server/inforeturning an ad-hocMapwith no record to pin — fixed with a realServerInfoDto. Good test.PanelHeight.draggained anedgeparam: the terminal is docked at the bottom and grows upward, an inline log card grows downward.🤖 Generated with Claude Code
Follow-up: the duplicated prompt on resize is upstream, and already fixed
Investigated with the dashboard taken out of the path entirely — a bare
docker exec -itin a real PTY, resized with the sameTIOCSWINSZioctl any native terminal sends. The double-paint reproduces identically there, so it is not the web terminal:It is
NousResearch/hermes-agentissue #26316 ("input text can be replayed after resize, causing duplicated prompt text"), fixed by #45586 fix(cli): preserve resize renderer state, whose docstring names the symptom exactly — "under- or over-allocating the TextArea height leaves stale prompt/input cells visible at the bottom of the terminal."That PR merged 2026-06-13. The running Hermes container is revision
88dbf95from 2026-06-12 — one day short of the fix. No issue filed: it would be a duplicate. The container wants an upgrade.What stays in this PR is still worth having on its own terms: a drag now costs one reflow and one resize instead of roughly thirty. An identical resize was separately measured to cost zero renders, so the dedupe is free insurance.