feat(agents): durable tmux-backed terminal sessions + fit/chrome fixes

- Run agent sessions under an isolated tmux server (its own systemd scope, with a
  keepalive session) so they survive a castle-api restart and stay rediscoverable;
  each WebSocket is a tmux attach client. Falls back to the in-memory pty backend
  when tmux is absent or CASTLE_API_AGENT_BACKEND=memory.
- Refactor the session layer behind one Attachment interface (create/list/attach/
  close), keeping resume-by-id, agent-native history, and continue modes.
- Fix the terminal not refilling when the dock is maximized (explicit refit signal
  + instant size transition so xterm measures the final box).
- On narrow screens, go full-bleed and strip chrome (header/border/padding) when
  maximized.
This commit is contained in:
2026-07-02 03:41:42 -07:00
parent 25e2275999
commit bd644ea905
4 changed files with 534 additions and 202 deletions

View File

@@ -99,12 +99,15 @@ export function AssistantDock() {
terminal's WebSocket survives minimize. */}
<div
className={cn(
"fixed z-40 flex flex-col",
"fixed z-40 flex flex-col bg-[var(--background)] shadow-2xl",
expanded
? "inset-4"
: "bottom-4 right-4 w-[760px] max-w-[calc(100vw-2rem)] h-[600px] max-h-[calc(100vh-2rem)]",
"bg-[var(--background)] border border-[var(--border)] rounded-xl shadow-2xl",
"transition-all duration-200",
? // Maximized: full-bleed on narrow screens (no margin/border/radius),
// ins-4 with chrome on sm+.
"inset-0 rounded-none border-0 sm:inset-4 sm:rounded-xl sm:border sm:border-[var(--border)]"
: "bottom-4 right-4 w-[760px] max-w-[calc(100vw-2rem)] h-[600px] max-h-[calc(100vh-2rem)] rounded-xl border border-[var(--border)]",
// Animate only open/close (opacity + translate); size changes (expand)
// are instant so the terminal's fit measures the final box immediately.
"transition-[opacity,transform] duration-200",
open
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-[calc(100%+2rem)] pointer-events-none",
@@ -114,7 +117,14 @@ export function AssistantDock() {
{/* Header */}
<div className="flex items-center gap-2 px-3 py-2 border-b border-[var(--border)]">
<Bot size={16} className="text-[var(--primary)] shrink-0" />
<div className="flex flex-wrap items-center gap-1.5 min-w-0">
<div
className={cn(
"flex flex-wrap items-center gap-1.5 min-w-0",
// Maximized on a narrow screen: drop the launcher pills for space
// (shrink to switch agents). Visible again at sm+.
expanded && "max-sm:hidden",
)}
>
<button
onClick={() => launch(SHELL)}
className={cn(
@@ -190,7 +200,7 @@ export function AssistantDock() {
</div>
{/* Body */}
<div className="relative flex-1 min-h-0 p-2">
<div className={cn("relative flex-1 min-h-0 p-2", expanded && "max-sm:p-0")}>
{active ? (
<AgentTerminal
key={active.key}
@@ -200,6 +210,8 @@ export function AssistantDock() {
resumeSessionId={active.resumeSessionId}
onSession={onSession}
fill
compact={expanded}
fitSignal={(open ? 1 : 0) + (expanded ? 2 : 0)}
/>
) : (
<div className="h-full flex items-center justify-center text-sm text-[var(--muted)]">