diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs
index d412e790e9..8cc93e0050 100644
--- a/desktop/src-tauri/src/managed_agents/runtime.rs
+++ b/desktop/src-tauri/src/managed_agents/runtime.rs
@@ -1356,6 +1356,7 @@ pub fn build_managed_agent_summary(
         max_turn_duration_seconds: record.max_turn_duration_seconds,
         parallelism: record.parallelism,
         system_prompt: effective_prompt,
+        avatar_url: record.avatar_url.clone(),
         model: effective_model,
         mcp_toolsets: record.mcp_toolsets.clone(),
         env_vars: record.env_vars.clone(),
diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs
index e9d733e545..4bd10fe48e 100644
--- a/desktop/src-tauri/src/managed_agents/types.rs
+++ b/desktop/src-tauri/src/managed_agents/types.rs
@@ -224,6 +224,7 @@ pub struct ManagedAgentSummary {
     pub max_turn_duration_seconds: Option<u64>,
     pub parallelism: u32,
     pub system_prompt: Option<String>,
+    pub avatar_url: Option<String>,
     pub model: Option<String>,
     pub mcp_toolsets: Option<String>,
     #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
diff --git a/desktop/src/features/agents/ui/AgentSessionToolItem.tsx b/desktop/src/features/agents/ui/AgentSessionToolItem.tsx
index 78dd98304f..773c9dd808 100644
--- a/desktop/src/features/agents/ui/AgentSessionToolItem.tsx
+++ b/desktop/src/features/agents/ui/AgentSessionToolItem.tsx
@@ -1,29 +1,16 @@
 import * as React from "react";
-import { ArrowUpRight, ChevronDown, Wrench } from "lucide-react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { ChevronDown, Send } from "lucide-react";
 
-import { useAppNavigation } from "@/app/navigation/useAppNavigation";
-import { useUsersBatchQuery } from "@/features/profile/hooks";
-import { resolveUserLabel } from "@/features/profile/lib/identity";
-import type { Channel, UserProfileSummary } from "@/shared/api/types";
-import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
 import { cn } from "@/shared/lib/cn";
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
-import { UserAvatar } from "@/shared/ui/UserAvatar";
+import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
 import type { TranscriptItem } from "./agentSessionTypes";
+import { getBuzzToolInfo } from "./agentSessionToolCatalog";
+import { buildCompactToolSummary } from "./agentSessionToolSummary";
 import {
-  formatToolTitle,
-  getBuzzToolInfo,
-  getToolStatusDisplay,
-} from "./agentSessionToolCatalog";
-import {
-  asRecord,
   formatCodeValue,
-  formatDuration,
-  formatTranscriptTime,
-  getResultArray,
-  getToolString,
-  getToolStringList,
-  shortenMiddle,
+  getToolDurationDisplay,
+  isInlineImageData,
 } from "./agentSessionUtils";
 
 export function ToolItem({
@@ -32,14 +19,12 @@ export function ToolItem({
   item: Extract<TranscriptItem, { type: "tool" }>;
 }) {
   const [isExpanded, setIsExpanded] = React.useState(false);
-  const status = getToolStatusDisplay(item.status, item.isError);
   const hasArgs = Object.keys(item.args).length > 0;
   const hasResult = item.result.trim().length > 0;
   const canonicalToolName = item.buzzToolName ?? item.toolName;
   const buzzTool = getBuzzToolInfo(canonicalToolName);
-  const ToolIcon = buzzTool?.icon ?? Wrench;
-  const showStatus = status.state !== "output-available";
-  const toolTitle = formatToolTitle(canonicalToolName, item.title);
+  const compactSummary = buildCompactToolSummary(item);
+  const duration = getToolDurationDisplay(item);
   const handleToggle = React.useCallback(
     (event: React.SyntheticEvent<HTMLDetailsElement>) => {
       setIsExpanded(event.currentTarget.open);
@@ -48,40 +33,38 @@ export function ToolItem({
   );
 
   return (
-    <div className="not-prose w-full px-1">
+    <div
+      className="not-prose w-full px-0 py-0.5"
+      data-testid="transcript-tool-item"
+    >
       <details
         className="group w-full"
         onToggle={handleToggle}
         open={isExpanded}
       >
-        <summary className="inline-flex max-w-full cursor-pointer list-none items-center gap-1.5 py-px">
-          {ToolIcon ? (
-            <ToolIcon
-              className={cn(
-                "h-4 w-4 shrink-0",
-                buzzTool ? "text-primary" : "text-muted-foreground",
-              )}
+        <summary
+          className={cn(
+            "max-w-full cursor-pointer list-none py-px",
+            compactSummary.presentation === "inline" &&
+              "inline-flex items-center gap-1.5",
+            compactSummaryTone(),
+          )}
+        >
+          {compactSummary.presentation === "message" ? (
+            <CompactMessageSummary
+              duration={duration}
+              isError={item.isError || item.status === "failed"}
+              label={compactSummary.label}
+              preview={compactSummary.preview}
             />
-          ) : null}
-          <span className="min-w-0 truncate text-sm font-medium">
-            {toolTitle}
-          </span>
-          {buzzTool ? (
-            <BuzzToolInlineAction args={item.args} result={item.result} />
-          ) : null}
-          {showStatus ? (
-            <span className="flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
-              <status.Icon
-                className={cn(
-                  "h-4 w-4",
-                  item.status === "executing" && "animate-pulse",
-                )}
-              />
-              {status.label}
-            </span>
-          ) : null}
-          <ToolTimestamp item={item} />
-          <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180" />
+          ) : (
+            <CompactToolSummaryRow
+              duration={duration}
+              preview={compactSummary.preview}
+              thumbnailSrc={compactSummary.thumbnailSrc}
+              label={compactSummary.label}
+            />
+          )}
         </summary>
 
         <ToolDetailBlocks
@@ -89,6 +72,14 @@ export function ToolItem({
           description={buzzTool?.label}
           hasArgs={hasArgs}
           hasResult={hasResult}
+          imagePreview={
+            compactSummary.kind === "view_image" && isExpanded
+              ? {
+                  src: compactSummary.thumbnailSrc,
+                  title: compactSummary.preview,
+                }
+              : null
+          }
           isError={item.isError}
           result={item.result}
         />
@@ -97,11 +88,219 @@ export function ToolItem({
   );
 }
 
+function compactSummaryTone() {
+  return "text-muted-foreground/60 group-open:text-muted-foreground";
+}
+
+function resolveImageSrc(source: string): string {
+  return isInlineImageData(source) ? source : rewriteRelayUrl(source);
+}
+
+function CompactToolSummaryRow({
+  duration,
+  label,
+  preview,
+  thumbnailSrc,
+}: {
+  duration: string | null;
+  label: string;
+  preview: string | null;
+  thumbnailSrc: string | null;
+}) {
+  const [thumbnailFailed, setThumbnailFailed] = React.useState(false);
+  const mutedTone = compactSummaryTone();
+  const resolvedThumbnail = React.useMemo(() => {
+    if (!thumbnailSrc || thumbnailFailed) return null;
+    return resolveImageSrc(thumbnailSrc);
+  }, [thumbnailFailed, thumbnailSrc]);
+
+  return (
+    <>
+      <span className={cn("shrink-0 text-sm font-semibold", mutedTone)}>
+        {label}
+      </span>
+      {resolvedThumbnail ? (
+        <img
+          alt=""
+          className="h-5 w-auto max-w-12 shrink-0 rounded-sm object-cover"
+          decoding="async"
+          loading="lazy"
+          onError={() => setThumbnailFailed(true)}
+          src={resolvedThumbnail}
+          title={preview ?? undefined}
+        />
+      ) : preview ? (
+        <span
+          className={cn("min-w-0 max-w-48 truncate text-sm", mutedTone)}
+          title={preview}
+        >
+          {preview}
+        </span>
+      ) : null}
+      {duration ? (
+        <span className={cn("shrink-0 text-xs", mutedTone)}>{duration}</span>
+      ) : null}
+      <ChevronDown
+        className={cn(
+          "h-3.5 w-3.5 shrink-0 transition-transform group-open:rotate-180",
+          mutedTone,
+        )}
+      />
+    </>
+  );
+}
+
+function CompactMessageSummary({
+  duration,
+  isError,
+  label,
+  preview,
+}: {
+  duration: string | null;
+  isError: boolean;
+  label: string;
+  preview: string | null;
+}) {
+  const mutedTone = compactSummaryTone();
+  return (
+    <div className="flex max-w-[85%] flex-col items-start gap-1.5">
+      <div
+        className={cn(
+          "min-w-0 rounded-2xl border px-3 py-2 text-sm leading-relaxed shadow-sm",
+          isError
+            ? "border-destructive/25 bg-destructive/10 text-destructive"
+            : "border-primary/15 bg-primary/6 text-foreground",
+        )}
+        data-testid="transcript-tool-message-preview"
+      >
+        <p className="whitespace-pre-wrap wrap-break-word">
+          {preview || "Message content unavailable."}
+        </p>
+      </div>
+      <div className="inline-flex max-w-full items-center gap-1.5 px-1">
+        <Send className={cn("h-3.5 w-3.5 shrink-0", mutedTone)} />
+        <span className={cn("truncate text-xs font-medium", mutedTone)}>
+          {label}
+        </span>
+        {duration ? (
+          <span className={cn("shrink-0 text-xs", mutedTone)}>{duration}</span>
+        ) : null}
+        <ChevronDown
+          className={cn(
+            "h-3.5 w-3.5 shrink-0 transition-transform group-open:rotate-180",
+            mutedTone,
+          )}
+        />
+      </div>
+    </div>
+  );
+}
+
+function ViewImageToolPreview({
+  src,
+  title,
+}: {
+  src: string;
+  title: string | null;
+}) {
+  const [lightboxOpen, setLightboxOpen] = React.useState(false);
+  const [imageFailed, setImageFailed] = React.useState(false);
+  const resolvedSrc = React.useMemo(() => resolveImageSrc(src), [src]);
+  const alt = title ?? "Viewed image";
+
+  if (imageFailed) {
+    return null;
+  }
+
+  return (
+    <>
+      {/* biome-ignore lint/a11y/useKeyWithClickEvents: opens lightbox on click */}
+      <img
+        alt={alt}
+        className="ml-1.5 block max-h-64 max-w-[min(24rem,calc(100%-0.375rem))] cursor-pointer rounded-lg object-contain"
+        decoding="async"
+        loading="lazy"
+        onClick={() => setLightboxOpen(true)}
+        onError={() => setImageFailed(true)}
+        src={resolvedSrc}
+        title={title ?? undefined}
+      />
+      <ImageLightbox
+        alt={alt}
+        onOpenChange={setLightboxOpen}
+        open={lightboxOpen}
+        src={resolvedSrc}
+      />
+    </>
+  );
+}
+
+function ImageLightbox({
+  alt,
+  onOpenChange,
+  open,
+  src,
+}: {
+  alt: string;
+  onOpenChange: (open: boolean) => void;
+  open: boolean;
+  src: string;
+}) {
+  return (
+    <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
+      <DialogPrimitive.Portal>
+        <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
+        <DialogPrimitive.Content
+          className="fixed inset-0 z-50 flex items-center justify-center p-8"
+          onInteractOutside={(event) => event.preventDefault()}
+          onPointerDownOutside={(event) => event.preventDefault()}
+        >
+          <DialogPrimitive.Title className="sr-only">
+            {alt}
+          </DialogPrimitive.Title>
+          <DialogPrimitive.Description className="sr-only">
+            Full-size image preview. Press Escape or click outside the image to
+            close.
+          </DialogPrimitive.Description>
+          <DialogPrimitive.Close
+            aria-label="Close lightbox"
+            className="absolute inset-0 cursor-default"
+          />
+          <img
+            alt={alt}
+            className="relative max-h-[90vh] max-w-[90vw] rounded-lg object-contain"
+            src={src}
+          />
+          <DialogPrimitive.Close className="absolute right-4 top-4 rounded-full bg-black/50 p-2 text-white/80 transition-colors hover:bg-black/70 hover:text-white focus:outline-hidden focus:ring-2 focus:ring-white/30">
+            <svg
+              aria-hidden="true"
+              fill="none"
+              height="20"
+              viewBox="0 0 24 24"
+              width="20"
+              xmlns="http://www.w3.org/2000/svg"
+            >
+              <path
+                d="M18 6L6 18M6 6l12 12"
+                stroke="currentColor"
+                strokeLinecap="round"
+                strokeLinejoin="round"
+                strokeWidth="2"
+              />
+            </svg>
+          </DialogPrimitive.Close>
+        </DialogPrimitive.Content>
+      </DialogPrimitive.Portal>
+    </DialogPrimitive.Root>
+  );
+}
+
 function ToolDetailBlocks({
   args,
   description,
   hasArgs,
   hasResult,
+  imagePreview,
   isError,
   result,
 }: {
@@ -109,6 +308,7 @@ function ToolDetailBlocks({
   description?: string;
   hasArgs: boolean;
   hasResult: boolean;
+  imagePreview: { src: string | null; title: string | null } | null;
   isError: boolean;
   result: string;
 }) {
@@ -119,6 +319,12 @@ function ToolDetailBlocks({
           {description}
         </p>
       ) : null}
+      {imagePreview?.src ? (
+        <ViewImageToolPreview
+          src={imagePreview.src}
+          title={imagePreview.title}
+        />
+      ) : null}
       {hasArgs ? (
         <ToolCodeBlock
           label="Parameters"
@@ -158,7 +364,7 @@ function ToolCodeBlock({
       </h4>
       <pre
         className={cn(
-          "max-h-64 overflow-auto whitespace-pre-wrap break-words rounded-md px-3 py-2 font-mono text-xs leading-5",
+          "max-h-64 overflow-auto whitespace-pre-wrap wrap-break-word rounded-md px-3 py-2 font-mono text-xs leading-5",
           tone === "error"
             ? "bg-destructive/10 text-destructive"
             : "bg-muted/50 text-foreground",
@@ -169,260 +375,3 @@ function ToolCodeBlock({
     </div>
   );
 }
-
-const toolFullDateTimeFormat = new Intl.DateTimeFormat(undefined, {
-  weekday: "long",
-  year: "numeric",
-  month: "long",
-  day: "numeric",
-  hour: "numeric",
-  minute: "2-digit",
-  second: "2-digit",
-});
-
-function ToolTimestamp({
-  item,
-}: {
-  item: Extract<TranscriptItem, { type: "tool" }>;
-}) {
-  const time = formatTranscriptTime(item.timestamp);
-  if (!time) return null;
-  const duration =
-    item.startedAt && item.completedAt
-      ? formatDuration(item.startedAt, item.completedAt)
-      : null;
-  const date = new Date(item.timestamp);
-  const fullDateTime = Number.isNaN(date.getTime())
-    ? item.timestamp
-    : toolFullDateTimeFormat.format(date);
-  return (
-    <Tooltip>
-      <TooltipTrigger asChild>
-        <span className="shrink-0 cursor-default text-[11px] text-muted-foreground/60">
-          {time}
-          {duration ? ` · ${duration}` : null}
-        </span>
-      </TooltipTrigger>
-      <TooltipContent side="top">{fullDateTime}</TooltipContent>
-    </Tooltip>
-  );
-}
-
-function BuzzToolInlineAction({
-  args,
-  result,
-}: {
-  args: Record<string, unknown>;
-  result: string;
-}) {
-  const { channels } = useChannelNavigation();
-  const { goChannel } = useAppNavigation();
-  const resultValue = React.useMemo(
-    () => parseToolResultValue(result),
-    [result],
-  );
-  const resultRecord = asRecord(resultValue);
-  const channelId =
-    getToolString(args, ["channel_id", "channelId"]) ??
-    getToolString(resultRecord, ["channel_id", "channelId"]);
-  const pubkeys = React.useMemo(
-    () => getToolStringList(args, ["pubkeys", "pubkey"]),
-    [args],
-  );
-  const profilesQuery = useUsersBatchQuery(pubkeys, {
-    enabled: pubkeys.length > 0,
-  });
-  const profiles = profilesQuery.data?.profiles;
-  const openChannel = React.useCallback(
-    (messageId?: string) => {
-      if (!channelId) return;
-      void goChannel(channelId, messageId ? { messageId } : undefined);
-    },
-    [channelId, goChannel],
-  );
-  const action = React.useMemo(
-    () =>
-      getBuzzToolInlineAction({
-        args,
-        channelId,
-        channels,
-        openChannel,
-        profiles,
-        resultValue,
-      }),
-    [args, channelId, channels, openChannel, profiles, resultValue],
-  );
-
-  if (!action) {
-    return null;
-  }
-
-  if (action.onClick) {
-    return (
-      <button
-        className="inline-flex max-w-[14rem] shrink min-w-0 items-center gap-1 rounded-full border border-primary/20 bg-primary/[0.05] px-1.5 py-0.5 text-[11px] font-normal leading-none text-primary/90 transition-colors hover:border-primary/35 hover:bg-primary/10 hover:text-primary"
-        onClick={(event) => {
-          event.preventDefault();
-          event.stopPropagation();
-          action.onClick?.();
-        }}
-        title={action.title}
-        type="button"
-      >
-        {action.avatar}
-        <span className="shrink-0">{action.label}</span>
-        <span className="truncate">{action.value}</span>
-        <ArrowUpRight className="h-4 w-4 shrink-0" />
-      </button>
-    );
-  }
-
-  return (
-    <span
-      className="inline-flex max-w-[14rem] shrink min-w-0 items-center gap-1 rounded-full border border-border/60 bg-muted/40 px-1.5 py-0.5 text-[11px] font-normal leading-none text-muted-foreground"
-      title={action.title}
-    >
-      {action.avatar}
-      <span className="shrink-0">{action.label}</span>
-      <span className="truncate">{action.value}</span>
-    </span>
-  );
-}
-
-type BuzzToolInlineActionModel = {
-  avatar?: React.ReactNode;
-  label: string;
-  value: string;
-  title: string;
-  onClick?: () => void;
-};
-
-function getBuzzToolInlineAction({
-  args,
-  channelId,
-  channels,
-  openChannel,
-  profiles,
-  resultValue,
-}: {
-  args: Record<string, unknown>;
-  channelId: string | null;
-  channels: Channel[];
-  openChannel: (messageId?: string) => void;
-  profiles: Record<string, UserProfileSummary> | undefined;
-  resultValue: unknown;
-}): BuzzToolInlineActionModel | null {
-  const resultRecord = asRecord(resultValue);
-  const eventId =
-    getToolString(args, ["event_id", "eventId"]) ??
-    getToolString(resultRecord, ["event_id", "eventId", "id"]);
-
-  if (eventId && channelId) {
-    return {
-      label: resultRecord.accepted === true ? "posted" : "event",
-      onClick: () => openChannel(eventId),
-      title: eventId,
-      value: getChannelChipLabel(channels, channelId),
-    };
-  }
-
-  const messages = getResultArray(resultValue, resultRecord, "messages");
-  if (messages) {
-    return {
-      label: "read",
-      onClick: channelId ? () => openChannel() : undefined,
-      title: `${messages.length} messages`,
-      value: `${messages.length} message${messages.length === 1 ? "" : "s"}`,
-    };
-  }
-
-  if (channelId) {
-    return {
-      label: "channel",
-      onClick: () => openChannel(),
-      title: channelId,
-      value: getChannelChipLabel(channels, channelId),
-    };
-  }
-
-  const workflowId =
-    getToolString(args, ["workflow_id", "workflowId"]) ??
-    getToolString(resultRecord, ["workflow_id", "workflowId"]);
-  if (workflowId) {
-    return {
-      label: "workflow",
-      title: workflowId,
-      value: shortenMiddle(workflowId, 26),
-    };
-  }
-
-  const pubkeys = getToolStringList(args, ["pubkeys", "pubkey"]);
-  if (pubkeys.length > 0) {
-    if (pubkeys.length === 1) {
-      const pk = pubkeys[0];
-      const displayName = resolveUserLabel({ pubkey: pk, profiles });
-      const profile = profiles?.[pk.toLowerCase()];
-      return {
-        avatar: (
-          <UserAvatar
-            avatarUrl={profile?.avatarUrl ?? null}
-            className="shrink-0"
-            displayName={displayName}
-            size="xs"
-          />
-        ),
-        label: "user",
-        title: pk,
-        value: displayName,
-      };
-    }
-    return {
-      label: "users",
-      title: pubkeys
-        .map((pk) => resolveUserLabel({ pubkey: pk, profiles }))
-        .join(", "),
-      value: `${pubkeys.length} users`,
-    };
-  }
-
-  const query = getToolString(args, ["query"]);
-  if (query) {
-    return {
-      label: "query",
-      title: query,
-      value: shortenMiddle(query, 30),
-    };
-  }
-
-  if (typeof resultRecord.accepted === "boolean") {
-    return {
-      label: "relay",
-      title: resultRecord.accepted ? "accepted" : "rejected",
-      value: resultRecord.accepted ? "accepted" : "rejected",
-    };
-  }
-
-  return null;
-}
-
-function parseToolResultValue(result: string): unknown {
-  const trimmed = result.trim();
-  if (!trimmed) return null;
-
-  try {
-    const parsed = JSON.parse(trimmed);
-    if (typeof parsed !== "string") return parsed;
-    try {
-      return JSON.parse(parsed);
-    } catch {
-      return parsed;
-    }
-  } catch {
-    return null;
-  }
-}
-
-function getChannelChipLabel(channels: Channel[], channelId: string) {
-  const channel = channels.find((candidate) => candidate.id === channelId);
-  return channel ? `#${channel.name}` : `#${shortenMiddle(channelId, 22)}`;
-}
diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
index 2f1f47e1f8..a7d3b99a98 100644
--- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
+++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
@@ -1,32 +1,90 @@
 import * as React from "react";
-import { Bot, Brain, ChevronDown, Radio, TerminalSquare } from "lucide-react";
+import {
+  AlertCircle,
+  Brain,
+  CheckCheck,
+  ChevronDown,
+  CircleDot,
+  Radio,
+  TerminalSquare,
+} from "lucide-react";
 
 import {
   resolveUserLabel,
   type UserProfileLookup,
 } from "@/features/profile/lib/identity";
 import { cn } from "@/shared/lib/cn";
+import { normalizePubkey } from "@/shared/lib/pubkey";
 import { Markdown } from "@/shared/ui/markdown";
+import { Toggle } from "@/shared/ui/toggle";
 import { UserAvatar } from "@/shared/ui/UserAvatar";
-import type { TranscriptItem } from "./agentSessionTypes";
+import type { PromptSection, TranscriptItem } from "./agentSessionTypes";
 import { ToolItem } from "./AgentSessionToolItem";
+import {
+  buildTranscriptDisplayBlocks,
+  formatTurnSetupLabel,
+  turnSetupDetail,
+  turnSetupTimestamp,
+  type TranscriptDisplayBlock,
+  type TranscriptTurnSegment,
+} from "./agentSessionTranscriptGrouping";
 import { formatTranscriptTime } from "./agentSessionUtils";
 import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
 
+const TRANSCRIPT_ACP_SOURCE_STORAGE_KEY = "buzz:show-transcript-acp-source";
+
+/**
+ * Opt-in only: source pills are useful while iterating on observer parsing, but
+ * they should not appear for every local dev session.
+ */
+const SHOW_TRANSCRIPT_ACP_SOURCE = shouldShowTranscriptAcpSource();
+
+function shouldShowTranscriptAcpSource() {
+  const envValue = import.meta.env.VITE_SHOW_TRANSCRIPT_ACP_SOURCE;
+  if (envValue === "1" || envValue === "true") {
+    return true;
+  }
+
+  if (typeof window === "undefined") {
+    return false;
+  }
+
+  try {
+    return (
+      window.localStorage.getItem(TRANSCRIPT_ACP_SOURCE_STORAGE_KEY) === "1"
+    );
+  } catch {
+    return false;
+  }
+}
+
+type AgentTranscriptIdentityProps = {
+  agentAvatarUrl: string | null;
+  agentName: string;
+  agentPubkey: string;
+};
+
 export function AgentSessionTranscriptList({
+  agentAvatarUrl,
   agentName,
+  agentPubkey,
   emptyDescription,
   items,
   profiles,
-}: {
-  agentName: string;
+}: AgentTranscriptIdentityProps & {
   emptyDescription: string;
+  isWorking?: boolean;
   items: TranscriptItem[];
   profiles?: UserProfileLookup;
 }) {
+  const displayBlocks = React.useMemo(
+    () => buildTranscriptDisplayBlocks(items),
+    [items],
+  );
+
   if (items.length === 0) {
     return (
-      <div className="flex min-h-56 flex-col items-center justify-center px-6 py-10 text-center">
+      <div className="flex min-h-40 flex-col items-center justify-center px-6 py-10 text-center">
         <Radio className="mx-auto h-4 w-4 text-muted-foreground" />
         <p className="mt-3 text-sm font-medium">No ACP activity yet</p>
         <p className="mt-1 text-sm text-muted-foreground">{emptyDescription}</p>
@@ -35,37 +93,423 @@ export function AgentSessionTranscriptList({
   }
 
   return (
-    <div
-      aria-label="Live ACP transcript"
-      aria-live="polite"
-      className="w-full py-1"
-      role="log"
-    >
-      {items.map((item) => (
-        <div className="mt-4 first:mt-0" key={item.id}>
-          <TranscriptItemView
+    <div className="w-full">
+      <div
+        aria-label="Live ACP transcript"
+        aria-live="polite"
+        className="w-full py-0.5"
+        role="log"
+      >
+        {displayBlocks.map((block) => (
+          <TranscriptDisplayBlockView
+            agentAvatarUrl={agentAvatarUrl}
             agentName={agentName}
-            item={item}
+            agentPubkey={agentPubkey}
+            block={block}
+            key={getDisplayBlockKey(block)}
             profiles={profiles}
           />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+function TranscriptAcpSourceBadge({ source }: { source: string }) {
+  return (
+    <span
+      className="mb-1 inline-flex max-w-full rounded border border-amber-500/25 bg-amber-500/10 px-1.5 py-0.5 font-mono text-xs leading-none text-amber-800 dark:text-amber-200"
+      data-testid="transcript-acp-source"
+      title={`ACP wire source: ${source}`}
+    >
+      {source}
+    </span>
+  );
+}
+
+function getDisplayBlockKey(block: TranscriptDisplayBlock) {
+  if (block.kind === "single") {
+    return block.item.id;
+  }
+  return `turn:${block.turnId}`;
+}
+
+function TranscriptDisplayBlockView({
+  agentAvatarUrl,
+  agentName,
+  agentPubkey,
+  block,
+  profiles,
+}: AgentTranscriptIdentityProps & {
+  block: TranscriptDisplayBlock;
+  profiles?: UserProfileLookup;
+}) {
+  if (block.kind === "single") {
+    return (
+      <TranscriptItemRow
+        agentAvatarUrl={agentAvatarUrl}
+        agentName={agentName}
+        agentPubkey={agentPubkey}
+        item={block.item}
+        profiles={profiles}
+      />
+    );
+  }
+
+  return (
+    <div
+      className="first:mt-0 mt-2.5"
+      data-testid="transcript-turn-group"
+      data-turn-id={block.turnId}
+    >
+      {block.segments.map((segment) => (
+        <TranscriptTurnSegmentView
+          agentAvatarUrl={agentAvatarUrl}
+          agentName={agentName}
+          agentPubkey={agentPubkey}
+          key={getTurnSegmentKey(block.turnId, segment)}
+          profiles={profiles}
+          segment={segment}
+        />
+      ))}
+    </div>
+  );
+}
+
+function getTurnSegmentKey(turnId: string, segment: TranscriptTurnSegment) {
+  if (segment.kind === "setup") {
+    return `turn:${turnId}:setup`;
+  }
+  if (segment.kind === "prompt") {
+    return `turn:${turnId}:prompt`;
+  }
+  return segment.item.id;
+}
+
+function TranscriptTurnSegmentView({
+  agentAvatarUrl,
+  agentName,
+  agentPubkey,
+  profiles,
+  segment,
+}: AgentTranscriptIdentityProps & {
+  profiles?: UserProfileLookup;
+  segment: TranscriptTurnSegment;
+}) {
+  if (segment.kind === "prompt") {
+    return (
+      <TurnPromptBlock
+        context={segment.context}
+        profiles={profiles}
+        setup={segment.setup}
+        user={segment.user}
+      />
+    );
+  }
+
+  if (segment.kind === "setup") {
+    return <TurnSetupStatus items={segment.items} />;
+  }
+
+  return (
+    <TranscriptItemRow
+      agentAvatarUrl={agentAvatarUrl}
+      agentName={agentName}
+      agentPubkey={agentPubkey}
+      item={segment.item}
+      profiles={profiles}
+    />
+  );
+}
+
+function TurnPromptBlock({
+  context,
+  profiles,
+  setup,
+  user,
+}: {
+  context: Extract<TranscriptItem, { type: "metadata" }> | null;
+  profiles?: UserProfileLookup;
+  setup: Extract<TranscriptItem, { type: "lifecycle" }>[];
+  user: Extract<TranscriptItem, { type: "message" }>;
+}) {
+  return (
+    <div data-testid="transcript-prompt-bundle">
+      {SHOW_TRANSCRIPT_ACP_SOURCE ? (
+        <div className="mb-1 flex flex-wrap gap-1">
+          <TranscriptAcpSourceBadge source="session/prompt:user" />
+          {context ? (
+            <TranscriptAcpSourceBadge source="session/prompt:context" />
+          ) : null}
+        </div>
+      ) : null}
+      <PromptUserMessage
+        context={context}
+        item={user}
+        profiles={profiles}
+        setup={setup}
+      />
+    </div>
+  );
+}
+
+function PromptUserMessage({
+  context = null,
+  item,
+  profiles,
+  setup = [],
+}: {
+  context?: Extract<TranscriptItem, { type: "metadata" }> | null;
+  item: Extract<TranscriptItem, { type: "message" }>;
+  profiles?: UserProfileLookup;
+  setup?: Extract<TranscriptItem, { type: "lifecycle" }>[];
+}) {
+  const [contextOpen, setContextOpen] = React.useState(false);
+  const text = item.text.trim();
+  const authorProfile = item.authorPubkey
+    ? profiles?.[item.authorPubkey.toLowerCase()]
+    : null;
+  const authorLabel = item.authorPubkey
+    ? resolveUserLabel({
+        pubkey: item.authorPubkey,
+        fallbackName: item.title,
+        profiles,
+      })
+    : item.title || "User";
+
+  return (
+    <div
+      className="flex flex-row items-start justify-end"
+      data-role="user-message"
+      data-testid="transcript-user-message"
+    >
+      <UserAvatar
+        avatarUrl={authorProfile?.avatarUrl ?? null}
+        className="order-last ml-2 mt-1 shrink-0"
+        displayName={authorLabel}
+        size="xs"
+      />
+      <div className="group relative flex max-w-[85%] min-w-0 flex-col items-end gap-1">
+        <div className="w-full min-w-0 rounded-2xl bg-muted p-2.5 text-sm leading-relaxed text-foreground">
+          <Markdown content={text || " "} mediaInset tight />
+          {contextOpen && context ? (
+            <PromptContextSections sections={context.sections} setup={setup} />
+          ) : null}
         </div>
+        <TurnSetupFooter
+          context={context}
+          contextOpen={contextOpen}
+          items={setup}
+          onContextOpenChange={setContextOpen}
+          timestamp={item.timestamp}
+        />
+      </div>
+    </div>
+  );
+}
+
+function PromptContextSections({
+  sections,
+  setup,
+}: {
+  sections: PromptSection[];
+  setup: Extract<TranscriptItem, { type: "lifecycle" }>[];
+}) {
+  return (
+    <div
+      className="mt-2 space-y-2 border-t border-border/40 pt-2"
+      data-testid="transcript-prompt-context-sections"
+    >
+      <PromptSetupSummary items={setup} />
+      {sections.map((section) => (
+        <details
+          className="group/section"
+          key={`${section.title}:${section.body.slice(0, 48)}`}
+        >
+          <summary className="inline-flex max-w-full cursor-pointer list-none items-center gap-1.5 text-xs font-medium text-foreground/80">
+            <span className="truncate">{section.title}</span>
+            <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform group-open/section:rotate-180" />
+          </summary>
+          <pre className="mt-1.5 max-h-56 overflow-auto whitespace-pre-wrap wrap-break-word rounded-md bg-background/40 px-2 py-1.5 font-mono text-xs leading-5 text-muted-foreground">
+            {section.body.trim() || "No metadata."}
+          </pre>
+        </details>
       ))}
     </div>
   );
 }
 
-const TranscriptItemView = React.memo(function TranscriptItemView({
+function PromptSetupSummary({
+  items,
+}: {
+  items: Extract<TranscriptItem, { type: "lifecycle" }>[];
+}) {
+  const label = formatTurnSetupLabel(items);
+  const detail = turnSetupDetail(items);
+  const setupText = [label, detail].filter(Boolean).join(" · ");
+
+  if (!setupText) {
+    return null;
+  }
+
+  return (
+    <p
+      className="text-xs leading-5 text-muted-foreground"
+      data-testid="transcript-prompt-setup-summary"
+    >
+      {setupText}
+    </p>
+  );
+}
+
+function TurnSetupFooter({
+  context = null,
+  contextOpen = false,
+  items,
+  onContextOpenChange,
+  timestamp,
+}: {
+  context?: Extract<TranscriptItem, { type: "metadata" }> | null;
+  contextOpen?: boolean;
+  items: Extract<TranscriptItem, { type: "lifecycle" }>[];
+  onContextOpenChange?: (open: boolean) => void;
+  timestamp: string;
+}) {
+  const label = formatTurnSetupLabel(items);
+  const detail = turnSetupDetail(items);
+  const tooltipText = [label, detail].filter(Boolean).join(" · ");
+  const showSetup = items.length > 0;
+  const showContext = context != null && context.sections.length > 0;
+
+  if (!showSetup && !showContext) {
+    return <TranscriptTimestamp timestamp={timestamp} />;
+  }
+
+  const contextToggle = showContext ? (
+    <Toggle
+      aria-label={`${contextOpen ? "Hide" : "Show"} prompt context`}
+      data-testid="transcript-prompt-context-toggle"
+      className="data-[state=on]:bg-primary/10 data-[state=on]:text-primary dark:data-[state=on]:bg-primary/15"
+      onPressedChange={onContextOpenChange}
+      pressed={contextOpen}
+      size="xs"
+      variant="ghost"
+    >
+      {showSetup ? <CheckCheck aria-hidden="true" /> : null}
+      Context
+    </Toggle>
+  ) : null;
+
+  return (
+    <div
+      className="flex items-center gap-1.5 text-muted-foreground/80"
+      data-testid="transcript-turn-setup"
+    >
+      {showContext && showSetup ? (
+        <Tooltip>
+          <TooltipTrigger asChild>{contextToggle}</TooltipTrigger>
+          <TooltipContent side="top">
+            <p>{tooltipText}</p>
+          </TooltipContent>
+        </Tooltip>
+      ) : null}
+      {!showContext && showSetup ? (
+        <Tooltip>
+          <TooltipTrigger asChild>
+            <button
+              className="inline-flex shrink-0 items-center justify-center rounded-sm text-muted-foreground/70 transition-colors hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
+              type="button"
+            >
+              <CheckCheck className="h-3.5 w-3.5" />
+              <span className="sr-only">{tooltipText}</span>
+            </button>
+          </TooltipTrigger>
+          <TooltipContent side="top">
+            <p>{tooltipText}</p>
+          </TooltipContent>
+        </Tooltip>
+      ) : null}
+      {showContext && !showSetup ? contextToggle : null}
+      <TranscriptTimestamp timestamp={timestamp} />
+    </div>
+  );
+}
+
+function TranscriptItemRow({
+  agentAvatarUrl,
   agentName,
+  agentPubkey,
   item,
   profiles,
+}: AgentTranscriptIdentityProps & {
+  item: TranscriptItem;
+  profiles?: UserProfileLookup;
+}) {
+  return (
+    <div
+      className={cn("first:mt-0", getTranscriptItemRowSpacing(item))}
+      key={item.id}
+    >
+      {SHOW_TRANSCRIPT_ACP_SOURCE && item.acpSource ? (
+        <TranscriptAcpSourceBadge source={item.acpSource} />
+      ) : null}
+      <TranscriptItemView
+        agentAvatarUrl={agentAvatarUrl}
+        agentName={agentName}
+        agentPubkey={agentPubkey}
+        item={item}
+        profiles={profiles}
+      />
+    </div>
+  );
+}
+
+function TurnSetupStatus({
+  items,
 }: {
-  agentName: string;
+  items: Extract<TranscriptItem, { type: "lifecycle" }>[];
+}) {
+  const timestamp = turnSetupTimestamp(items);
+  if (items.length === 0 || !timestamp) {
+    return null;
+  }
+
+  return (
+    <div className="mt-1.5 rounded-md px-2 py-1.5">
+      <TurnSetupFooter items={items} timestamp={timestamp} />
+    </div>
+  );
+}
+
+function getTranscriptItemRowSpacing(item: TranscriptItem): string {
+  if (item.type === "message") {
+    return "my-2.5";
+  }
+  if (item.type === "tool") {
+    return "my-1";
+  }
+  return "my-2";
+}
+
+const TranscriptItemView = React.memo(function TranscriptItemView({
+  agentAvatarUrl,
+  agentName,
+  agentPubkey,
+  item,
+  profiles,
+}: AgentTranscriptIdentityProps & {
   item: TranscriptItem;
   profiles?: UserProfileLookup;
 }) {
   if (item.type === "message") {
     return (
-      <MessageItem agentName={agentName} item={item} profiles={profiles} />
+      <MessageItem
+        agentAvatarUrl={agentAvatarUrl}
+        agentName={agentName}
+        agentPubkey={agentPubkey}
+        item={item}
+        profiles={profiles}
+      />
     );
   }
   if (item.type === "tool") {
@@ -81,11 +525,12 @@ const TranscriptItemView = React.memo(function TranscriptItemView({
 });
 
 function MessageItem({
+  agentAvatarUrl,
   agentName,
+  agentPubkey,
   item,
   profiles,
-}: {
-  agentName: string;
+}: AgentTranscriptIdentityProps & {
   item: Extract<TranscriptItem, { type: "message" }>;
   profiles?: UserProfileLookup;
 }) {
@@ -101,34 +546,54 @@ function MessageItem({
         profiles,
       })
     : item.title || "User";
+  const agentProfile = profiles?.[normalizePubkey(agentPubkey)] ?? null;
+  const assistantLabel = resolveUserLabel({
+    pubkey: agentPubkey,
+    fallbackName: agentName,
+    profiles,
+    preferResolvedSelfLabel: true,
+  });
+  const assistantAvatarUrl = agentProfile?.avatarUrl ?? agentAvatarUrl;
 
   return (
     <div
       className={cn(
-        "flex flex-row px-1 py-1 animate-in fade-in duration-200 motion-reduce:animate-none",
+        "flex animate-in fade-in duration-200 motion-reduce:animate-none",
+        isAssistant
+          ? "flex-row px-0 py-1.5"
+          : "flex-row items-start justify-end px-0 py-0.5",
       )}
       data-role={isAssistant ? "assistant-message" : "user-message"}
+      data-testid={
+        isAssistant ? "transcript-assistant-message" : "transcript-user-message"
+      }
     >
       {!isAssistant ? (
         <UserAvatar
           avatarUrl={authorProfile?.avatarUrl ?? null}
-          className="mr-2 mt-1 h-5 w-5 shrink-0 text-[8px]"
+          className="order-last ml-2 mt-1 shrink-0"
           displayName={authorLabel}
           size="xs"
         />
       ) : null}
       <div
         className={cn(
-          "group relative min-w-0 flex flex-col items-start gap-1",
-          isAssistant ? "w-full" : "max-w-[85%]",
+          "group relative flex min-w-0 flex-col gap-1",
+          isAssistant ? "w-full items-start" : "max-w-[85%] items-end",
         )}
       >
         {isAssistant ? (
-          <div className="mb-0.5 flex items-center gap-1 text-xs">
-            <span className="flex h-5 w-5 items-center justify-center">
-              <Bot className="h-4 w-4 text-muted-foreground" />
+          <div className="mb-0.5 flex items-center gap-1.5 text-xs">
+            <UserAvatar
+              avatarUrl={assistantAvatarUrl}
+              className="shrink-0"
+              displayName={assistantLabel}
+              size="xs"
+              testId="transcript-assistant-avatar"
+            />
+            <span className="text-xs font-semibold text-foreground">
+              {assistantLabel}
             </span>
-            <span className="font-normal text-foreground">{agentName}</span>
             <TranscriptTimestamp timestamp={item.timestamp} />
           </div>
         ) : null}
@@ -142,7 +607,7 @@ function MessageItem({
             <Markdown compact content={text || " "} />
           ) : (
             <>
-              <p className="whitespace-pre-wrap break-words">{text}</p>
+              <Markdown content={text || " "} mediaInset tight />
               <TranscriptTimestamp timestamp={item.timestamp} />
             </>
           )}
@@ -158,7 +623,10 @@ function ThoughtItem({
   item: Extract<TranscriptItem, { type: "thought" }>;
 }) {
   return (
-    <details className="group not-prose w-full px-1">
+    <details
+      className="group not-prose w-full rounded-md border border-transparent px-0"
+      data-testid="transcript-thought-item"
+    >
       <summary className="inline-flex max-w-full cursor-pointer list-none items-center gap-1.5 py-px text-muted-foreground">
         <Brain className="h-4 w-4" />
         <span className="truncate text-sm font-medium">{item.title}</span>
@@ -178,11 +646,14 @@ function MetadataItem({
   item: Extract<TranscriptItem, { type: "metadata" }>;
 }) {
   return (
-    <details className="group not-prose w-full px-1">
+    <details
+      className="group not-prose w-full rounded-md border border-border/50 bg-muted/20 px-2 py-1"
+      data-testid="transcript-metadata-item"
+    >
       <summary className="inline-flex max-w-full cursor-pointer list-none items-center gap-1.5 py-px text-muted-foreground">
-        <TerminalSquare className="h-4 w-4" />
-        <span className="truncate text-sm font-medium">{item.title}</span>
-        <span className="shrink-0 text-xs">
+        <TerminalSquare className="h-3.5 w-3.5 shrink-0 opacity-70" />
+        <span className="truncate text-xs font-medium">{item.title}</span>
+        <span className="shrink-0 text-xs text-muted-foreground/70">
           {item.sections.length} section{item.sections.length === 1 ? "" : "s"}
         </span>
         <TranscriptTimestamp timestamp={item.timestamp} />
@@ -198,7 +669,7 @@ function MetadataItem({
               <span className="truncate">{section.title}</span>
               <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-open/section:rotate-180" />
             </summary>
-            <pre className="mt-2 max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md bg-muted/50 px-3 py-2 font-mono text-[11px] leading-5 text-muted-foreground">
+            <pre className="mt-2 max-h-56 overflow-auto whitespace-pre-wrap wrap-break-word rounded-md bg-muted/50 px-3 py-2 font-mono text-xs leading-5 text-muted-foreground">
               {section.body.trim() || "No metadata."}
             </pre>
           </details>
@@ -217,12 +688,20 @@ function LifecycleItem({
   return (
     <div
       className={cn(
-        "flex items-center justify-start gap-1.5 px-1 py-2 text-left text-xs",
-        isError ? "text-destructive" : "text-muted-foreground",
+        "flex items-center justify-start gap-1.5 rounded-md px-2 py-1.5 text-left text-xs",
+        isError
+          ? "border border-destructive/20 bg-destructive/5 text-destructive"
+          : "text-muted-foreground/80",
       )}
+      data-testid="transcript-lifecycle-item"
     >
+      {isError ? (
+        <AlertCircle className="h-3.5 w-3.5 shrink-0" />
+      ) : (
+        <CircleDot className="h-3 w-3 shrink-0 opacity-50" />
+      )}
       <span className="font-medium">{item.title}</span>
-      {item.text ? <span> - {item.text}</span> : null}
+      {item.text ? <span className="opacity-80">· {item.text}</span> : null}
       <TranscriptTimestamp timestamp={item.timestamp} />
     </div>
   );
@@ -248,7 +727,7 @@ function TranscriptTimestamp({ timestamp }: { timestamp: string }) {
   return (
     <Tooltip>
       <TooltipTrigger asChild>
-        <span className="shrink-0 cursor-default text-[11px] text-muted-foreground/60">
+        <span className="shrink-0 cursor-default text-xs text-muted-foreground/60">
           {formatted}
         </span>
       </TooltipTrigger>
diff --git a/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx b/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx
index 3defb7d2bd..e3ba558d1c 100644
--- a/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx
+++ b/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx
@@ -21,14 +21,23 @@ import type {
   ObserverEvent,
   TranscriptItem,
 } from "./agentSessionTypes";
+import {
+  deriveLatestSessionId,
+  resolveRawRailLayout,
+  scopeByChannel,
+} from "./agentSessionPanelLayout";
 import { shorten } from "./agentSessionUtils";
 import { useObserverEvents, useAgentTranscript } from "./useObserverEvents";
 
 type ManagedAgentSessionPanelProps = {
-  agent: Pick<ManagedAgent, "pubkey" | "name" | "status">;
+  agent: Pick<ManagedAgent, "pubkey" | "name" | "status"> & {
+    avatarUrl?: string | null;
+  };
   channelId?: string | null;
   className?: string;
   emptyDescription?: string;
+  isWorking?: boolean;
+  rawLayout?: "responsive" | "exclusive";
   showHeader?: boolean;
   showRaw?: boolean;
   profiles?: UserProfileLookup;
@@ -39,6 +48,8 @@ export function ManagedAgentSessionPanel({
   channelId = null,
   className,
   emptyDescription = "Mention this agent in a channel to watch the next turn.",
+  isWorking = false,
+  rawLayout = "responsive",
   showHeader = true,
   showRaw = true,
   profiles,
@@ -52,29 +63,21 @@ export function ManagedAgentSessionPanel({
 
   // Filter transcript items by channelId (lightweight — items now carry channelId)
   const scopedTranscript = React.useMemo(
-    () =>
-      channelId
-        ? transcript.filter((item) => item.channelId === channelId)
-        : transcript,
+    () => scopeByChannel(transcript, channelId),
     [channelId, transcript],
   );
 
   // Filter raw events by channelId for the RawEventRail
   const scopedEvents = React.useMemo(
-    () =>
-      channelId
-        ? events.filter((event) => event.channelId === channelId)
-        : events,
+    () => scopeByChannel(events, channelId),
     [channelId, events],
   );
 
   // Derive latestSessionId from channel-scoped events
-  const latestSessionId = React.useMemo(() => {
-    for (let i = scopedEvents.length - 1; i >= 0; i--) {
-      if (scopedEvents[i].sessionId) return scopedEvents[i].sessionId;
-    }
-    return null;
-  }, [scopedEvents]);
+  const latestSessionId = React.useMemo(
+    () => deriveLatestSessionId(scopedEvents),
+    [scopedEvents],
+  );
 
   return (
     <section
@@ -93,13 +96,17 @@ export function ManagedAgentSessionPanel({
       ) : null}
 
       <SessionBody
+        agentAvatarUrl={agent.avatarUrl ?? null}
         agentName={agent.name}
+        agentPubkey={agent.pubkey}
         connectionState={connectionState}
         emptyDescription={emptyDescription}
         errorMessage={errorMessage}
         events={scopedEvents}
         hasObserver={hasObserver}
+        isWorking={isWorking}
         profiles={profiles}
+        rawLayout={rawLayout}
         showRaw={showRaw}
         transcript={scopedTranscript}
       />
@@ -143,26 +150,51 @@ function SessionHeader({
 }
 
 function SessionBody({
+  agentAvatarUrl,
   agentName,
+  agentPubkey,
   connectionState,
   emptyDescription,
   errorMessage,
   events,
   hasObserver,
+  isWorking,
   profiles,
+  rawLayout,
   showRaw,
   transcript,
 }: {
+  agentAvatarUrl: string | null;
   agentName: string;
+  agentPubkey: string;
   connectionState: ConnectionState;
   emptyDescription: string;
   errorMessage: string | null;
   events: ObserverEvent[];
   hasObserver: boolean;
+  isWorking: boolean;
   profiles?: UserProfileLookup;
+  rawLayout: "responsive" | "exclusive";
   showRaw: boolean;
   transcript: TranscriptItem[];
 }) {
+  const rawRail = resolveRawRailLayout(showRaw, rawLayout);
+
+  if (rawRail.mode === "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 ? (
@@ -172,18 +204,21 @@ function SessionBody({
       ) : (
         <div
           className={cn(
-            showRaw
+            rawRail.mode === "side"
               ? "mt-4 grid gap-4 xl:grid-cols-[minmax(0,1fr)_20rem]"
               : "mt-0",
           )}
         >
           <AgentSessionTranscriptList
+            agentAvatarUrl={agentAvatarUrl}
             agentName={agentName}
+            agentPubkey={agentPubkey}
             emptyDescription={emptyDescription}
+            isWorking={isWorking}
             items={transcript}
             profiles={profiles}
           />
-          {showRaw ? <RawEventRail events={events} /> : null}
+          {rawRail.mode === "side" ? <RawEventRail events={events} /> : null}
         </div>
       )}
 
diff --git a/desktop/src/features/agents/ui/RawEventRail.tsx b/desktop/src/features/agents/ui/RawEventRail.tsx
index 74a655b77e..153b77e77c 100644
--- a/desktop/src/features/agents/ui/RawEventRail.tsx
+++ b/desktop/src/features/agents/ui/RawEventRail.tsx
@@ -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 font-mono text-xs 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>
   );
 }
diff --git a/desktop/src/features/agents/ui/agentSessionPanelLayout.test.mjs b/desktop/src/features/agents/ui/agentSessionPanelLayout.test.mjs
new file mode 100644
index 0000000000..96af79d16f
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionPanelLayout.test.mjs
@@ -0,0 +1,85 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  deriveLatestSessionId,
+  resolveRawRailLayout,
+  scopeByChannel,
+} from "./agentSessionPanelLayout.ts";
+
+// ---- scopeByChannel ----
+
+const items = [
+  { id: "a", channelId: "channel-1" },
+  { id: "b", channelId: "channel-2" },
+  { id: "c", channelId: "channel-1" },
+];
+
+test("scopeByChannel returns the input unchanged when channelId is null", () => {
+  assert.equal(scopeByChannel(items, null), items);
+});
+
+test("scopeByChannel returns the input unchanged when channelId is undefined", () => {
+  assert.equal(scopeByChannel(items, undefined), items);
+});
+
+test("scopeByChannel filters items down to the requested channel", () => {
+  const scoped = scopeByChannel(items, "channel-1");
+  assert.deepEqual(
+    scoped.map((item) => item.id),
+    ["a", "c"],
+  );
+});
+
+test("scopeByChannel returns an empty array when no item matches", () => {
+  assert.deepEqual(scopeByChannel(items, "channel-99"), []);
+});
+
+// ---- deriveLatestSessionId ----
+
+test("deriveLatestSessionId returns null for an empty list", () => {
+  assert.equal(deriveLatestSessionId([]), null);
+});
+
+test("deriveLatestSessionId returns the last event's sessionId", () => {
+  const events = [
+    { seq: 1, sessionId: "sess-1" },
+    { seq: 2, sessionId: "sess-2" },
+  ];
+  assert.equal(deriveLatestSessionId(events), "sess-2");
+});
+
+test("deriveLatestSessionId skips trailing events without a sessionId", () => {
+  const events = [
+    { seq: 1, sessionId: "sess-1" },
+    { seq: 2, sessionId: null },
+    { seq: 3, sessionId: undefined },
+  ];
+  assert.equal(deriveLatestSessionId(events), "sess-1");
+});
+
+test("deriveLatestSessionId returns null when no event carries a sessionId", () => {
+  const events = [{ seq: 1, sessionId: null }, { seq: 2 }];
+  assert.equal(deriveLatestSessionId(events), null);
+});
+
+// ---- resolveRawRailLayout (raw-ACP view toggle) ----
+
+test("resolveRawRailLayout hides the rail when showRaw is off", () => {
+  assert.deepEqual(resolveRawRailLayout(false, "responsive"), {
+    mode: "hidden",
+  });
+  assert.deepEqual(resolveRawRailLayout(false, "exclusive"), {
+    mode: "hidden",
+  });
+});
+
+test("resolveRawRailLayout renders the rail exclusively when toggled on in exclusive layout", () => {
+  assert.deepEqual(resolveRawRailLayout(true, "exclusive"), {
+    mode: "exclusive",
+  });
+});
+
+test("resolveRawRailLayout renders the rail beside the transcript in responsive layout", () => {
+  assert.deepEqual(resolveRawRailLayout(true, "responsive"), { mode: "side" });
+});
diff --git a/desktop/src/features/agents/ui/agentSessionPanelLayout.ts b/desktop/src/features/agents/ui/agentSessionPanelLayout.ts
new file mode 100644
index 0000000000..e0aaeb203f
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionPanelLayout.ts
@@ -0,0 +1,48 @@
+import type { ObserverEvent } from "./agentSessionTypes";
+
+/**
+ * Filter transcript items or raw observer events down to a single channel.
+ * A null `channelId` means "no scoping" — the input is returned as-is.
+ */
+export function scopeByChannel<T extends { channelId?: string | null }>(
+  items: readonly T[],
+  channelId: string | null | undefined,
+): T[] {
+  if (!channelId) return items as T[];
+  return items.filter((item) => item.channelId === channelId);
+}
+
+/**
+ * Derive the most recent session id from a list of observer events by
+ * scanning from the end. Returns null when no event carries a sessionId.
+ */
+export function deriveLatestSessionId(
+  events: readonly ObserverEvent[],
+): string | null {
+  for (let i = events.length - 1; i >= 0; i--) {
+    const sessionId = events[i]?.sessionId;
+    if (sessionId) return sessionId;
+  }
+  return null;
+}
+
+export type RawRailLayout =
+  | { mode: "hidden" }
+  | { mode: "exclusive" }
+  | { mode: "side" };
+
+/**
+ * Decide how the raw-ACP event rail should be rendered relative to the
+ * transcript:
+ * - `hidden`    — raw view is off
+ * - `exclusive` — raw rail replaces the transcript entirely
+ * - `side`      — raw rail renders alongside the transcript (responsive)
+ */
+export function resolveRawRailLayout(
+  showRaw: boolean,
+  rawLayout: "responsive" | "exclusive",
+): RawRailLayout {
+  if (!showRaw) return { mode: "hidden" };
+  if (rawLayout === "exclusive") return { mode: "exclusive" };
+  return { mode: "side" };
+}
diff --git a/desktop/src/features/agents/ui/agentSessionToolItemHelpers.test.mjs b/desktop/src/features/agents/ui/agentSessionToolItemHelpers.test.mjs
new file mode 100644
index 0000000000..52b548ec6b
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionToolItemHelpers.test.mjs
@@ -0,0 +1,128 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  formatDurationMs,
+  getToolDurationDisplay,
+  isInlineImageData,
+  parseToolResultValue,
+} from "./agentSessionUtils.ts";
+
+// ---- isInlineImageData (dual-layer image-scheme security guard) ----
+
+test("isInlineImageData accepts data:image/ URIs", () => {
+  const dataUri =
+    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgYGAAAAAEAAH2FzhVAAAAAElFTkSuQmCC";
+  assert.equal(isInlineImageData(dataUri), true);
+});
+
+test("isInlineImageData rejects non-image data: schemes (no passthrough widening)", () => {
+  // A non-image data: URI must NOT be treated as a safe inline image —
+  // it has to fall through to the relay rewrite path.
+  assert.equal(
+    isInlineImageData(
+      "data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==",
+    ),
+    false,
+  );
+  assert.equal(isInlineImageData("data:application/json;base64,e30="), false);
+});
+
+test("isInlineImageData rejects relay-relative and absolute media URLs", () => {
+  assert.equal(isInlineImageData("/media/abc123.png"), false);
+  assert.equal(isInlineImageData("https://relay.example/media/abc.png"), false);
+});
+
+// ---- formatDurationMs ----
+
+test("formatDurationMs returns null for negative input", () => {
+  assert.equal(formatDurationMs(-1), null);
+});
+
+test("formatDurationMs renders sub-10s with one decimal", () => {
+  assert.equal(formatDurationMs(400), "0.4s");
+  assert.equal(formatDurationMs(9900), "9.9s");
+});
+
+test("formatDurationMs rounds 10s..60s to whole seconds", () => {
+  assert.equal(formatDurationMs(12300), "12s");
+  assert.equal(formatDurationMs(59400), "59s");
+});
+
+test("formatDurationMs renders minutes and seconds", () => {
+  assert.equal(formatDurationMs(90000), "1m 30s");
+  assert.equal(formatDurationMs(120000), "2m");
+});
+
+test("formatDurationMs carries a rounded 60s into the next minute", () => {
+  // 89.7s rounds the seconds component to 60, which must carry to 1m 30s
+  assert.equal(formatDurationMs(89700), "1m 30s");
+});
+
+// ---- parseToolResultValue (JSON double-parse) ----
+
+test("parseToolResultValue returns null for empty/whitespace", () => {
+  assert.equal(parseToolResultValue(""), null);
+  assert.equal(parseToolResultValue("   "), null);
+});
+
+test("parseToolResultValue parses a JSON object", () => {
+  assert.deepEqual(parseToolResultValue('{"duration_ms":123}'), {
+    duration_ms: 123,
+  });
+});
+
+test("parseToolResultValue unwraps a double-encoded JSON string", () => {
+  // The result is a JSON string that itself contains JSON.
+  const doubleEncoded = JSON.stringify(JSON.stringify({ ok: true }));
+  assert.deepEqual(parseToolResultValue(doubleEncoded), { ok: true });
+});
+
+test("parseToolResultValue returns the inner string when it is not JSON", () => {
+  const wrapped = JSON.stringify("plain text");
+  assert.equal(parseToolResultValue(wrapped), "plain text");
+});
+
+test("parseToolResultValue returns null for invalid JSON", () => {
+  assert.equal(parseToolResultValue("not json {"), null);
+});
+
+// ---- getToolDurationDisplay (fallback chain) ----
+
+const startedAt = "2026-06-14T19:00:00.000Z";
+const completedAt = "2026-06-14T19:00:02.000Z";
+
+test("getToolDurationDisplay prefers start/complete timestamps", () => {
+  assert.equal(
+    getToolDurationDisplay({ startedAt, completedAt, result: "" }),
+    "2.0s",
+  );
+});
+
+test("getToolDurationDisplay falls back to duration_ms in the result payload", () => {
+  assert.equal(
+    getToolDurationDisplay({
+      startedAt: null,
+      completedAt: null,
+      result: JSON.stringify({ duration_ms: 3500 }),
+    }),
+    "3.5s",
+  );
+});
+
+test("getToolDurationDisplay falls back to elapsed_ms when duration_ms absent", () => {
+  assert.equal(
+    getToolDurationDisplay({
+      result: JSON.stringify({ elapsed_ms: 65000 }),
+    }),
+    "1m 5s",
+  );
+});
+
+test("getToolDurationDisplay returns null when no duration is available", () => {
+  assert.equal(getToolDurationDisplay({ result: "" }), null);
+  assert.equal(
+    getToolDurationDisplay({ result: JSON.stringify({ other: 1 }) }),
+    null,
+  );
+});
diff --git a/desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs b/desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs
new file mode 100644
index 0000000000..fe25dc2cae
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs
@@ -0,0 +1,157 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { buildCompactToolSummary } from "./agentSessionToolSummary.ts";
+
+const baseTimestamp = "2026-06-14T19:00:00.000Z";
+
+function makeTool(overrides = {}) {
+  return {
+    id: "tool:1",
+    type: "tool",
+    title: "Tool call",
+    toolName: "shell",
+    buzzToolName: null,
+    status: "completed",
+    args: {},
+    result: "",
+    isError: false,
+    timestamp: baseTimestamp,
+    startedAt: baseTimestamp,
+    completedAt: "2026-06-14T19:00:01.000Z",
+    ...overrides,
+  };
+}
+
+test("buildCompactToolSummary formats Buzz send_message preview", () => {
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "send_message",
+      buzzToolName: "send_message",
+      title: "Send Message",
+      args: { content: "Hello team" },
+    }),
+  );
+
+  assert.equal(summary.kind, "buzz");
+  assert.equal(summary.label, "Send Message");
+  assert.equal(summary.preview, "Hello team");
+  assert.equal(summary.presentation, "message");
+});
+
+test("buildCompactToolSummary treats buzz messages send commands as messages", () => {
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "buzz-dev-mcp__shell",
+      args: {
+        command:
+          'buzz --format compact messages send --channel channel-1 --content "@Ned are you working"',
+      },
+    }),
+  );
+
+  assert.equal(summary.kind, "shell");
+  assert.equal(summary.label, "Send Message");
+  assert.equal(summary.preview, "@Ned are you working");
+  assert.equal(summary.presentation, "message");
+});
+
+test("buildCompactToolSummary extracts simple piped buzz message content", () => {
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "shell",
+      args: {
+        command:
+          'echo "hello from stdin" | ./target/release/buzz messages send --channel channel-1 --content -',
+      },
+    }),
+  );
+
+  assert.equal(summary.label, "Send Message");
+  assert.equal(summary.preview, "hello from stdin");
+  assert.equal(summary.presentation, "message");
+});
+
+test("buildCompactToolSummary formats shell command preview", () => {
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "buzz-dev-mcp__shell",
+      args: { command: "git status" },
+    }),
+  );
+
+  assert.equal(summary.label, "Ran command");
+  assert.equal(summary.preview, "git status");
+  assert.equal(summary.presentation, "inline");
+});
+
+test("buildCompactToolSummary formats view_image thumbnail source", () => {
+  const source =
+    "https://sprout-oss.stage.blox.sqprod.co/media/ffd1b2721f2d52e19f0ca2be9aa7842cdec5b4e0215aaab2a67c26a2a76a6a83.png";
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "buzz-dev-mcp__view_image",
+      args: { source },
+    }),
+  );
+
+  assert.equal(summary.label, "Viewed image");
+  assert.equal(summary.thumbnailSrc, source);
+  assert.equal(summary.preview, source);
+});
+
+test("buildCompactToolSummary uses basename for local view_image paths", () => {
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "view_image",
+      args: { source: "desktop/assets/screenshot.png" },
+    }),
+  );
+
+  assert.equal(summary.thumbnailSrc, null);
+  assert.equal(summary.preview, "screenshot.png");
+});
+
+test("buildCompactToolSummary formats read_file path preview", () => {
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "read_file",
+      args: { path: "desktop/src/app/App.tsx" },
+    }),
+  );
+
+  assert.equal(summary.label, "Read file");
+  assert.equal(summary.preview, "desktop/src/app/App.tsx");
+});
+
+test("buildCompactToolSummary formats todo list preview", () => {
+  const summary = buildCompactToolSummary(
+    makeTool({
+      toolName: "todo",
+      args: {
+        todos: [
+          { text: "Ship compact summaries", done: false },
+          { text: "Verify UI", done: false },
+        ],
+      },
+    }),
+  );
+
+  assert.equal(summary.label, "Updated todos");
+  assert.equal(summary.preview, "Ship compact summaries (+1)");
+});
+
+test("buildCompactToolSummary uses running and failed labels", () => {
+  assert.equal(
+    buildCompactToolSummary(
+      makeTool({ toolName: "str_replace", status: "executing" }),
+    ).label,
+    "Editing file",
+  );
+  assert.equal(
+    buildCompactToolSummary(
+      makeTool({ toolName: "str_replace", status: "failed", isError: true }),
+    ).label,
+    "Edit failed",
+  );
+});
diff --git a/desktop/src/features/agents/ui/agentSessionToolSummary.ts b/desktop/src/features/agents/ui/agentSessionToolSummary.ts
new file mode 100644
index 0000000000..1513e10174
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionToolSummary.ts
@@ -0,0 +1,550 @@
+import type { ToolStatus, TranscriptItem } from "./agentSessionTypes";
+import {
+  formatToolTitle,
+  getBuzzToolInfo,
+  isGenericToolTitle,
+  normalizeToolNameText,
+} from "./agentSessionToolCatalog";
+import {
+  asRecord,
+  getToolString,
+  getToolStringList,
+} from "./agentSessionUtils";
+
+export type CompactToolKind =
+  | "shell"
+  | "read_file"
+  | "view_image"
+  | "str_replace"
+  | "todo"
+  | "stop_hook"
+  | "post_compact_hook"
+  | "dev_mcp"
+  | "buzz"
+  | "generic";
+
+export type CompactToolSummary = {
+  kind: CompactToolKind;
+  label: string;
+  preview: string | null;
+  /** When set, the compact row renders a tiny image instead of text preview. */
+  thumbnailSrc: string | null;
+  presentation: "inline" | "message";
+};
+
+const DEVELOPER_TOOL_BASES = new Set([
+  "shell",
+  "read_file",
+  "view_image",
+  "str_replace",
+  "todo",
+  "stop",
+  "postcompact",
+]);
+
+type ToolItem = Extract<TranscriptItem, { type: "tool" }>;
+
+/** Build the muted compact summary label and preview for any tool row. */
+export function buildCompactToolSummary(item: ToolItem): CompactToolSummary {
+  const kind = resolveCompactToolKind(item);
+  const messageSendPreview = extractMessageSendPreview(item, kind);
+  if (messageSendPreview !== undefined) {
+    return {
+      kind,
+      label: compactMessageSendLabel(item.status, item.isError),
+      preview: messageSendPreview,
+      thumbnailSrc: null,
+      presentation: "message",
+    };
+  }
+
+  const { preview, thumbnailSrc } = extractCompactToolPreview(item, kind);
+  return {
+    kind,
+    label: compactToolLabel(kind, item, item.status, item.isError),
+    preview,
+    thumbnailSrc,
+    presentation: "inline",
+  };
+}
+
+function resolveCompactToolKind(item: ToolItem): CompactToolKind {
+  const developerKind = resolveDeveloperToolKind(item);
+  if (developerKind) {
+    return developerKind;
+  }
+
+  for (const value of [item.buzzToolName, item.toolName, item.title]) {
+    if (value && getBuzzToolInfo(value)) {
+      return "buzz";
+    }
+  }
+
+  return "generic";
+}
+
+function resolveDeveloperToolKind(item: ToolItem): CompactToolKind | null {
+  for (const value of [item.toolName, item.title, item.buzzToolName]) {
+    const kind = classifyDeveloperToolName(value);
+    if (kind) return kind;
+  }
+  return null;
+}
+
+function classifyDeveloperToolName(
+  value: string | null | undefined,
+): CompactToolKind | null {
+  if (!value) return null;
+
+  const normalized = normalizeToolNameText(value);
+  const base = stripMcpServerPrefix(normalized);
+
+  if (base === "shell" || normalized.endsWith("_shell")) {
+    return "shell";
+  }
+  if (base === "read_file") return "read_file";
+  if (base === "view_image") return "view_image";
+  if (base === "str_replace") return "str_replace";
+  if (base === "todo") return "todo";
+  if (base === "stop") return "stop_hook";
+  if (base === "postcompact") return "post_compact_hook";
+
+  if (DEVELOPER_TOOL_BASES.has(base)) {
+    return "dev_mcp";
+  }
+
+  if (normalized.includes("buzz_dev_mcp")) {
+    return "dev_mcp";
+  }
+
+  return null;
+}
+
+function stripMcpServerPrefix(normalized: string): string {
+  return normalized.replace(/^buzz_dev_mcp_/, "");
+}
+
+function compactToolLabel(
+  kind: CompactToolKind,
+  item: ToolItem,
+  status: ToolStatus,
+  isError: boolean,
+): string {
+  const failed = isError || status === "failed";
+  const running = status === "executing" || status === "pending";
+
+  if (kind === "buzz") {
+    const title = formatToolTitle(
+      item.buzzToolName ?? item.toolName,
+      item.title,
+    );
+    if (failed) return `${title} failed`;
+    if (running) return title;
+    return title;
+  }
+
+  const labels: Record<
+    Exclude<CompactToolKind, "buzz">,
+    { completed: string; running: string; failed: string }
+  > = {
+    generic: {
+      completed: "Ran tool",
+      running: "Running tool",
+      failed: "Tool failed",
+    },
+    ...developerToolLabels(),
+  };
+
+  const set = labels[kind];
+  if (failed) return set.failed;
+  if (running) return set.running;
+  return set.completed;
+}
+
+function developerToolLabels(): Record<
+  Exclude<CompactToolKind, "buzz" | "generic">,
+  { completed: string; running: string; failed: string }
+> {
+  return {
+    shell: {
+      completed: "Ran command",
+      running: "Running command",
+      failed: "Command failed",
+    },
+    read_file: {
+      completed: "Read file",
+      running: "Reading file",
+      failed: "Read failed",
+    },
+    view_image: {
+      completed: "Viewed image",
+      running: "Viewing image",
+      failed: "View failed",
+    },
+    str_replace: {
+      completed: "Edited file",
+      running: "Editing file",
+      failed: "Edit failed",
+    },
+    todo: {
+      completed: "Updated todos",
+      running: "Updating todos",
+      failed: "Todo update failed",
+    },
+    stop_hook: {
+      completed: "Checked todos",
+      running: "Checking todos",
+      failed: "Todo check failed",
+    },
+    post_compact_hook: {
+      completed: "Synced todos",
+      running: "Syncing todos",
+      failed: "Todo sync failed",
+    },
+    dev_mcp: {
+      completed: "Ran tool",
+      running: "Running tool",
+      failed: "Tool failed",
+    },
+  };
+}
+
+function compactMessageSendLabel(status: ToolStatus, isError: boolean) {
+  if (isError || status === "failed") {
+    return "Send Message failed";
+  }
+  return "Send Message";
+}
+
+type CompactToolPreview = {
+  preview: string | null;
+  thumbnailSrc: string | null;
+};
+
+function extractCompactToolPreview(
+  item: ToolItem,
+  kind: CompactToolKind,
+): CompactToolPreview {
+  const args = item.args;
+
+  switch (kind) {
+    case "shell":
+      return textPreview(getToolString(args, ["command"]));
+    case "read_file":
+    case "str_replace":
+      return textPreview(getToolString(args, ["path"]));
+    case "view_image":
+      return getViewImagePreview(getToolString(args, ["source"]));
+    case "todo":
+      return textPreview(getTodoPreview(args));
+    case "stop_hook":
+    case "post_compact_hook":
+      return emptyPreview();
+    case "dev_mcp":
+    case "generic":
+      return textPreview(
+        getToolString(args, [
+          "command",
+          "path",
+          "source",
+          "query",
+          "name",
+          "content",
+          "message",
+        ]) ??
+          (item.title && !isGenericToolTitle(item.title) ? item.title : null),
+      );
+    case "buzz":
+      return textPreview(extractBuzzToolPreview(args));
+  }
+}
+
+function extractBuzzToolPreview(args: Record<string, unknown>): string | null {
+  const content = getToolString(args, ["content", "message", "text", "body"]);
+  if (content) {
+    return content;
+  }
+
+  const query = getToolString(args, ["query", "search"]);
+  if (query) {
+    return query;
+  }
+
+  const channelId = getToolString(args, ["channel_id", "channelId"]);
+  if (channelId) {
+    return channelId;
+  }
+
+  const workflowId = getToolString(args, ["workflow_id", "workflowId"]);
+  if (workflowId) {
+    return workflowId;
+  }
+
+  const pubkeys = getToolStringList(args, ["pubkeys", "pubkey"]);
+  if (pubkeys.length === 1) {
+    return pubkeys[0];
+  }
+  if (pubkeys.length > 1) {
+    return `${pubkeys.length} users`;
+  }
+
+  return getToolString(args, ["event_id", "eventId", "name"]);
+}
+
+function extractMessageSendPreview(
+  item: ToolItem,
+  kind: CompactToolKind,
+): string | null | undefined {
+  if (isBuzzSendMessageTool(item)) {
+    return extractBuzzToolMessageContent(item.args);
+  }
+
+  if (kind !== "shell") {
+    return undefined;
+  }
+
+  const command = getToolString(item.args, ["command"]);
+  return command ? extractBuzzCliSendMessageContent(command) : undefined;
+}
+
+function isBuzzSendMessageTool(item: ToolItem) {
+  return [item.buzzToolName, item.toolName, item.title].some((value) => {
+    if (!value) return false;
+    return normalizeToolNameText(value) === "send_message";
+  });
+}
+
+function extractBuzzToolMessageContent(
+  args: Record<string, unknown>,
+): string | null {
+  return getToolString(args, ["content", "message", "text", "body"]);
+}
+
+function extractBuzzCliSendMessageContent(
+  command: string,
+): string | null | undefined {
+  const tokens = tokenizeShellCommand(command);
+  const commandRange = findBuzzMessagesSendCommand(tokens);
+  if (!commandRange) {
+    return undefined;
+  }
+
+  const content = getFlagValue(tokens, commandRange.sendIndex + 1, "--content");
+  if (!content) {
+    return null;
+  }
+  if (content !== "-") {
+    return content;
+  }
+
+  const pipedContent = extractSimpleEchoPipeContent(
+    tokens,
+    commandRange.buzzIndex,
+  );
+  return pipedContent ?? null;
+}
+
+function tokenizeShellCommand(command: string): string[] {
+  const tokens: string[] = [];
+  let current = "";
+  let quote: "'" | '"' | null = null;
+  let escaping = false;
+
+  const pushCurrent = () => {
+    if (current.length > 0) {
+      tokens.push(current);
+      current = "";
+    }
+  };
+
+  for (const char of command) {
+    if (escaping) {
+      current += char;
+      escaping = false;
+      continue;
+    }
+
+    if (char === "\\" && quote !== "'") {
+      escaping = true;
+      continue;
+    }
+
+    if (quote) {
+      if (char === quote) {
+        quote = null;
+      } else {
+        current += char;
+      }
+      continue;
+    }
+
+    if (char === "'" || char === '"') {
+      quote = char;
+      continue;
+    }
+
+    if (/\s/.test(char)) {
+      pushCurrent();
+      continue;
+    }
+
+    if (char === "|" || char === ";" || char === "&") {
+      pushCurrent();
+      tokens.push(char);
+      continue;
+    }
+
+    current += char;
+  }
+
+  if (escaping) {
+    current += "\\";
+  }
+  pushCurrent();
+  return tokens;
+}
+
+function findBuzzMessagesSendCommand(
+  tokens: string[],
+): { buzzIndex: number; sendIndex: number } | null {
+  for (let i = 0; i < tokens.length; i++) {
+    if (!isBuzzExecutable(tokens[i])) {
+      continue;
+    }
+
+    const messagesIndex = tokens.indexOf("messages", i + 1);
+    if (messagesIndex === -1) {
+      continue;
+    }
+    if (
+      messagesIndex > i &&
+      hasCommandSeparator(tokens, i + 1, messagesIndex)
+    ) {
+      continue;
+    }
+    if (tokens[messagesIndex + 1] === "send") {
+      return { buzzIndex: i, sendIndex: messagesIndex + 1 };
+    }
+  }
+
+  return null;
+}
+
+function isBuzzExecutable(token: string) {
+  return token === "buzz" || token.split(/[\\/]/).pop() === "buzz";
+}
+
+function hasCommandSeparator(tokens: string[], start: number, end: number) {
+  for (let i = start; i < end; i++) {
+    if (isCommandSeparator(tokens[i])) {
+      return true;
+    }
+  }
+  return false;
+}
+
+function isCommandSeparator(token: string) {
+  return token === "|" || token === ";" || token === "&";
+}
+
+function getFlagValue(tokens: string[], start: number, flag: string) {
+  for (let i = start; i < tokens.length; i++) {
+    const token = tokens[i];
+    if (isCommandSeparator(token)) {
+      return null;
+    }
+    if (token === flag) {
+      return tokens[i + 1] && !isCommandSeparator(tokens[i + 1])
+        ? tokens[i + 1]
+        : null;
+    }
+    if (token.startsWith(`${flag}=`)) {
+      return token.slice(flag.length + 1);
+    }
+  }
+  return null;
+}
+
+function extractSimpleEchoPipeContent(
+  tokens: string[],
+  buzzIndex: number,
+): string | null {
+  const pipeIndex = tokens.lastIndexOf("|", buzzIndex);
+  if (pipeIndex <= 0) {
+    return null;
+  }
+
+  const echoStart = findSegmentStart(tokens, pipeIndex - 1);
+  const leftSegment = tokens.slice(echoStart, pipeIndex);
+  if (leftSegment[0] !== "echo") {
+    return null;
+  }
+
+  const contentTokens = leftSegment
+    .slice(1)
+    .filter((token) => !token.startsWith("-"));
+  return contentTokens.length > 0 ? contentTokens.join(" ") : null;
+}
+
+function findSegmentStart(tokens: string[], beforeIndex: number) {
+  for (let i = beforeIndex; i >= 0; i--) {
+    if (isCommandSeparator(tokens[i])) {
+      return i + 1;
+    }
+  }
+  return 0;
+}
+
+function textPreview(preview: string | null): CompactToolPreview {
+  return { preview, thumbnailSrc: null };
+}
+
+function emptyPreview(): CompactToolPreview {
+  return { preview: null, thumbnailSrc: null };
+}
+
+function getViewImagePreview(source: string | null): CompactToolPreview {
+  if (!source) {
+    return emptyPreview();
+  }
+
+  const trimmed = source.trim();
+  if (
+    trimmed.startsWith("data:image/") ||
+    trimmed.startsWith("http://") ||
+    trimmed.startsWith("https://")
+  ) {
+    return {
+      preview: trimmed,
+      thumbnailSrc: trimmed,
+    };
+  }
+
+  const basename = trimmed.split(/[/\\]/).pop() ?? trimmed;
+  return {
+    preview: basename,
+    thumbnailSrc: null,
+  };
+}
+
+function getTodoPreview(args: Record<string, unknown>): string | null {
+  const todos = args.todos;
+  if (!Array.isArray(todos)) {
+    return "todo list";
+  }
+  if (todos.length === 0) {
+    return "empty list";
+  }
+
+  const first = todos[0];
+  const firstText =
+    first && typeof first === "object"
+      ? getToolString(asRecord(first), ["text"])
+      : null;
+
+  if (firstText) {
+    return todos.length > 1 ? `${firstText} (+${todos.length - 1})` : firstText;
+  }
+
+  return `${todos.length} item${todos.length === 1 ? "" : "s"}`;
+}
diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs
new file mode 100644
index 0000000000..a5cd0c96b1
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs
@@ -0,0 +1,155 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { buildTranscript } from "./agentSessionTranscript.ts";
+
+const turnId = "turn-abc";
+const sessionId = "sess-1";
+const channelId = "channel-1";
+const baseTimestamp = "2026-06-14T22:20:23.000Z";
+
+function makeTurnEvents() {
+  return [
+    {
+      seq: 1,
+      timestamp: baseTimestamp,
+      kind: "turn_started",
+      agentIndex: 0,
+      channelId,
+      sessionId: null,
+      turnId,
+      payload: { triggeringEventIds: ["event-1"] },
+    },
+    {
+      seq: 2,
+      timestamp: baseTimestamp,
+      kind: "session_resolved",
+      agentIndex: 0,
+      channelId,
+      sessionId,
+      turnId,
+      payload: { sessionId, isNewSession: false },
+    },
+    {
+      seq: 3,
+      timestamp: baseTimestamp,
+      kind: "acp_write",
+      agentIndex: 0,
+      channelId,
+      sessionId,
+      turnId,
+      payload: {
+        method: "session/prompt",
+        params: {
+          sessionId,
+          prompt: [
+            {
+              type: "text",
+              text: "[Buzz event: message]\nContent: @Ned deliberate, wider pass\nFrom: Tyler hex: abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
+            },
+          ],
+        },
+      },
+    },
+    {
+      seq: 4,
+      timestamp: "2026-06-14T22:20:47.000Z",
+      kind: "acp_read",
+      agentIndex: 0,
+      channelId,
+      sessionId,
+      turnId,
+      payload: {
+        method: "session/update",
+        params: {
+          update: {
+            sessionUpdate: "agent_message_chunk",
+            messageId: "msg-1",
+            content: [{ type: "text", text: "On it." }],
+          },
+        },
+      },
+    },
+  ];
+}
+
+test("buildTranscript attaches turnId and sessionId to generated items", () => {
+  const items = buildTranscript(makeTurnEvents());
+
+  assert.ok(items.length >= 4);
+  for (const item of items) {
+    assert.equal(item.turnId, turnId);
+    assert.equal(item.channelId, channelId);
+  }
+
+  const sessionResolved = items.find(
+    (item) =>
+      item.type === "lifecycle" && item.acpSource === "session_resolved",
+  );
+  assert.equal(sessionResolved?.sessionId, sessionId);
+
+  const userPrompt = items.find(
+    (item) =>
+      item.type === "message" &&
+      item.role === "user" &&
+      item.acpSource === "session/prompt:user",
+  );
+  assert.ok(userPrompt);
+  assert.equal(userPrompt.sessionId, sessionId);
+});
+
+test("buildTranscript tags assistant chunks with agent_message_chunk", () => {
+  const items = buildTranscript([
+    {
+      seq: 1,
+      timestamp: "2026-06-14T20:47:14.000Z",
+      kind: "acp_read",
+      agentIndex: 0,
+      channelId: "channel-1",
+      sessionId: "sess-1",
+      turnId: "turn-1",
+      payload: {
+        method: "session/update",
+        params: {
+          update: {
+            sessionUpdate: "agent_message_chunk",
+            messageId: "msg-1",
+            content: [{ type: "text", text: "Marge is summoned." }],
+          },
+        },
+      },
+    },
+  ]);
+
+  assert.equal(items.length, 1);
+  assert.equal(items[0]?.type, "message");
+  assert.equal(items[0]?.acpSource, "agent_message_chunk");
+});
+
+test("buildTranscript tags thought chunks with agent_thought_chunk", () => {
+  const items = buildTranscript([
+    {
+      seq: 2,
+      timestamp: "2026-06-14T20:47:15.000Z",
+      kind: "acp_read",
+      agentIndex: 0,
+      channelId: "channel-1",
+      sessionId: "sess-1",
+      turnId: "turn-1",
+      payload: {
+        method: "session/update",
+        params: {
+          update: {
+            sessionUpdate: "agent_thought_chunk",
+            messageId: "thought-1",
+            content: [{ type: "text", text: "Considering next step." }],
+          },
+        },
+      },
+    },
+  ]);
+
+  assert.equal(items.length, 1);
+  assert.equal(items[0]?.type, "thought");
+  assert.equal(items[0]?.acpSource, "agent_thought_chunk");
+});
diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.ts b/desktop/src/features/agents/ui/agentSessionTranscript.ts
index 60cf7f62b4..945945526e 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscript.ts
+++ b/desktop/src/features/agents/ui/agentSessionTranscript.ts
@@ -108,6 +108,12 @@ function sealOpenMessages(d: TranscriptDraft) {
   }
 }
 
+type TranscriptItemContext = {
+  channelId: string | null;
+  turnId: string | null;
+  sessionId: string | null;
+};
+
 function upsertMessage(
   d: TranscriptDraft,
   id: string,
@@ -115,8 +121,9 @@ function upsertMessage(
   title: string,
   text: string,
   timestamp: string,
-  channelId: string | null,
+  ctx: TranscriptItemContext,
   authorPubkey: string | null = null,
+  acpSource?: string,
 ) {
   const currentKey = d.activeMessageKey.get(id);
 
@@ -126,8 +133,11 @@ function upsertMessage(
       replaceItem(d, currentKey, {
         ...existing,
         text: existing.text + text,
-        channelId,
+        channelId: ctx.channelId,
+        turnId: ctx.turnId ?? existing.turnId,
+        sessionId: ctx.sessionId ?? existing.sessionId,
         authorPubkey: authorPubkey ?? existing.authorPubkey,
+        acpSource: acpSource ?? existing.acpSource,
       });
       return;
     }
@@ -142,8 +152,11 @@ function upsertMessage(
     title,
     text,
     timestamp,
-    channelId,
+    channelId: ctx.channelId,
+    turnId: ctx.turnId,
+    sessionId: ctx.sessionId,
     authorPubkey,
+    acpSource,
   });
   d.activeMessageKey = new Map(d.activeMessageKey);
   d.activeMessageKey.set(id, newKey);
@@ -156,15 +169,33 @@ function upsertTextItem(
   title: string,
   text: string,
   timestamp: string,
-  channelId: string | null,
+  ctx: TranscriptItemContext,
+  acpSource?: string,
 ) {
   const existing = d.itemsById.get(id);
   if (existing && existing.type === type) {
-    replaceItem(d, id, { ...existing, text: existing.text + text, channelId });
+    replaceItem(d, id, {
+      ...existing,
+      text: existing.text + text,
+      channelId: ctx.channelId,
+      turnId: ctx.turnId ?? existing.turnId,
+      sessionId: ctx.sessionId ?? existing.sessionId,
+      acpSource: acpSource ?? existing.acpSource,
+    });
     return;
   }
   sealOpenMessages(d);
-  pushItem(d, { id, type, title, text, timestamp, channelId });
+  pushItem(d, {
+    id,
+    type,
+    title,
+    text,
+    timestamp,
+    channelId: ctx.channelId,
+    turnId: ctx.turnId,
+    sessionId: ctx.sessionId,
+    acpSource,
+  });
 }
 
 function upsertMetadata(
@@ -173,15 +204,33 @@ function upsertMetadata(
   title: string,
   sections: PromptSection[],
   timestamp: string,
-  channelId: string | null,
+  ctx: TranscriptItemContext,
+  acpSource?: string,
 ) {
   const existing = d.itemsById.get(id);
   if (existing?.type === "metadata") {
-    replaceItem(d, id, { ...existing, sections, channelId });
+    replaceItem(d, id, {
+      ...existing,
+      sections,
+      channelId: ctx.channelId,
+      turnId: ctx.turnId ?? existing.turnId,
+      sessionId: ctx.sessionId ?? existing.sessionId,
+      acpSource: acpSource ?? existing.acpSource,
+    });
     return;
   }
   sealOpenMessages(d);
-  pushItem(d, { id, type: "metadata", title, sections, timestamp, channelId });
+  pushItem(d, {
+    id,
+    type: "metadata",
+    title,
+    sections,
+    timestamp,
+    channelId: ctx.channelId,
+    turnId: ctx.turnId,
+    sessionId: ctx.sessionId,
+    acpSource,
+  });
 }
 
 function upsertTool(
@@ -195,7 +244,8 @@ function upsertTool(
   result: string,
   isError: boolean,
   timestamp: string,
-  channelId: string | null,
+  ctx: TranscriptItemContext,
+  acpSource?: string,
 ) {
   const existing = d.itemsById.get(id);
   const canonicalBuzzToolName =
@@ -224,7 +274,10 @@ function upsertTool(
         existing.completedAt == null
           ? timestamp
           : existing.completedAt,
-      channelId,
+      channelId: ctx.channelId,
+      turnId: ctx.turnId ?? existing.turnId,
+      sessionId: ctx.sessionId ?? existing.sessionId,
+      acpSource: acpSource ?? existing.acpSource,
     });
     return;
   }
@@ -242,7 +295,10 @@ function upsertTool(
     timestamp,
     startedAt: timestamp,
     completedAt: null,
-    channelId,
+    channelId: ctx.channelId,
+    turnId: ctx.turnId,
+    sessionId: ctx.sessionId,
+    acpSource,
   });
 }
 
@@ -258,6 +314,11 @@ export function processTranscriptEvent(
 
   const channelId = event.channelId ?? null;
   const ch = channelId ?? "global";
+  const ctx: TranscriptItemContext = {
+    channelId,
+    turnId: event.turnId,
+    sessionId: event.sessionId ?? d.latestSessionId,
+  };
 
   if (event.kind === "turn_started") {
     upsertTextItem(
@@ -267,7 +328,8 @@ export function processTranscriptEvent(
       "Turn started",
       describeTurnStarted(event.payload),
       event.timestamp,
-      channelId,
+      ctx,
+      event.kind,
     );
   } else if (event.kind === "session_resolved") {
     upsertTextItem(
@@ -277,7 +339,8 @@ export function processTranscriptEvent(
       "Session ready",
       describeSessionResolved(event.payload),
       event.timestamp,
-      channelId,
+      ctx,
+      event.kind,
     );
   } else if (event.kind === "acp_parse_error") {
     upsertTextItem(
@@ -287,7 +350,8 @@ export function processTranscriptEvent(
       "Wire parse error",
       extractBlockText(event.payload),
       event.timestamp,
-      channelId,
+      ctx,
+      event.kind,
     );
   } else if (event.kind === "turn_error" || event.kind === "agent_panic") {
     const payload = asRecord(event.payload);
@@ -302,7 +366,8 @@ export function processTranscriptEvent(
       title,
       `${outcome}: ${error}`,
       event.timestamp,
-      channelId,
+      ctx,
+      event.kind,
     );
   } else if (event.kind === "acp_read" || event.kind === "acp_write") {
     const payload = asRecord(event.payload);
@@ -320,8 +385,9 @@ export function processTranscriptEvent(
             parsedPrompt.userTitle,
             parsedPrompt.userText,
             event.timestamp,
-            channelId,
+            ctx,
             parsedPrompt.userPubkey,
+            "session/prompt:user",
           );
         }
         if (parsedPrompt.sections.length > 0) {
@@ -331,7 +397,8 @@ export function processTranscriptEvent(
             "Prompt context",
             parsedPrompt.sections,
             event.timestamp,
-            channelId,
+            ctx,
+            "session/prompt:context",
           );
         }
       }
@@ -350,7 +417,9 @@ export function processTranscriptEvent(
           "Assistant",
           extractContentText(update.content),
           event.timestamp,
-          channelId,
+          ctx,
+          null,
+          updateType,
         );
       } else if (updateType === "user_message_chunk") {
         upsertMessage(
@@ -360,7 +429,9 @@ export function processTranscriptEvent(
           "User",
           extractContentText(update.content),
           event.timestamp,
-          channelId,
+          ctx,
+          null,
+          updateType,
         );
       } else if (updateType === "agent_thought_chunk") {
         upsertTextItem(
@@ -370,7 +441,8 @@ export function processTranscriptEvent(
           "Thinking",
           extractContentText(update.content),
           event.timestamp,
-          channelId,
+          ctx,
+          updateType,
         );
       } else if (updateType === "tool_call") {
         const toolId = asString(update.toolCallId) ?? `tool:${event.seq}`;
@@ -386,7 +458,8 @@ export function processTranscriptEvent(
           extractToolResult(update),
           false,
           event.timestamp,
-          channelId,
+          ctx,
+          updateType,
         );
       } else if (updateType === "tool_call_update") {
         const toolId = asString(update.toolCallId) ?? `tool:${event.seq}`;
@@ -405,7 +478,8 @@ export function processTranscriptEvent(
           extractToolResult(update),
           status === "failed",
           event.timestamp,
-          channelId,
+          ctx,
+          updateType,
         );
       } else if (updateType === "plan") {
         upsertTextItem(
@@ -415,7 +489,8 @@ export function processTranscriptEvent(
           "Plan",
           extractContentText(update.content) || JSON.stringify(update, null, 2),
           event.timestamp,
-          channelId,
+          ctx,
+          updateType,
         );
       }
     }
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptGrouping.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscriptGrouping.test.mjs
new file mode 100644
index 0000000000..7639a97c65
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptGrouping.test.mjs
@@ -0,0 +1,213 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  buildTranscriptDisplayBlocks,
+  flattenDisplayBlocks,
+  formatTurnSetupLabel,
+} from "./agentSessionTranscriptGrouping.ts";
+
+const baseTimestamp = "2026-06-14T22:20:23.000Z";
+
+function lifecycle(id, title, acpSource, turnId, text = "") {
+  return {
+    id,
+    type: "lifecycle",
+    title,
+    text,
+    timestamp: baseTimestamp,
+    acpSource,
+    turnId,
+    sessionId: "sess-1",
+    channelId: "channel-1",
+  };
+}
+
+function userPrompt(id, text, turnId) {
+  return {
+    id,
+    type: "message",
+    role: "user",
+    title: "Buzz event",
+    text,
+    timestamp: baseTimestamp,
+    acpSource: "session/prompt:user",
+    turnId,
+    sessionId: "sess-1",
+    channelId: "channel-1",
+  };
+}
+
+function promptContext(id, turnId) {
+  return {
+    id,
+    type: "metadata",
+    title: "Prompt context",
+    sections: [{ title: "Channel", body: "general" }],
+    timestamp: baseTimestamp,
+    acpSource: "session/prompt:context",
+    turnId,
+    sessionId: "sess-1",
+    channelId: "channel-1",
+  };
+}
+
+function assistantMessage(id, text, turnId) {
+  return {
+    id,
+    type: "message",
+    role: "assistant",
+    title: "Assistant",
+    text,
+    timestamp: "2026-06-14T22:20:47.000Z",
+    acpSource: "agent_message_chunk",
+    turnId,
+    sessionId: "sess-1",
+    channelId: "channel-1",
+  };
+}
+
+function toolCall(id, turnId) {
+  return {
+    id,
+    type: "tool",
+    title: "Shell",
+    toolName: "buzz-dev-mcp__shell",
+    buzzToolName: null,
+    status: "completed",
+    args: {},
+    result: "ok",
+    isError: false,
+    timestamp: "2026-06-14T22:20:47.000Z",
+    startedAt: "2026-06-14T22:20:47.000Z",
+    completedAt: "2026-06-14T22:20:47.400Z",
+    acpSource: "tool_call_update",
+    turnId,
+    sessionId: "sess-1",
+    channelId: "channel-1",
+  };
+}
+
+test("buildTranscriptDisplayBlocks bundles user prompt, setup, and context together", () => {
+  const rawItems = [
+    lifecycle(
+      "turn",
+      "Turn started",
+      "turn_started",
+      "turn-1",
+      "Triggered by 1 event.",
+    ),
+    lifecycle("session", "Session ready", "session_resolved", "turn-1"),
+    userPrompt("prompt", "@Ned deliberate, wider pass", "turn-1"),
+    promptContext("context", "turn-1"),
+    assistantMessage("assistant", "Thinking out loud.", "turn-1"),
+    toolCall("tool", "turn-1"),
+  ];
+
+  const blocks = buildTranscriptDisplayBlocks(rawItems);
+  const displayOrder = flattenDisplayBlocks(blocks).map((item) => item.id);
+
+  assert.deepEqual(displayOrder, [
+    "prompt",
+    "turn",
+    "session",
+    "context",
+    "assistant",
+    "tool",
+  ]);
+
+  const turnBlock = blocks[0];
+  assert.equal(turnBlock?.kind, "turn");
+  assert.equal(turnBlock.segments[0]?.kind, "prompt");
+  const promptSegment = turnBlock.segments[0];
+  assert.equal(promptSegment.user.id, "prompt");
+  assert.equal(promptSegment.context?.id, "context");
+  assert.equal(promptSegment.setup.length, 2);
+  assert.equal(turnBlock.segments[1]?.kind, "item");
+  assert.equal(turnBlock.segments[2]?.kind, "item");
+});
+
+test("buildTranscriptDisplayBlocks collapses setup lifecycle inside prompt bundle", () => {
+  const rawItems = [
+    lifecycle("turn", "Turn started", "turn_started", "turn-1"),
+    lifecycle("session", "Session ready", "session_resolved", "turn-1"),
+    userPrompt("prompt", "hello", "turn-1"),
+  ];
+
+  const blocks = buildTranscriptDisplayBlocks(rawItems);
+  assert.equal(blocks.length, 1);
+  assert.equal(blocks[0]?.kind, "turn");
+
+  const turnBlock = blocks[0];
+  assert.equal(turnBlock.segments.length, 1);
+  assert.equal(turnBlock.segments[0]?.kind, "prompt");
+  assert.equal(
+    formatTurnSetupLabel(turnBlock.segments[0].setup),
+    "Turn started · Session ready",
+  );
+});
+
+test("buildTranscriptDisplayBlocks hides setup and context when prompt is missing", () => {
+  const rawItems = [
+    lifecycle("turn", "Turn started", "turn_started", "turn-1"),
+    lifecycle("session", "Session ready", "session_resolved", "turn-1"),
+    promptContext("context", "turn-1"),
+    toolCall("tool", "turn-1"),
+  ];
+
+  const blocks = buildTranscriptDisplayBlocks(rawItems);
+  const displayOrder = flattenDisplayBlocks(blocks).map((item) => item.id);
+
+  assert.deepEqual(displayOrder, ["tool"]);
+});
+
+test("buildTranscriptDisplayBlocks drops setup-and-context-only turns", () => {
+  const rawItems = [
+    lifecycle("turn", "Turn started", "turn_started", "turn-1"),
+    lifecycle("session", "Session ready", "session_resolved", "turn-1"),
+    promptContext("context", "turn-1"),
+  ];
+
+  const blocks = buildTranscriptDisplayBlocks(rawItems);
+
+  assert.deepEqual(blocks, []);
+});
+
+test("buildTranscriptDisplayBlocks leaves error lifecycle prominent outside prompt bundle", () => {
+  const rawItems = [
+    lifecycle("turn", "Turn started", "turn_started", "turn-1"),
+    userPrompt("prompt", "hello", "turn-1"),
+    lifecycle(
+      "error",
+      "Turn error",
+      "turn_error",
+      "turn-1",
+      "timeout: agent hung",
+    ),
+  ];
+
+  const blocks = buildTranscriptDisplayBlocks(rawItems);
+  const displayOrder = flattenDisplayBlocks(blocks).map((item) => item.id);
+
+  assert.deepEqual(displayOrder, ["prompt", "turn", "error"]);
+  assert.equal(blocks[0]?.segments[0]?.kind, "prompt");
+  assert.equal(blocks[0]?.segments[1]?.kind, "item");
+  assert.equal(blocks[0]?.segments[1]?.item.id, "error");
+});
+
+test("buildTranscriptDisplayBlocks passes through items without turnId", () => {
+  const orphan = {
+    id: "orphan",
+    type: "lifecycle",
+    title: "Wire parse error",
+    text: "bad json",
+    timestamp: baseTimestamp,
+    acpSource: "acp_parse_error",
+    channelId: "channel-1",
+  };
+
+  const blocks = buildTranscriptDisplayBlocks([orphan]);
+  assert.equal(blocks.length, 1);
+  assert.equal(blocks[0]?.kind, "single");
+  assert.equal(blocks[0]?.item.id, "orphan");
+});
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptGrouping.ts b/desktop/src/features/agents/ui/agentSessionTranscriptGrouping.ts
new file mode 100644
index 0000000000..fd2877f27a
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptGrouping.ts
@@ -0,0 +1,208 @@
+import type { TranscriptItem } from "./agentSessionTypes";
+
+export type TranscriptTurnSegment =
+  | { kind: "item"; item: TranscriptItem }
+  | { kind: "setup"; items: Extract<TranscriptItem, { type: "lifecycle" }>[] }
+  | {
+      kind: "prompt";
+      user: Extract<TranscriptItem, { type: "message" }>;
+      context: Extract<TranscriptItem, { type: "metadata" }> | null;
+      setup: Extract<TranscriptItem, { type: "lifecycle" }>[];
+    };
+
+export type TranscriptDisplayBlock =
+  | { kind: "single"; item: TranscriptItem }
+  | { kind: "turn"; turnId: string; segments: TranscriptTurnSegment[] };
+
+function isUserPrompt(
+  item: TranscriptItem,
+): item is Extract<TranscriptItem, { type: "message" }> {
+  return (
+    item.type === "message" &&
+    item.role === "user" &&
+    item.acpSource === "session/prompt:user"
+  );
+}
+
+function isPromptContext(
+  item: TranscriptItem,
+): item is Extract<TranscriptItem, { type: "metadata" }> {
+  return (
+    item.type === "metadata" && item.acpSource === "session/prompt:context"
+  );
+}
+
+function isSetupLifecycle(
+  item: TranscriptItem,
+): item is Extract<TranscriptItem, { type: "lifecycle" }> {
+  return (
+    item.type === "lifecycle" &&
+    (item.acpSource === "turn_started" || item.acpSource === "session_resolved")
+  );
+}
+
+function isErrorLifecycle(
+  item: TranscriptItem,
+): item is Extract<TranscriptItem, { type: "lifecycle" }> {
+  return (
+    item.type === "lifecycle" && item.title.toLowerCase().includes("error")
+  );
+}
+
+type TurnBucket = {
+  turnId: string;
+  items: TranscriptItem[];
+};
+
+function classifyTurnItems(items: TranscriptItem[]): TranscriptTurnSegment[] {
+  const userPrompt = items.find(isUserPrompt) ?? null;
+  const setupLifecycle = items.filter(isSetupLifecycle);
+  const promptContext = items.find(isPromptContext) ?? null;
+  const consumed = new Set<TranscriptItem>();
+
+  if (userPrompt) consumed.add(userPrompt);
+  for (const item of setupLifecycle) consumed.add(item);
+  if (promptContext) consumed.add(promptContext);
+
+  const activity = items.filter((item) => !consumed.has(item));
+
+  if (!userPrompt) {
+    return activity.map((item) => ({ kind: "item", item }));
+  }
+
+  const segments: TranscriptTurnSegment[] = [
+    {
+      kind: "prompt",
+      user: userPrompt,
+      context: promptContext,
+      setup: setupLifecycle,
+    },
+  ];
+
+  for (const item of activity) {
+    if (isErrorLifecycle(item)) {
+      segments.push({ kind: "item", item });
+      continue;
+    }
+    segments.push({ kind: "item", item });
+  }
+
+  return segments;
+}
+
+/**
+ * Build presentation-only display blocks from normalized transcript items.
+ * Raw observer order is preserved in the source items; this only reorders
+ * within a turn for user-facing narrative flow.
+ */
+export function buildTranscriptDisplayBlocks(
+  items: TranscriptItem[],
+): TranscriptDisplayBlock[] {
+  const blocks: TranscriptDisplayBlock[] = [];
+  const turnBuckets = new Map<string, TurnBucket>();
+  const displayOrder: Array<
+    { kind: "single"; item: TranscriptItem } | { kind: "turn"; turnId: string }
+  > = [];
+
+  for (const item of items) {
+    const turnId = item.turnId;
+    if (!turnId) {
+      displayOrder.push({ kind: "single", item });
+      continue;
+    }
+
+    let bucket = turnBuckets.get(turnId);
+    if (!bucket) {
+      bucket = { turnId, items: [] };
+      turnBuckets.set(turnId, bucket);
+      displayOrder.push({ kind: "turn", turnId });
+    }
+    bucket.items.push(item);
+  }
+
+  for (const entry of displayOrder) {
+    if (entry.kind === "single") {
+      blocks.push({ kind: "single", item: entry.item });
+      continue;
+    }
+
+    const bucket = turnBuckets.get(entry.turnId);
+    if (!bucket || bucket.items.length === 0) {
+      continue;
+    }
+
+    const segments = classifyTurnItems(bucket.items);
+    if (segments.length > 0) {
+      blocks.push({
+        kind: "turn",
+        turnId: entry.turnId,
+        segments,
+      });
+    }
+  }
+
+  return blocks;
+}
+
+/** Flatten display blocks back to items for testing display order. */
+export function flattenDisplayBlocks(
+  blocks: TranscriptDisplayBlock[],
+): TranscriptItem[] {
+  const result: TranscriptItem[] = [];
+
+  for (const block of blocks) {
+    if (block.kind === "single") {
+      result.push(block.item);
+      continue;
+    }
+
+    for (const segment of block.segments) {
+      if (segment.kind === "item") {
+        result.push(segment.item);
+      } else if (segment.kind === "prompt") {
+        result.push(segment.user);
+        result.push(...segment.setup);
+        if (segment.context) {
+          result.push(segment.context);
+        }
+      } else {
+        result.push(...segment.items);
+      }
+    }
+  }
+
+  return result;
+}
+
+/** Human-readable labels for a collapsed turn setup row. */
+export function formatTurnSetupLabel(
+  items: Extract<TranscriptItem, { type: "lifecycle" }>[],
+): string {
+  const labels = items.map((item) => item.title);
+  return labels.join(" · ");
+}
+
+/** Earliest timestamp among setup lifecycle items. */
+export function turnSetupTimestamp(
+  items: Extract<TranscriptItem, { type: "lifecycle" }>[],
+): string | null {
+  if (items.length === 0) return null;
+  return items.reduce(
+    (earliest, item) =>
+      Date.parse(item.timestamp) < Date.parse(earliest)
+        ? item.timestamp
+        : earliest,
+    items[0].timestamp,
+  );
+}
+
+/** Optional detail text from setup lifecycle items (e.g. trigger count). */
+export function turnSetupDetail(
+  items: Extract<TranscriptItem, { type: "lifecycle" }>[],
+): string | null {
+  const details = items
+    .map((item) => item.text.trim())
+    .filter((text) => text.length > 0);
+  if (details.length === 0) return null;
+  return details.join(" ");
+}
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs
index e84af14683..1f7b4b5a22 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs
@@ -65,6 +65,37 @@ test("parsePromptText extracts content, hex pubkey, and a title-cased kind", ()
   );
 });
 
+test("parsePromptText preserves multiline event content in the user bubble text", () => {
+  const text = [
+    "[Buzz event: @mention]",
+    "Event ID: event-1",
+    "Channel: agents",
+    `From: tho (hex: ${HEX})`,
+    "Time: 2026-06-15T17:15:00Z",
+    "Content: @Ned",
+    "",
+    "- remove that stray cherry pick if it's not adding value here",
+    "- help me understand what that e2eBridge change does",
+    "- we'd want the e2e seed path as a separate pull request",
+    'Tags: [["h","agents"]]',
+    "Parsed: mentions=[Ned]",
+  ].join("\n");
+
+  const result = parsePromptText(text);
+
+  assert.equal(
+    result.userText,
+    [
+      "@Ned",
+      "",
+      "- remove that stray cherry pick if it's not adding value here",
+      "- help me understand what that e2eBridge change does",
+      "- we'd want the e2e seed path as a separate pull request",
+    ].join("\n"),
+  );
+  assert.equal(result.userPubkey, HEX);
+});
+
 test("parsePromptText lowercases the extracted hex pubkey", () => {
   const text = [
     "[Buzz event: dm]",
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
index 00c13c0567..b74f8cd8ac 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
@@ -85,9 +85,39 @@ function parsePromptSections(text: string): PromptSection[] {
   return sections;
 }
 
+const EVENT_CONTENT_BOUNDARY_RE =
+  /^(?:Event ID|Channel|Kind|From|Time|Tags|Parsed):\s*/;
+const EVENT_BLOCK_BOUNDARY_RE = /^--- Event \d+\b/;
+
 function extractEventContent(body: string): string {
-  const contentMatch = body.match(/^Content:\s*(.*)$/m);
-  return contentMatch?.[1]?.trim() ?? "";
+  const lines = body.split(/\r?\n/);
+  const chunks: string[] = [];
+
+  for (let i = 0; i < lines.length; i++) {
+    const match = lines[i].match(/^Content:\s?(.*)$/);
+    if (!match) {
+      continue;
+    }
+
+    const contentLines = [match[1] ?? ""];
+    for (let j = i + 1; j < lines.length; j++) {
+      const line = lines[j];
+      if (
+        EVENT_CONTENT_BOUNDARY_RE.test(line) ||
+        EVENT_BLOCK_BOUNDARY_RE.test(line)
+      ) {
+        break;
+      }
+      contentLines.push(line);
+    }
+
+    const content = contentLines.join("\n").trim();
+    if (content) {
+      chunks.push(content);
+    }
+  }
+
+  return chunks.join("\n\n");
 }
 
 function extractEventAuthorPubkey(body: string): string | null {
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptPresentation.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscriptPresentation.test.mjs
new file mode 100644
index 0000000000..a13736189a
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptPresentation.test.mjs
@@ -0,0 +1,148 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+  buildTranscriptPresentation,
+  getActivityHeadline,
+  isMeaningfulItem,
+} from "./agentSessionTranscriptPresentation.ts";
+
+const baseTimestamp = "2026-06-14T19:00:00.000Z";
+
+function makeTool(overrides = {}) {
+  return {
+    id: "tool:1",
+    type: "tool",
+    title: "Send Message",
+    toolName: "send_message",
+    buzzToolName: "send_message",
+    status: "executing",
+    args: { channel_id: "abc" },
+    result: "",
+    isError: false,
+    timestamp: baseTimestamp,
+    startedAt: baseTimestamp,
+    completedAt: null,
+    ...overrides,
+  };
+}
+
+function makeMessage(overrides = {}) {
+  return {
+    id: "msg:1",
+    type: "message",
+    role: "assistant",
+    title: "Assistant",
+    text: "Looking into that now.",
+    timestamp: baseTimestamp,
+    ...overrides,
+  };
+}
+
+test("getActivityHeadline formats tool titles and assistant text", () => {
+  assert.equal(getActivityHeadline(makeTool()), "Send Message");
+  assert.equal(
+    getActivityHeadline(makeMessage({ text: "First line\nSecond line" })),
+    "First line",
+  );
+  assert.equal(getActivityHeadline(makeMessage({ text: "   " })), "Responding");
+});
+
+test("isMeaningfulItem ignores lifecycle noise and metadata", () => {
+  assert.equal(
+    isMeaningfulItem({
+      id: "life:1",
+      type: "lifecycle",
+      title: "Turn started",
+      text: "",
+      timestamp: baseTimestamp,
+    }),
+    false,
+  );
+  assert.equal(
+    isMeaningfulItem({
+      id: "meta:1",
+      type: "metadata",
+      title: "Prompt context",
+      sections: [],
+      timestamp: baseTimestamp,
+    }),
+    false,
+  );
+  assert.equal(
+    isMeaningfulItem({
+      id: "life:2",
+      type: "lifecycle",
+      title: "Turn error",
+      text: "boom",
+      timestamp: baseTimestamp,
+    }),
+    true,
+  );
+});
+
+test("buildTranscriptPresentation marks running tools as active while working", () => {
+  const items = [
+    makeMessage({ id: "msg:user", role: "user", text: "Please help" }),
+    makeTool({ id: "tool:running", status: "executing" }),
+  ];
+
+  const presentation = buildTranscriptPresentation(items, true);
+
+  assert.equal(presentation.state, "tool_running");
+  assert.equal(presentation.headline, "Send Message");
+  assert.equal(presentation.counts.tools, 1);
+  assert.equal(presentation.counts.messages, 1);
+  assert.ok(presentation.activeItemIds.has("tool:running"));
+});
+
+test("buildTranscriptPresentation highlights assistant streaming while working", () => {
+  const items = [
+    makeMessage({ id: "msg:assistant", role: "assistant", text: "Drafting" }),
+  ];
+
+  const presentation = buildTranscriptPresentation(items, true);
+
+  assert.equal(presentation.state, "responding");
+  assert.equal(presentation.headline, "Drafting");
+  assert.ok(presentation.activeItemIds.has("msg:assistant"));
+});
+
+test("buildTranscriptPresentation surfaces lifecycle errors", () => {
+  const items = [
+    makeTool({
+      id: "tool:done",
+      status: "completed",
+      completedAt: "2026-06-14T19:00:05.000Z",
+    }),
+    {
+      id: "life:error",
+      type: "lifecycle",
+      title: "Turn error",
+      text: "timeout",
+      timestamp: "2026-06-14T19:00:06.000Z",
+    },
+  ];
+
+  const presentation = buildTranscriptPresentation(items, false);
+
+  assert.equal(presentation.state, "error");
+  assert.equal(presentation.hasError, true);
+  assert.equal(presentation.headline, "Turn error");
+});
+
+test("buildTranscriptPresentation returns idle state when not working", () => {
+  const items = [
+    makeTool({
+      id: "tool:done",
+      status: "completed",
+      completedAt: "2026-06-14T19:00:05.000Z",
+    }),
+  ];
+
+  const presentation = buildTranscriptPresentation(items, false);
+
+  assert.equal(presentation.state, "idle");
+  assert.equal(presentation.activeItemIds.size, 0);
+  assert.equal(presentation.headline, "Send Message");
+});
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptPresentation.ts b/desktop/src/features/agents/ui/agentSessionTranscriptPresentation.ts
new file mode 100644
index 0000000000..bb6fd4c9fd
--- /dev/null
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptPresentation.ts
@@ -0,0 +1,280 @@
+import { formatToolTitle } from "./agentSessionToolCatalog";
+import type { TranscriptItem } from "./agentSessionTypes";
+
+export type TranscriptActivityCounts = {
+  tools: number;
+  toolErrors: number;
+  thoughts: number;
+  messages: number;
+  lifecycle: number;
+  metadata: number;
+};
+
+export type TranscriptActivityState =
+  | "idle"
+  | "responding"
+  | "thinking"
+  | "tool_running"
+  | "error";
+
+export type TranscriptPresentation = {
+  headline: string;
+  state: TranscriptActivityState;
+  counts: TranscriptActivityCounts;
+  latestMeaningfulItem: TranscriptItem | null;
+  latestMeaningfulItemId: string | null;
+  activeItemIds: ReadonlySet<string>;
+  lastUpdatedAt: string | null;
+  hasError: boolean;
+};
+
+const LIFECYCLE_NOISE = new Set([
+  "turn started",
+  "session ready",
+  "wire parse error",
+]);
+
+/** Human-readable headline for a single transcript item. */
+export function getActivityHeadline(item: TranscriptItem): string | null {
+  if (item.type === "tool") {
+    return formatToolTitle(item.buzzToolName ?? item.toolName, item.title);
+  }
+
+  if (item.type === "message") {
+    if (item.role === "assistant") {
+      const trimmed = item.text.trim();
+      if (trimmed.length > 0) {
+        const firstLine = trimmed.split("\n")[0]?.trim() ?? "";
+        if (firstLine.length > 0) {
+          return firstLine.length > 72
+            ? `${firstLine.slice(0, 69)}…`
+            : firstLine;
+        }
+      }
+      return "Responding";
+    }
+    return item.title || "User prompt";
+  }
+
+  if (item.type === "thought") {
+    return item.title === "Plan" ? "Planning" : item.title;
+  }
+
+  if (item.type === "metadata") {
+    return item.title;
+  }
+
+  return item.title;
+}
+
+function isLifecycleNoise(
+  item: Extract<TranscriptItem, { type: "lifecycle" }>,
+) {
+  return LIFECYCLE_NOISE.has(item.title.toLowerCase());
+}
+
+/** Whether an item should contribute to the "Now" summary and headline scan. */
+export function isMeaningfulItem(item: TranscriptItem): boolean {
+  if (item.type === "lifecycle") {
+    return !isLifecycleNoise(item);
+  }
+  if (item.type === "metadata") {
+    return false;
+  }
+  return true;
+}
+
+function isToolRunning(item: Extract<TranscriptItem, { type: "tool" }>) {
+  return item.status === "executing" || item.status === "pending";
+}
+
+function isLifecycleError(
+  item: Extract<TranscriptItem, { type: "lifecycle" }>,
+) {
+  return item.title.toLowerCase().includes("error");
+}
+
+function countItems(items: TranscriptItem[]): TranscriptActivityCounts {
+  const counts: TranscriptActivityCounts = {
+    tools: 0,
+    toolErrors: 0,
+    thoughts: 0,
+    messages: 0,
+    lifecycle: 0,
+    metadata: 0,
+  };
+
+  for (const item of items) {
+    switch (item.type) {
+      case "tool":
+        counts.tools += 1;
+        if (item.isError || item.status === "failed") {
+          counts.toolErrors += 1;
+        }
+        break;
+      case "thought":
+        counts.thoughts += 1;
+        break;
+      case "message":
+        counts.messages += 1;
+        break;
+      case "lifecycle":
+        counts.lifecycle += 1;
+        break;
+      case "metadata":
+        counts.metadata += 1;
+        break;
+    }
+  }
+
+  return counts;
+}
+
+function findLatestMeaningfulItem(
+  items: TranscriptItem[],
+): TranscriptItem | null {
+  for (let i = items.length - 1; i >= 0; i--) {
+    const item = items[i];
+    if (isMeaningfulItem(item)) {
+      return item;
+    }
+  }
+  return null;
+}
+
+function resolveActivityState(
+  latest: TranscriptItem | null,
+  hasError: boolean,
+  isWorking: boolean,
+): TranscriptActivityState {
+  if (!isWorking) {
+    return hasError ? "error" : "idle";
+  }
+
+  if (hasError && latest?.type === "lifecycle" && isLifecycleError(latest)) {
+    return "error";
+  }
+
+  if (latest?.type === "tool" && isToolRunning(latest)) {
+    return "tool_running";
+  }
+
+  if (latest?.type === "thought") {
+    return "thinking";
+  }
+
+  if (latest?.type === "message" && latest.role === "assistant") {
+    return "responding";
+  }
+
+  if (latest?.type === "tool") {
+    return "tool_running";
+  }
+
+  return "idle";
+}
+
+function resolveHeadline(
+  latest: TranscriptItem | null,
+  state: TranscriptActivityState,
+  isWorking: boolean,
+): string {
+  if (latest) {
+    const headline = getActivityHeadline(latest);
+    if (headline) {
+      return headline;
+    }
+  }
+
+  if (isWorking) {
+    switch (state) {
+      case "tool_running":
+        return "Running a tool";
+      case "thinking":
+        return "Thinking";
+      case "responding":
+        return "Responding";
+      case "error":
+        return "Encountered an error";
+      default:
+        return "Working";
+    }
+  }
+
+  if (state === "error") {
+    return "Last turn ended with an error";
+  }
+
+  return "Waiting for activity";
+}
+
+function collectActiveItemIds(
+  items: TranscriptItem[],
+  isWorking: boolean,
+): ReadonlySet<string> {
+  if (!isWorking || items.length === 0) {
+    return new Set();
+  }
+
+  const active = new Set<string>();
+
+  for (let i = items.length - 1; i >= 0; i--) {
+    const item = items[i];
+
+    if (item.type === "tool" && isToolRunning(item)) {
+      active.add(item.id);
+      break;
+    }
+
+    if (item.type === "thought") {
+      active.add(item.id);
+      break;
+    }
+
+    if (item.type === "message" && item.role === "assistant") {
+      active.add(item.id);
+      break;
+    }
+  }
+
+  return active;
+}
+
+function detectError(items: TranscriptItem[]): boolean {
+  for (let i = items.length - 1; i >= 0; i--) {
+    const item = items[i];
+    if (!isMeaningfulItem(item)) {
+      continue;
+    }
+    if (item.type === "lifecycle" && isLifecycleError(item)) {
+      return true;
+    }
+    if (item.type === "tool" && (item.isError || item.status === "failed")) {
+      return true;
+    }
+    break;
+  }
+  return false;
+}
+
+/** Derive presentation metadata for a transcript list. */
+export function buildTranscriptPresentation(
+  items: TranscriptItem[],
+  isWorking = false,
+): TranscriptPresentation {
+  const latestMeaningfulItem = findLatestMeaningfulItem(items);
+  const hasError = detectError(items);
+  const state = resolveActivityState(latestMeaningfulItem, hasError, isWorking);
+
+  return {
+    headline: resolveHeadline(latestMeaningfulItem, state, isWorking),
+    state,
+    counts: countItems(items),
+    latestMeaningfulItem,
+    latestMeaningfulItemId: latestMeaningfulItem?.id ?? null,
+    activeItemIds: collectActiveItemIds(items, isWorking),
+    lastUpdatedAt:
+      items.length > 0 ? (items[items.length - 1]?.timestamp ?? null) : null,
+    hasError,
+  };
+}
diff --git a/desktop/src/features/agents/ui/agentSessionTypes.ts b/desktop/src/features/agents/ui/agentSessionTypes.ts
index 2ff4ea3055..c4061bb1d5 100644
--- a/desktop/src/features/agents/ui/agentSessionTypes.ts
+++ b/desktop/src/features/agents/ui/agentSessionTypes.ts
@@ -20,42 +20,52 @@ export type ConnectionState =
 
 export type ToolStatus = "executing" | "completed" | "failed" | "pending";
 
+/** Observer/ACP wire label for dev-only transcript debugging. */
+export type TranscriptAcpSource = string;
+
+/** Shared optional identity fields attached during transcript construction. */
+export type TranscriptItemIdentity = {
+  turnId?: string | null;
+  sessionId?: string | null;
+  channelId?: string | null;
+};
+
 export type TranscriptItem =
-  | {
+  | ({
       id: string;
       type: "message";
       role: "assistant" | "user";
       title: string;
       text: string;
       timestamp: string;
+      acpSource?: TranscriptAcpSource;
       authorPubkey?: string | null;
-      channelId?: string | null;
-    }
-  | {
+    } & TranscriptItemIdentity)
+  | ({
       id: string;
       type: "thought";
       title: string;
       text: string;
       timestamp: string;
-      channelId?: string | null;
-    }
-  | {
+      acpSource?: TranscriptAcpSource;
+    } & TranscriptItemIdentity)
+  | ({
       id: string;
       type: "lifecycle";
       title: string;
       text: string;
       timestamp: string;
-      channelId?: string | null;
-    }
-  | {
+      acpSource?: TranscriptAcpSource;
+    } & TranscriptItemIdentity)
+  | ({
       id: string;
       type: "metadata";
       title: string;
       sections: PromptSection[];
       timestamp: string;
-      channelId?: string | null;
-    }
-  | {
+      acpSource?: TranscriptAcpSource;
+    } & TranscriptItemIdentity)
+  | ({
       id: string;
       type: "tool";
       title: string;
@@ -68,8 +78,8 @@ export type TranscriptItem =
       timestamp: string;
       startedAt: string;
       completedAt: string | null;
-      channelId?: string | null;
-    };
+      acpSource?: TranscriptAcpSource;
+    } & TranscriptItemIdentity);
 
 export type PromptSection = {
   title: string;
diff --git a/desktop/src/features/agents/ui/agentSessionUtils.ts b/desktop/src/features/agents/ui/agentSessionUtils.ts
index 19d915ec81..39cec560ea 100644
--- a/desktop/src/features/agents/ui/agentSessionUtils.ts
+++ b/desktop/src/features/agents/ui/agentSessionUtils.ts
@@ -64,6 +64,91 @@ export function asRecord(value: unknown): Record<string, unknown> {
     : {};
 }
 
+/**
+ * True when a tool image source is an inline `data:image/` URI that should be
+ * rendered as-is. This is the dual-layer image-scheme guard: only the
+ * `data:image/` prefix is treated as a safe passthrough — every other scheme
+ * (including other `data:` subtypes) must be routed through the relay rewriter.
+ * Never widen this beyond `data:image/`.
+ */
+export function isInlineImageData(source: string): boolean {
+  return source.startsWith("data:image/");
+}
+
+function getToolNumber(
+  record: Record<string, unknown>,
+  keys: string[],
+): number | null {
+  for (const key of keys) {
+    const value = record[key];
+    if (typeof value === "number" && Number.isFinite(value)) {
+      return value;
+    }
+  }
+  return null;
+}
+
+/** Format a millisecond duration; negative input yields null. */
+export function formatDurationMs(ms: number): string | null {
+  if (ms < 0) return null;
+  const totalSeconds = ms / 1000;
+  if (totalSeconds < 60) {
+    return totalSeconds < 10
+      ? `${totalSeconds.toFixed(1)}s`
+      : `${Math.round(totalSeconds)}s`;
+  }
+  let minutes = Math.floor(totalSeconds / 60);
+  let seconds = Math.round(totalSeconds % 60);
+  if (seconds === 60) {
+    minutes += 1;
+    seconds = 0;
+  }
+  return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
+}
+
+/**
+ * Parse a tool result string into a value. Handles the double-encoding case
+ * where a JSON string itself contains JSON. Returns null on empty or invalid
+ * input.
+ */
+export function parseToolResultValue(result: string): unknown {
+  const trimmed = result.trim();
+  if (!trimmed) return null;
+
+  try {
+    const parsed = JSON.parse(trimmed);
+    if (typeof parsed !== "string") return parsed;
+    try {
+      return JSON.parse(parsed);
+    } catch {
+      return parsed;
+    }
+  } catch {
+    return null;
+  }
+}
+
+/**
+ * Resolve a tool's display duration. Prefers the start/complete timestamps,
+ * then falls back to `duration_ms`/`elapsed_ms` fields inside the parsed
+ * result payload.
+ */
+export function getToolDurationDisplay(item: {
+  startedAt?: string | null;
+  completedAt?: string | null;
+  result: string;
+}): string | null {
+  if (item.startedAt && item.completedAt) {
+    return formatDuration(item.startedAt, item.completedAt);
+  }
+
+  const resultRecord = asRecord(parseToolResultValue(item.result));
+  const durationMs =
+    getToolNumber(resultRecord, ["duration_ms", "durationMs"]) ??
+    getToolNumber(resultRecord, ["elapsed_ms", "elapsedMs"]);
+  return durationMs == null ? null : formatDurationMs(durationMs);
+}
+
 export function asString(value: unknown): string | null {
   return typeof value === "string" ? value : null;
 }
diff --git a/desktop/src/features/agents/ui/rawEventRail.test.mjs b/desktop/src/features/agents/ui/rawEventRail.test.mjs
new file mode 100644
index 0000000000..7750fdac25
--- /dev/null
+++ b/desktop/src/features/agents/ui/rawEventRail.test.mjs
@@ -0,0 +1,42 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { describeRawEvent } from "./agentSessionTranscriptHelpers.ts";
+
+function rawEvent(overrides = {}) {
+  return {
+    seq: 1,
+    kind: "acp",
+    sessionId: "sess-1",
+    channelId: "channel-1",
+    payload: {},
+    ...overrides,
+  };
+}
+
+test("describeRawEvent surfaces the session/update sessionUpdate label", () => {
+  const event = rawEvent({
+    payload: {
+      method: "session/update",
+      params: { update: { sessionUpdate: "agent_message_chunk" } },
+    },
+  });
+  assert.equal(describeRawEvent(event), "agent_message_chunk");
+});
+
+test("describeRawEvent falls back to the method when session/update lacks an update label", () => {
+  const event = rawEvent({
+    payload: { method: "session/update", params: {} },
+  });
+  assert.equal(describeRawEvent(event), "session/update");
+});
+
+test("describeRawEvent uses the method for non-session/update payloads", () => {
+  const event = rawEvent({ payload: { method: "session/prompt" } });
+  assert.equal(describeRawEvent(event), "session/prompt");
+});
+
+test("describeRawEvent falls back to the event kind when no method is present", () => {
+  const event = rawEvent({ kind: "acp_parse_error", payload: {} });
+  assert.equal(describeRawEvent(event), "acp_parse_error");
+});
diff --git a/desktop/src/features/channels/lib/agentSessionCandidates.ts b/desktop/src/features/channels/lib/agentSessionCandidates.ts
index a77db42da6..92aa6811bc 100644
--- a/desktop/src/features/channels/lib/agentSessionCandidates.ts
+++ b/desktop/src/features/channels/lib/agentSessionCandidates.ts
@@ -11,6 +11,7 @@ export type ChannelAgentSessionAgent = Pick<
   "pubkey" | "name" | "status"
 > & {
   agentSource: "managed" | "member-bot" | "relay";
+  avatarUrl?: string | null;
   canInterruptTurn: boolean;
   channelIds?: string[];
   channels?: string[];
@@ -57,6 +58,7 @@ export function buildChannelAgentSessionCandidates({
       name: agent.name,
       status: agent.status,
       agentSource: "managed",
+      avatarUrl: agent.avatarUrl,
       canInterruptTurn: true,
       channelIds: existing?.channelIds,
       channels: existing?.channels,
diff --git a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
index ae633bbf30..fe8c58b0bb 100644
--- a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
+++ b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
@@ -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 {
@@ -79,20 +94,43 @@ export function AgentSessionThreadPanel({
   const agentHeaderActions = (
     <div className="ml-auto flex shrink-0 items-center gap-2">
       {isLive && isWorking ? (
-        <Badge
-          className="shrink-0 gap-1 px-2 py-0 text-[10px]"
-          variant="default"
-        >
+        <Badge className="shrink-0 gap-1 px-2 py-0 text-xs" variant="default">
           <CircleDot className="h-2.5 w-2.5" />
           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-xs 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>
             <Button
               aria-label="Stop current agent turn"
-              className="h-6 px-2 text-[11px]"
+              className="h-6 px-2 text-xs"
               data-testid="agent-session-stop-turn"
               disabled={!canInterruptTurn}
               onClick={() => {
@@ -140,7 +178,9 @@ export function AgentSessionThreadPanel({
         >
           <ArrowLeft />
         </Button>
-        <AuxiliaryPanelTitle>Activity</AuxiliaryPanelTitle>
+        <AuxiliaryPanelTitle>
+          {showRawFeed ? "Raw ACP Activity" : "Activity"}
+        </AuxiliaryPanelTitle>
       </AuxiliaryPanelHeaderGroup>
       {agentHeaderActions}
     </>
@@ -161,9 +201,11 @@ export function AgentSessionThreadPanel({
         channelId={channel.id}
         className="border-0 bg-transparent p-0 shadow-none"
         emptyDescription={`Mention ${agent.name} in the channel to see its work here.`}
+        isWorking={isWorking}
         profiles={profiles}
+        rawLayout="exclusive"
         showHeader={false}
-        showRaw={false}
+        showRaw={showRawFeed}
       />
     </div>
   );
diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx
index a289200ad7..ad992960ab 100644
--- a/desktop/src/features/channels/ui/BotActivityBar.tsx
+++ b/desktop/src/features/channels/ui/BotActivityBar.tsx
@@ -2,8 +2,7 @@ import * as React from "react";
 import { Loader2 } from "lucide-react";
 
 import { useAgentTranscript } from "@/features/agents/ui/useObserverEvents";
-import type { TranscriptItem } from "@/features/agents/ui/agentSessionTypes";
-import { formatToolTitle } from "@/features/agents/ui/agentSessionToolCatalog";
+import { getActivityHeadline } from "@/features/agents/ui/agentSessionTranscriptPresentation";
 import type { UserProfileLookup } from "@/features/profile/lib/identity";
 import type { ManagedAgent } from "@/shared/api/types";
 import { cn } from "@/shared/lib/cn";
@@ -27,18 +26,6 @@ const HOVER_OPEN_DELAY_MS = 150;
 const HOVER_CLOSE_DELAY_MS = 180;
 const HEADLINE_ROTATION_MS = 2200;
 
-function getActivityHeadline(item: TranscriptItem): string | null {
-  if (item.type === "tool") {
-    return formatToolTitle(item.buzzToolName ?? item.toolName, item.title);
-  }
-
-  if (item.type === "message") {
-    return item.role === "assistant" ? "Responding" : item.title;
-  }
-
-  return item.title;
-}
-
 export function BotActivityComposerAction({
   agents,
   channelId = null,
@@ -184,16 +171,17 @@ export function BotActivityComposerAction({
                 className={cn(
                   "border border-background",
                   isInline
-                    ? "!h-[18px] !w-[18px] shadow-xs ring-1 ring-primary/25 text-[7px]"
-                    : "!h-5 !w-5 text-[8px]",
+                    ? "!h-[18px] !w-[18px] shadow-xs ring-1 ring-primary/25 text-xs leading-none"
+                    : "shrink-0",
                 )}
                 displayName={agent.name}
                 key={agent.pubkey}
+                size="xs"
               />
             ))}
           </span>
           {typingAgents.length > 2 ? (
-            <span className="text-[11px] leading-none">
+            <span className="text-xs leading-none">
               +{typingAgents.length - 2}
             </span>
           ) : null}
@@ -240,8 +228,9 @@ export function BotActivityComposerAction({
               >
                 <UserAvatar
                   avatarUrl={agentAvatarUrl(agent)}
-                  className="!h-6 !w-6 shrink-0 text-[9px]"
+                  className="shrink-0"
                   displayName={agent.name}
+                  size="sm"
                 />
                 <span className="min-w-0 flex-1 truncate">{agent.name}</span>
                 <Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground/70" />
diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx
index 7f8582b0b4..a99e3e2d0b 100644
--- a/desktop/src/features/channels/ui/ChannelScreen.tsx
+++ b/desktop/src/features/channels/ui/ChannelScreen.tsx
@@ -185,25 +185,6 @@ export function ChannelScreen({
         : [],
     [activeChannel],
   );
-  const messageProfilePubkeys = React.useMemo(
-    () => [
-      ...new Set([
-        ...messageAuthorPubkeys,
-        ...messageMentionPubkeys,
-        ...activeDmParticipantPubkeys,
-        ...typingEntries.map((entry) => entry.pubkey),
-      ]),
-    ],
-    [
-      activeDmParticipantPubkeys,
-      messageAuthorPubkeys,
-      messageMentionPubkeys,
-      typingEntries,
-    ],
-  );
-  const messageProfilesQuery = useUsersBatchQuery(messageProfilePubkeys, {
-    enabled: messageProfilePubkeys.length > 0,
-  });
   const channelMembersQuery = useChannelMembersQuery(activeChannel?.id ?? null);
   const channelMembers = channelMembersQuery.data;
   const managedAgentsQuery = useManagedAgentsQuery();
@@ -225,6 +206,27 @@ export function ChannelScreen({
     }
     return pubkeys;
   }, [channelMembers, managedAgents, relayAgents]);
+  const messageProfilePubkeys = React.useMemo(
+    () => [
+      ...new Set([
+        ...messageAuthorPubkeys,
+        ...messageMentionPubkeys,
+        ...activeDmParticipantPubkeys,
+        ...agentPubkeys,
+        ...typingEntries.map((entry) => entry.pubkey),
+      ]),
+    ],
+    [
+      activeDmParticipantPubkeys,
+      agentPubkeys,
+      messageAuthorPubkeys,
+      messageMentionPubkeys,
+      typingEntries,
+    ],
+  );
+  const messageProfilesQuery = useUsersBatchQuery(messageProfilePubkeys, {
+    enabled: messageProfilePubkeys.length > 0,
+  });
   const allAgentSessionCandidates = React.useMemo(
     () =>
       buildChannelAgentSessionCandidates({
diff --git a/desktop/src/features/channels/ui/useChannelActivityTyping.ts b/desktop/src/features/channels/ui/useChannelActivityTyping.ts
index 007f090d39..d3943dbb11 100644
--- a/desktop/src/features/channels/ui/useChannelActivityTyping.ts
+++ b/desktop/src/features/channels/ui/useChannelActivityTyping.ts
@@ -98,7 +98,7 @@ export function mergeAgentNamesIntoProfiles(
   relayAgents: RelayAgent[],
 ): UserProfileLookup {
   const merged = { ...profiles };
-  for (const agent of [...relayAgents, ...managedAgents]) {
+  for (const agent of relayAgents) {
     const key = normalizePubkey(agent.pubkey);
     merged[key] = {
       ...merged[key],
@@ -108,5 +108,15 @@ export function mergeAgentNamesIntoProfiles(
       isAgent: true,
     };
   }
+  for (const agent of managedAgents) {
+    const key = normalizePubkey(agent.pubkey);
+    merged[key] = {
+      ...merged[key],
+      displayName: merged[key]?.displayName || agent.name,
+      avatarUrl: merged[key]?.avatarUrl ?? agent.avatarUrl,
+      nip05Handle: merged[key]?.nip05Handle ?? null,
+      isAgent: true,
+    };
+  }
   return merged;
 }
diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts
index 11e25da4d3..0954193e86 100644
--- a/desktop/src/shared/api/tauri.ts
+++ b/desktop/src/shared/api/tauri.ts
@@ -45,10 +45,7 @@ import type {
   OpenDmInput,
 } from "@/shared/api/types";
 
-type RawIdentity = {
-  pubkey: string;
-  display_name: string;
-};
+type RawIdentity = { pubkey: string; display_name: string };
 
 type RawProfile = {
   pubkey: string;
@@ -213,6 +210,7 @@ export type RawManagedAgent = {
   max_turn_duration_seconds: number | null;
   parallelism: number;
   system_prompt: string | null;
+  avatar_url?: string | null;
   model: string | null;
   mcp_toolsets: string | null;
   env_vars?: Record<string, string>;
@@ -868,6 +866,7 @@ export function fromRawManagedAgent(agent: RawManagedAgent): ManagedAgent {
     maxTurnDurationSeconds: agent.max_turn_duration_seconds,
     parallelism: agent.parallelism,
     systemPrompt: agent.system_prompt,
+    avatarUrl: agent.avatar_url ?? null,
     model: agent.model,
     mcpToolsets: agent.mcp_toolsets,
     envVars: agent.env_vars ?? {},
diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts
index 653a1be992..99df2caa42 100644
--- a/desktop/src/shared/api/types.ts
+++ b/desktop/src/shared/api/types.ts
@@ -282,6 +282,7 @@ export type ManagedAgent = {
   maxTurnDurationSeconds: number | null;
   parallelism: number;
   systemPrompt: string | null;
+  avatarUrl: string | null;
   model: string | null;
   mcpToolsets: string | null;
   /** Per-agent env vars. Layered on top of persona envVars. */
diff --git a/desktop/src/shared/ui/UserAvatar.tsx b/desktop/src/shared/ui/UserAvatar.tsx
index cab6a8fd66..8b7ede4613 100644
--- a/desktop/src/shared/ui/UserAvatar.tsx
+++ b/desktop/src/shared/ui/UserAvatar.tsx
@@ -6,8 +6,8 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar";
 type UserAvatarSize = "xs" | "sm" | "md";
 
 const sizeClasses: Record<UserAvatarSize, string> = {
-  xs: "h-5 w-5 text-[8px]",
-  sm: "h-6 w-6 text-[9px]",
+  xs: "h-5 w-5 text-xs",
+  sm: "h-6 w-6 text-xs",
   md: "h-10 w-10 text-xs",
 };
 
diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx
index 61e498f9ea..3a3eb50fc7 100644
--- a/desktop/src/shared/ui/markdown.tsx
+++ b/desktop/src/shared/ui/markdown.tsx
@@ -224,6 +224,7 @@ type MarkdownProps = {
   agentMentionPubkeysByName?: Record<string, string>;
   mentionNames?: string[];
   mentionPubkeysByName?: Record<string, string>;
+  mediaInset?: boolean;
   searchQuery?: string;
   tight?: boolean;
   videoReviewContext?: VideoReviewContext;
@@ -748,6 +749,7 @@ function createMarkdownComponents(
   variant: MarkdownVariant,
   runtimeRef: React.RefObject<MarkdownRuntime>,
   interactive = true,
+  mediaInset = false,
 ): Components {
   const paragraphClassName =
     variant === "tight"
@@ -898,7 +900,12 @@ function createMarkdownComponents(
       if (resolvedSrc?.endsWith(".mp4")) {
         const entry = src ? imetaByUrl?.get(src) : undefined;
         return (
-          <span data-block-media="">
+          <span
+            className={cn(
+              mediaInset && "mx-1.5 block max-w-[calc(100%-0.75rem)]",
+            )}
+            data-block-media=""
+          >
             <MarkdownVideoPlayer
               key={src ?? resolvedSrc}
               alt={alt}
@@ -910,7 +917,13 @@ function createMarkdownComponents(
         );
       }
       return (
-        <span data-block-media="" className="block">
+        <span
+          data-block-media=""
+          className={cn(
+            "block",
+            mediaInset && "mx-1.5 max-w-[calc(100%-0.75rem)]",
+          )}
+        >
           <ImageBlock alt={alt} resolvedSrc={resolvedSrc} src={src} />
         </span>
       );
@@ -1115,6 +1128,7 @@ function MarkdownInner({
   imetaByUrl,
   interactive = true,
   agentMentionPubkeysByName,
+  mediaInset = false,
   mentionNames,
   mentionPubkeysByName,
   searchQuery,
@@ -1161,8 +1175,9 @@ function MarkdownInner({
   });
 
   const components = React.useMemo(
-    () => createMarkdownComponents(variant, runtimeRef, interactive),
-    [variant, runtimeRef, interactive],
+    () =>
+      createMarkdownComponents(variant, runtimeRef, interactive, mediaInset),
+    [variant, runtimeRef, interactive, mediaInset],
   );
 
   // biome-ignore lint/suspicious/noExplicitAny: PluggableList type not directly importable
@@ -1276,6 +1291,7 @@ export const Markdown = React.memo(
     prev.compact === next.compact &&
     prev.customEmoji === next.customEmoji &&
     prev.interactive === next.interactive &&
+    prev.mediaInset === next.mediaInset &&
     prev.tight === next.tight &&
     prev.agentMentionPubkeysByName === next.agentMentionPubkeysByName &&
     prev.mentionPubkeysByName === next.mentionPubkeysByName &&
diff --git a/desktop/src/shared/ui/toggle.tsx b/desktop/src/shared/ui/toggle.tsx
index 5d3671d8b6..e6e9eac4fe 100644
--- a/desktop/src/shared/ui/toggle.tsx
+++ b/desktop/src/shared/ui/toggle.tsx
@@ -10,10 +10,14 @@ const toggleVariants = cva(
     variants: {
       variant: {
         default: "bg-transparent",
-        outline: "border border-input/40 bg-background hover:bg-muted/70",
+        ghost:
+          "bg-transparent hover:bg-muted/70 hover:text-foreground data-[state=on]:bg-muted data-[state=on]:text-foreground",
+        outline:
+          "border border-input/40 bg-background hover:bg-muted/70 data-[state=on]:bg-muted data-[state=on]:text-foreground",
       },
       size: {
         default: "h-9 px-3 min-w-9",
+        xs: "h-5 min-h-0 min-w-0 gap-1 rounded-md px-1.5 text-xs font-medium [&_svg]:size-3.5",
         sm: "h-8 px-2 min-w-8",
         lg: "h-10 px-3 min-w-10",
       },
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index f3481d2343..8b8e591035 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -38,6 +38,7 @@ type MockCommandAvailability = {
 type MockManagedAgentSeed = {
   pubkey: string;
   name: string;
+  avatarUrl?: string | null;
   personaId?: string | null;
   status?: RawManagedAgent["status"];
   channelNames?: string[];
@@ -358,6 +359,7 @@ type RawManagedAgent = {
   max_turn_duration_seconds: number | null;
   parallelism: number;
   system_prompt: string | null;
+  avatar_url: string | null;
   model: string | null;
   env_vars?: Record<string, string>;
   status: "running" | "stopped" | "deployed" | "not_deployed";
@@ -873,6 +875,7 @@ function cloneManagedAgent(agent: MockManagedAgent): RawManagedAgent {
     max_turn_duration_seconds: agent.max_turn_duration_seconds ?? null,
     parallelism: agent.parallelism,
     system_prompt: agent.system_prompt,
+    avatar_url: agent.avatar_url ?? null,
     model: agent.model,
     env_vars: { ...(agent.env_vars ?? {}) },
     status: agent.status,
@@ -962,6 +965,7 @@ function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent {
     max_turn_duration_seconds: null,
     parallelism: 1,
     system_prompt: null,
+    avatar_url: seed.avatarUrl ?? null,
     model: null,
     env_vars: {},
     status,
@@ -4747,6 +4751,7 @@ async function handleCreateManagedAgent(
     max_turn_duration_seconds: args.input.maxTurnDurationSeconds ?? null,
     parallelism: args.input.parallelism ?? 1,
     system_prompt: args.input.systemPrompt?.trim() || null,
+    avatar_url: avatarUrl,
     model: args.input.model?.trim() || null,
     env_vars: { ...(args.input.envVars ?? {}) },
     status: args.input.spawnAfterCreate ? "running" : "stopped",
diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts
index 6f4f533274..9057568594 100644
--- a/desktop/tests/e2e/channels.spec.ts
+++ b/desktop/tests/e2e/channels.spec.ts
@@ -773,6 +773,12 @@ test("shows and clears activity indicators for active channel agents", async ({
   await expect(page.getByTestId("agent-session-thread-panel")).toContainText(
     "alice",
   );
+  await expect(page.getByTestId("agent-transcript-now-summary")).toHaveCount(0);
+  await expect(page.getByTestId("agent-session-stop-turn")).toBeVisible();
+  await expect(page.getByTestId("agent-session-stop-turn")).toBeDisabled();
+  await expect(page.getByTestId("agent-session-thread-panel")).toContainText(
+    "No ACP activity yet",
+  );
   await expect(page.getByTestId("message-typing-indicator")).toHaveCount(0);
 
   await page.evaluate((pubkey) => {
