From a7507441f9386f51b1246240ea0074ed79561994 Mon Sep 17 00:00:00 2001 From: Arach Tchoupani Date: Sat, 6 Jun 2026 20:34:00 -0400 Subject: [PATCH 1/2] feat(web): add live tmux peek Add a real tmux peek endpoint and inspector panel, widen terminal defaults, smooth live activity playback, and guard local agent snapshot reads when broker snapshots are missing messages. --- packages/runtime/src/local-agents.test.ts | 37 +++ packages/runtime/src/local-agents.ts | 17 +- packages/web/client/app.css | 9 + .../client/components/AgentLiveActions.tsx | 16 +- .../client/components/agent-live-actions.css | 35 ++- packages/web/client/index.html | 13 ++ packages/web/client/lib/types.ts | 12 + .../scout/inspector/AgentsInspector.tsx | 5 + .../scout/inspector/ConversationInspector.tsx | 152 ++++++++++--- .../web/client/scout/inspector/TmuxPeek.tsx | 174 +++++++++++++++ packages/web/client/scout/slots/ctx-panel.css | 127 ++++++++++- .../web/client/screens/TerminalScreen.tsx | 11 +- .../create-openscout-web-server.test.ts | 86 ++++++- .../web/server/create-openscout-web-server.ts | 211 ++++++++++++++++++ 14 files changed, 852 insertions(+), 53 deletions(-) create mode 100644 packages/web/client/scout/inspector/TmuxPeek.tsx diff --git a/packages/runtime/src/local-agents.test.ts b/packages/runtime/src/local-agents.test.ts index 31ddd17c..97729032 100644 --- a/packages/runtime/src/local-agents.test.ts +++ b/packages/runtime/src/local-agents.test.ts @@ -16,6 +16,7 @@ import { clearEndpointFailureMetadata, endpointStateAfterSuccessfulSessionWarmup, areHarnessBinariesAvailable, + brokerSnapshotMessages, normalizeClaudeRuntimeLaunchArgs, normalizeLocalAgentSystemPrompt, renderLocalAgentSystemPromptTemplate, @@ -494,3 +495,39 @@ describe("local agent reply cleanup", () => { expect(cleaned).toBe("SHAPER_BROKER_OK"); }); }); + +describe("local agent broker snapshots", () => { + test("treats missing or malformed broker snapshot messages as empty", () => { + expect(brokerSnapshotMessages(undefined)).toEqual([]); + expect(brokerSnapshotMessages({})).toEqual([]); + expect(brokerSnapshotMessages({ messages: null })).toEqual([]); + expect(brokerSnapshotMessages({ messages: [] })).toEqual([]); + }); + + test("filters malformed broker snapshot messages", () => { + expect(brokerSnapshotMessages({ + messages: { + valid: { + actorId: "agent-1", + body: "ready", + createdAt: 123, + }, + missingBody: { + actorId: "agent-2", + createdAt: 124, + }, + badTimestamp: { + actorId: "agent-3", + body: "bad", + createdAt: "124", + }, + }, + })).toEqual([ + { + actorId: "agent-1", + body: "ready", + createdAt: 123, + }, + ]); + }); +}); diff --git a/packages/runtime/src/local-agents.ts b/packages/runtime/src/local-agents.ts index 172eef27..a11d57a2 100644 --- a/packages/runtime/src/local-agents.ts +++ b/packages/runtime/src/local-agents.ts @@ -234,6 +234,21 @@ interface BrokerSnapshot { messages: Record; } +function isBrokerSnapshotMessage(value: unknown): value is BrokerSnapshotMessage { + if (!value || typeof value !== "object") return false; + const candidate = value as Partial; + return typeof candidate.actorId === "string" + && typeof candidate.body === "string" + && typeof candidate.createdAt === "number"; +} + +export function brokerSnapshotMessages(value: unknown): BrokerSnapshotMessage[] { + if (!value || typeof value !== "object") return []; + const messages = (value as { messages?: unknown }).messages; + if (!messages || typeof messages !== "object" || Array.isArray(messages)) return []; + return Object.values(messages).filter(isBrokerSnapshotMessage); +} + const DEFAULT_LOCAL_AGENT_CAPABILITIES: AgentCapability[] = ["chat", "invoke", "deliver"]; const DEFAULT_LOCAL_AGENT_HARNESS: AgentHarness = "claude"; const DEFAULT_ONE_TIME_LOCAL_AGENT_CARD_TTL_MS = 24 * 60 * 60 * 1000; @@ -2420,7 +2435,7 @@ async function readBrokerMessagesSince(sinceSeconds: number): Promise(baseUrl, "/v1/snapshot", { socketPath: resolveBrokerSocketPathForBaseUrl(baseUrl), }); - return Object.values(snapshot.messages) + return brokerSnapshotMessages(snapshot) .filter((message) => normalizeBrokerTimestamp(message.createdAt) >= sinceSeconds) .sort((lhs, rhs) => normalizeBrokerTimestamp(lhs.createdAt) - normalizeBrokerTimestamp(rhs.createdAt)); } diff --git a/packages/web/client/app.css b/packages/web/client/app.css index 2f827be8..72af8254 100644 --- a/packages/web/client/app.css +++ b/packages/web/client/app.css @@ -165,6 +165,15 @@ } * { box-sizing: border-box; margin: 0; } + +html, +body, +#root { + min-height: 100%; + background: var(--bg, #050605); + color: var(--ink, #f3f4ee); +} + body { margin: 0; } /* ── Content shell ── */ diff --git a/packages/web/client/components/AgentLiveActions.tsx b/packages/web/client/components/AgentLiveActions.tsx index b320367e..7a8f2547 100644 --- a/packages/web/client/components/AgentLiveActions.tsx +++ b/packages/web/client/components/AgentLiveActions.tsx @@ -75,9 +75,12 @@ export function AgentLiveActions({ const canObserveTerminal = agent.transport === "tmux" && Boolean(activeSessionId); const canTakeover = canObserveTerminal || Boolean(resolvedCatalog?.resumeCommand); const hasLiveTurn = Boolean(activeSessionId) || state === "working"; + const isCompact = variant === "compact"; const status = hasLiveTurn ? activeSessionId - ? `Live ${shortSession(activeSessionId)}` + ? isCompact + ? canObserveTerminal ? "Live terminal" : "Live trace" + : `Live ${shortSession(activeSessionId)}` : "Working" : state === "ready" ? "Ready" @@ -87,7 +90,12 @@ export function AgentLiveActions({ : agent.updatedAt ? timeAgo(agent.updatedAt) : null; - const observeLabel = canObserveTerminal ? "Observe terminal" : "Observe trace"; + const observeLabel = canObserveTerminal + ? isCompact ? "Observe" : "Observe terminal" + : isCompact ? "Trace" : "Observe trace"; + const observeTitle = canObserveTerminal + ? "Observe the live tmux terminal" + : "Open the web observe trace"; const openTerminal = (mode: "observe" | "takeover") => { onNavigate?.(); @@ -143,8 +151,8 @@ export function AgentLiveActions({ type="button" className="agent-live-actions-button agent-live-actions-button--primary" onClick={() => canObserveTerminal ? openTerminal("observe") : openTrace()} - title={canObserveTerminal ? "Observe the live tmux terminal" : "Open the web observe trace"} - aria-label={observeLabel} + title={observeTitle} + aria-label={observeTitle} >