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 246be1fa8e
commit 5117168454
39 changed files with 2981 additions and 34 deletions
+79
View File
@@ -510,6 +510,85 @@ button, input, textarea, select {
}
}
/* The terminal panel: a fourth child of .shell, to the left of the inspector.
Built beside it rather than in chat.css because the shell layout lives here,
and the two are the same shape -- a fixed-width column that hides with the
`hidden` attribute. */
.terminal {
width: var(--terminal-width);
flex: none;
display: flex;
flex-direction: column;
min-height: 0;
background: var(--bg-sunken);
border-left: 1px solid var(--border);
}
.terminal__header {
display: flex;
align-items: center;
gap: var(--sp-2);
height: var(--header-height);
flex: none;
padding: 0 var(--sp-3);
border-bottom: 1px solid var(--border);
}
.terminal__title {
display: flex;
align-items: center;
gap: var(--sp-2);
flex: 1;
min-width: 0;
font-size: var(--text-sm);
font-weight: 600;
color: var(--ink-muted);
}
.terminal__where {
font-weight: 400;
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--ink-faint);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* The element xterm renders into. It measures itself from this box, so it must
have a size of its own -- min-height: 0 on a flex child is what stops the
terminal growing the panel instead of scrolling inside it. */
.terminal__screen {
flex: 1;
min-height: 0;
padding: var(--sp-2);
background: var(--code-bg);
}
.terminal__screen .xterm { height: 100%; }
.terminal__status {
flex: none;
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
border-top: 1px solid var(--border);
font-size: var(--text-xs);
color: var(--ink-faint);
line-height: var(--leading-normal);
}
.terminal__status strong { color: var(--ink-muted); font-weight: 600; }
.terminal__message { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.terminal__message--error { color: var(--danger); }
@media (max-width: 64rem) {
.terminal {
position: fixed;
inset: 0 0 0 auto;
width: min(var(--terminal-width), 100vw);
z-index: 40;
box-shadow: var(--shadow-lg);
}
}
.topbar {
display: flex;
align-items: center;
+4
View File
@@ -66,6 +66,10 @@
/* --- Layout ----------------------------------------------------------- */
--sidebar-width: 17.5rem;
--inspector-width: 24rem;
/* Wider than the inspector because the content is not prose: eighty columns
of --font-mono do not fit in 24rem, and a terminal narrower than eighty
re-wraps everything a program prints. */
--terminal-width: 34rem;
--thread-max-width: 48rem;
--header-height: 3.5rem;