mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user