feat(agents): add raw ACP activity view toggle

- Add a Raw switch to the agent activity panel header so users can swap between the formatted transcript and raw ACP JSON-RPC payloads.
- Update the activity title to reflect raw mode and render the raw feed as the full panel content instead of a nested dark card.
- Keep the existing transcript view unchanged by default and scope the raw-mode reset to agent/channel changes.
This commit is contained in:
Taylor Ho
2026-06-14 23:21:05 -07:00
parent 6b64b70275
commit baba54d6b6
3 changed files with 84 additions and 36 deletions
@@ -31,6 +31,7 @@ type ManagedAgentSessionPanelProps = {
compact?: boolean;
emptyDescription?: string;
isWorking?: boolean;
rawLayout?: "responsive" | "exclusive";
showHeader?: boolean;
showInterventionHint?: boolean;
showRaw?: boolean;
@@ -44,6 +45,7 @@ export function ManagedAgentSessionPanel({
compact = false,
emptyDescription = "Mention this agent in a channel to watch the next turn.",
isWorking = false,
rawLayout = "responsive",
showHeader = true,
showInterventionHint = false,
showRaw = true,
@@ -108,6 +110,7 @@ export function ManagedAgentSessionPanel({
hasObserver={hasObserver}
isWorking={isWorking}
profiles={profiles}
rawLayout={rawLayout}
showInterventionHint={showInterventionHint}
showRaw={showRaw}
transcript={scopedTranscript}
@@ -161,6 +164,7 @@ function SessionBody({
hasObserver,
isWorking,
profiles,
rawLayout,
showInterventionHint,
showRaw,
transcript,
@@ -174,10 +178,26 @@ function SessionBody({
hasObserver: boolean;
isWorking: boolean;
profiles?: UserProfileLookup;
rawLayout: "responsive" | "exclusive";
showInterventionHint: boolean;
showRaw: boolean;
transcript: TranscriptItem[];
}) {
if (showRaw && rawLayout === "exclusive") {
return (
<>
<RawEventRail events={events} />
{errorMessage ? (
<p className="mt-4 inline-flex items-center gap-2 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
<CircleAlert className="h-4 w-4" />
{errorMessage}
</p>
) : null}
</>
);
}
return (
<>
{!hasObserver ? (
@@ -187,7 +207,7 @@ function SessionBody({
) : (
<div
className={cn(
showRaw
showRaw && rawLayout === "responsive"
? "mt-4 grid gap-4 xl:grid-cols-[minmax(0,1fr)_20rem]"
: "mt-0",
)}
@@ -201,7 +221,9 @@ function SessionBody({
profiles={profiles}
showInterventionHint={showInterventionHint}
/>
{showRaw ? <RawEventRail events={events} /> : null}
{showRaw && rawLayout === "responsive" ? (
<RawEventRail events={events} />
) : null}
</div>
)}
+13 -31
View File
@@ -1,46 +1,28 @@
import * as React from "react";
import { Button } from "@/shared/ui/button";
import type { ObserverEvent } from "./agentSessionTypes";
import { describeRawEvent } from "./agentSessionTranscript";
export function RawEventRail({ events }: { events: ObserverEvent[] }) {
const [expanded, setExpanded] = React.useState(false);
const visible = expanded ? events : events.slice(-18);
return (
<aside className="rounded-lg border border-border/70 bg-[#17171d] text-zinc-100">
<div className="flex items-center justify-between border-b border-white/10 px-3 py-2">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-zinc-400">
Raw ACP
<section className="flex min-h-0 w-full flex-col text-foreground">
<div className="min-h-0 flex-1">
{events.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
No raw events yet.
</p>
<p className="text-xs text-zinc-500">JSON-RPC payloads</p>
</div>
<Button
className="h-7 text-zinc-300 hover:text-zinc-50"
onClick={() => setExpanded((current) => !current)}
size="sm"
variant="ghost"
>
{expanded ? "Latest" : "All"}
</Button>
</div>
<div className="max-h-[34rem] overflow-auto px-3 py-3">
{visible.length === 0 ? (
<p className="text-xs text-zinc-500">No raw events yet.</p>
) : (
<div className="space-y-2">
{visible.map((event) => (
{events.map((event) => (
<details
className="rounded-md border border-white/10 bg-white/[0.03] px-2 py-1.5"
className="group rounded-md border border-border/55 bg-muted/25 px-2.5 py-1.5 transition-colors open:bg-muted/35"
key={event.seq}
>
<summary className="cursor-pointer select-none text-xs text-zinc-300">
<span className="font-mono text-zinc-500">#{event.seq}</span>{" "}
<summary className="cursor-pointer select-none text-xs text-muted-foreground transition-colors group-open:text-foreground">
<span className="font-mono text-muted-foreground/70">
#{event.seq}
</span>{" "}
{describeRawEvent(event)}
</summary>
<pre className="mt-2 max-h-72 overflow-auto whitespace-pre-wrap break-words text-[11px] leading-5 text-zinc-300">
<pre className="mt-2 max-h-72 overflow-auto whitespace-pre-wrap wrap-break-word rounded-md border border-border/40 bg-background/45 p-2 text-[11px] leading-5 text-muted-foreground">
{JSON.stringify(event.payload, null, 2)}
</pre>
</details>
@@ -48,6 +30,6 @@ export function RawEventRail({ events }: { events: ObserverEvent[] }) {
</div>
)}
</div>
</aside>
</section>
);
}
@@ -1,4 +1,5 @@
import { ArrowLeft, CircleDot, Octagon, X } from "lucide-react";
import * as React from "react";
import { ArrowLeft, CircleDot, Octagon, TerminalSquare, X } from "lucide-react";
import { toast } from "sonner";
import { ManagedAgentSessionPanel } from "@/features/agents/ui/ManagedAgentSessionPanel";
@@ -25,6 +26,7 @@ import {
PANEL_SINGLE_COLUMN_HEADER_LAYER_CLASS,
} from "@/shared/ui/OverlayPanelBackdrop";
import { THREAD_PANEL_MIN_WIDTH_PX } from "@/shared/hooks/useThreadPanelWidth";
import { Switch } from "@/shared/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import type { ChannelAgentSessionAgent } from "./useChannelAgentSessions";
@@ -60,6 +62,19 @@ export function AgentSessionThreadPanel({
useEscapeKey(onClose, isOverlay || isSinglePanelView);
const { ref: scrollRef, onScroll } = useStickToBottom<HTMLDivElement>();
const rawFeedScopeKey = `${agent.pubkey}:${channel.id}`;
const [rawFeedState, setRawFeedState] = React.useState(() => ({
scopeKey: rawFeedScopeKey,
show: false,
}));
const showRawFeed =
rawFeedState.scopeKey === rawFeedScopeKey && rawFeedState.show;
const handleRawFeedChange = React.useCallback(
(checked: boolean) => {
setRawFeedState({ scopeKey: rawFeedScopeKey, show: checked });
},
[rawFeedScopeKey],
);
async function handleInterruptTurn() {
try {
@@ -87,6 +102,32 @@ export function AgentSessionThreadPanel({
Live
</Badge>
) : null}
{isLive ? (
<div
className="flex min-w-19 shrink-0 items-center justify-end gap-2"
title={
showRawFeed
? "Hide raw JSON-RPC payloads."
: "Show raw JSON-RPC payloads for this channel."
}
>
<label
className="flex shrink-0 items-center gap-1.5 text-[11px] font-medium text-muted-foreground"
htmlFor="agent-session-raw-feed-switch"
>
<TerminalSquare className="h-3 w-3" />
Raw
</label>
<Switch
aria-label={showRawFeed ? "Hide raw feed" : "Show raw feed"}
checked={showRawFeed}
className="shrink-0 data-[state=unchecked]:bg-muted-foreground/45 [&>span]:bg-white"
data-testid="agent-session-toggle-raw-feed"
id="agent-session-raw-feed-switch"
onCheckedChange={handleRawFeedChange}
/>
</div>
) : null}
{isLive && isWorking ? (
<Tooltip>
<TooltipTrigger asChild>
@@ -140,7 +181,9 @@ export function AgentSessionThreadPanel({
>
<ArrowLeft />
</Button>
<AuxiliaryPanelTitle>Activity</AuxiliaryPanelTitle>
<AuxiliaryPanelTitle>
{showRawFeed ? "Raw ACP Activity" : "Activity"}
</AuxiliaryPanelTitle>
</AuxiliaryPanelHeaderGroup>
{agentHeaderActions}
</>
@@ -164,9 +207,10 @@ export function AgentSessionThreadPanel({
emptyDescription={`Mention ${agent.name} in the channel to see its work here.`}
isWorking={isWorking}
profiles={profiles}
rawLayout="exclusive"
showHeader={false}
showInterventionHint={canInterruptTurn}
showRaw={false}
showRaw={showRawFeed}
/>
</div>
);