diff --git a/web/oss/src/components/AgentChatSlice/components/Inspector/InspectSessionButton.tsx b/web/oss/src/components/AgentChatSlice/components/Inspector/InspectSessionButton.tsx
index 40fa9ce81e..e9eb2996de 100644
--- a/web/oss/src/components/AgentChatSlice/components/Inspector/InspectSessionButton.tsx
+++ b/web/oss/src/components/AgentChatSlice/components/Inspector/InspectSessionButton.tsx
@@ -1,25 +1,27 @@
/**
- * "Inspect session" trigger (build-spec §6) — opens the docked Inspector at Session scope. The
+ * "Inspect session" trigger (build-spec §6) — toggles the docked Inspector at Session scope. The
* first-class session entry point (the old panel only reached session view via the in-panel
- * toggle). Placed in the thread's session controls.
+ * toggle). Placed in the thread's session controls; clicking again collapses the panel.
*/
import {MagnifyingGlass} from "@phosphor-icons/react"
import {Button, Tooltip} from "antd"
-import {useSetAtom} from "jotai"
+import {useAtomValue, useSetAtom} from "jotai"
-import {openInspectorSessionAtom} from "./state"
+import {inspectorTargetAtom, toggleInspectorSessionAtom} from "./state"
export default function InspectSessionButton({sessionId}: {sessionId: string | null}) {
- const openSession = useSetAtom(openInspectorSessionAtom)
+ const toggleSession = useSetAtom(toggleInspectorSessionAtom)
+ const open = useAtomValue(inspectorTargetAtom)?.sessionId === sessionId && !!sessionId
return (
-
+
}
disabled={!sessionId}
- onClick={() => sessionId && openSession(sessionId)}
+ onClick={() => sessionId && toggleSession(sessionId)}
aria-label="Inspect session"
+ aria-pressed={open}
/>
)
diff --git a/web/oss/src/components/AgentChatSlice/components/Inspector/LensBody.tsx b/web/oss/src/components/AgentChatSlice/components/Inspector/LensBody.tsx
index fd2979a419..066a7cc712 100644
--- a/web/oss/src/components/AgentChatSlice/components/Inspector/LensBody.tsx
+++ b/web/oss/src/components/AgentChatSlice/components/Inspector/LensBody.tsx
@@ -9,6 +9,7 @@ import {CopyButton} from "@agenta/ui/components/presentational"
import {useAtomValue} from "jotai"
import {ContextLens} from "./lenses/ContextLens"
+import {ResponseLens} from "./lenses/ResponseLens"
import {RuntimeLens} from "./lenses/RuntimeLens"
import {TimelineLens} from "./lenses/TimelineLens"
import type {InspectorLens} from "./state"
@@ -65,5 +66,6 @@ export function LensBody({
/>
)
if (lens === "context") return
+ if (lens === "response") return
return
}
diff --git a/web/oss/src/components/AgentChatSlice/components/Inspector/LensRail.tsx b/web/oss/src/components/AgentChatSlice/components/Inspector/LensRail.tsx
index f3230fbb5c..0b46aa0af2 100644
--- a/web/oss/src/components/AgentChatSlice/components/Inspector/LensRail.tsx
+++ b/web/oss/src/components/AgentChatSlice/components/Inspector/LensRail.tsx
@@ -10,6 +10,7 @@ const LABEL: Record = {
timeline: "Timeline",
context: "Context",
runtime: "Runtime",
+ response: "Response",
}
// One-line "what is this and when do I use it" for each lens — surfaced as a tab tooltip so the
@@ -21,6 +22,8 @@ const DESC: Record = {
"What the model saw — the role-tagged messages fed to the model, with an approximate token count. For auditing the context window.",
runtime:
"Live sandbox for this session — streams, session state, and mounts. Session-level, not per turn.",
+ response:
+ "How this session receives replies — stream token-by-token or batch in one frame. A per-session transport preference.",
}
export function LensRail({
@@ -32,7 +35,7 @@ export function LensRail({
}) {
return (
- {(["timeline", "context", "runtime"] as InspectorLens[]).map((l) => (
+ {(["timeline", "context", "runtime", "response"] as InspectorLens[]).map((l) => (
,
+ blurb: "Render the reply token-by-token as the agent produces it. Best for watching the agent think and for long responses.",
+ },
+ {
+ value: "batch",
+ label: "Batch",
+ icon: ,
+ blurb: "Wait for the full reply, then land it in one frame. Skips the live stream — useful when comparing final outputs or when the handler can only batch.",
+ },
+]
+
+export function ResponseLens({sessionId}: {sessionId: string}) {
+ const [mode, setMode] = useAtom(agentChannelModeAtomFamily(sessionId))
+
+ return (
+
+
+ How this session receives replies from the agent. A transport preference for this
+ conversation only — it is not saved on the revision.
+
+
+ )
+}
diff --git a/web/oss/src/components/AgentChatSlice/components/Inspector/state.ts b/web/oss/src/components/AgentChatSlice/components/Inspector/state.ts
index f660a7ee51..b112d8a678 100644
--- a/web/oss/src/components/AgentChatSlice/components/Inspector/state.ts
+++ b/web/oss/src/components/AgentChatSlice/components/Inspector/state.ts
@@ -7,7 +7,7 @@
import {atom} from "jotai"
import {atomWithStorage} from "jotai/utils"
-export type InspectorLens = "timeline" | "context" | "runtime"
+export type InspectorLens = "timeline" | "context" | "runtime" | "response"
export type TimelineFilter = "all" | "tools" | "interactions"
/** The open target. `null` = collapsed. `focusedTurn` (1-based) narrows the lenses to one turn;
@@ -41,6 +41,14 @@ export const openInspectorSessionAtom = atom(null, (_get, set, sessionId: string
set(inspectorTargetAtom, {sessionId, focusedTurn: null})
})
+/** Toggle the "Inspect session" trigger: close when already open on this session, else open it at
+ * session scope (a focused turn on the same session still counts as open, so it collapses). */
+export const toggleInspectorSessionAtom = atom(null, (get, set, sessionId: string) => {
+ if (!sessionId) return
+ const open = get(inspectorTargetAtom)?.sessionId === sessionId
+ set(inspectorTargetAtom, open ? null : {sessionId, focusedTurn: null})
+})
+
/** Open focused on a specific turn (the "Inspect turn" trigger) — scrolls/highlights it. */
export const openInspectorTurnAtom = atom(
null,
diff --git a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx
index 6de72cecf3..981430ee82 100644
--- a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx
+++ b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx
@@ -14,15 +14,9 @@ import {
} from "@agenta/entities/workflow"
import type {EvaluatorCatalogTemplate, Workflow, WorkflowTypeColor} from "@agenta/entities/workflow"
import {EntityPicker} from "@agenta/entity-ui"
-import {agentTemplateLayoutAtom, AGENT_TEMPLATE_LAYOUTS} from "@agenta/entity-ui/drill-in"
import {type WorkflowRevisionSelectionResult} from "@agenta/entity-ui/selection"
import {useEnrichedEvaluatorOnlyAdapter as useEvaluatorOnlyAdapter} from "@agenta/entity-ui/selection"
-import {
- playgroundController,
- isAgentModeAtomFamily,
- agentChannelModeAtom,
- type AgentChannelMode,
-} from "@agenta/playground"
+import {playgroundController, isAgentModeAtomFamily} from "@agenta/playground"
import {usePlaygroundLayout} from "@agenta/playground-ui/hooks"
import {textColors} from "@agenta/ui"
import {VersionBadge} from "@agenta/ui/components/presentational"
@@ -91,12 +85,6 @@ type PlaygroundHeaderProps = BaseContainerProps
/** Entity types that represent evaluator downstream nodes */
const EVALUATOR_ENTITY_TYPES = ["workflow"]
-// Response channel the agent playground speaks to the backend (transport concern, not config).
-const CHANNEL_OPTIONS: {value: AgentChannelMode; label: string}[] = [
- {value: "stream", label: "Stream"},
- {value: "batch", label: "Batch"},
-]
-
/** Resolves a user UUID to a display name via workspace members */
const MemberAuthor: React.FC<{userId: string}> = ({userId}) => {
const memberAtom = useMemo(() => workspaceMemberByIdFamily(userId), [userId])
@@ -286,12 +274,6 @@ const PlaygroundHeader: React.FC = ({className, ...divPro
const onboarding = useOptionalOnboardingContext()
const chromeHidden = !!onboarding && !onboarding.chromeRevealed
- // Agent playground settings (page-level): config-panel layout + stream/batch response channel.
- // These were previously buried in a config item's kebab; they're global, so they live here.
- const layout = useAtomValue(agentTemplateLayoutAtom)
- const setLayout = useSetAtom(agentTemplateLayoutAtom)
- const channelMode = useAtomValue(agentChannelModeAtom)
- const setChannelMode = useSetAtom(agentChannelModeAtom)
// SPIKE(virtuoso): live-tunable virtualization knobs (enable + overscan + row estimate).
// The whole section is hidden unless the NEXT_PUBLIC_AGENT_CHAT_VIRTUALIZATION env flag is set.
const virtualizationAvailable = isAgentChatVirtualizationAvailable()
@@ -304,42 +286,8 @@ const PlaygroundHeader: React.FC = ({className, ...divPro
const settingsMenuItems: MenuProps["items"] = useMemo(
() => [
- {
- key: "view",
- type: "group" as const,
- label: "View",
- children: AGENT_TEMPLATE_LAYOUTS.map((option) => ({
- key: `view-${option.value}`,
- label: option.label,
- icon:
- layout === option.value ? (
-
- ) : (
-
- ),
- onClick: () => setLayout(option.value),
- })),
- },
- {type: "divider" as const},
- {
- key: "channel",
- type: "group" as const,
- label: "Response",
- children: CHANNEL_OPTIONS.map((option) => ({
- key: `channel-${option.value}`,
- label: option.label,
- icon:
- channelMode === option.value ? (
-
- ) : (
-
- ),
- onClick: () => setChannelMode(option.value),
- })),
- },
...(virtualizationAvailable
? [
- {type: "divider" as const},
{
key: "virtualization",
type: "group" as const,
@@ -396,10 +344,6 @@ const PlaygroundHeader: React.FC = ({className, ...divPro
],
[
virtualizationAvailable,
- layout,
- setLayout,
- channelMode,
- setChannelMode,
virtualize,
setVirtualize,
overscan,
@@ -871,18 +815,20 @@ const PlaygroundHeader: React.FC = ({className, ...divPro
{label: "Chat", value: "chat"},
]}
/>
-
- }
- aria-label="Playground settings"
- />
-
+ {(settingsMenuItems?.length ?? 0) > 0 && (
+
+ }
+ aria-label="Playground settings"
+ />
+
+ )}
>
)}
diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx
index e0e6d03545..96f2d502f0 100644
--- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx
+++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx
@@ -39,7 +39,6 @@ import {agentSelfCommitSignalAtom, openAgentConfigSectionAtom} from "@agenta/sha
import {stripAgentaMetadataDeep} from "@agenta/shared/utils"
import {
ConfigAccordionSection,
- sectionIndicatorColor,
type SectionIndicatorTone,
} from "@agenta/ui/components/presentational"
import {useDrillInUI} from "@agenta/ui/drill-in"
@@ -53,7 +52,7 @@ import {
SlidersHorizontal,
Wrench,
} from "@phosphor-icons/react"
-import {Button, Tabs, Tooltip, Typography} from "antd"
+import {Button, Tooltip, Typography} from "antd"
import deepEqual from "fast-deep-equal"
import {useAtom, useAtomValue, useStore} from "jotai"
@@ -71,7 +70,6 @@ import {ToolManagementList} from "./agentTemplate/ToolManagementList"
import {useAgentTools} from "./agentTemplate/useAgentTools"
import {useConfigItemDrawer} from "./agentTemplate/useConfigItemDrawer"
import {useModelHarness} from "./agentTemplate/useModelHarness"
-import {agentTemplateLayoutAtom} from "./agentTemplateLayout"
import {ConfigItemDrawer} from "./ConfigItemDrawer"
import {connectionFromConfig, modelIdFromConfig} from "./connectionUtils"
import {InstructionsDrawer} from "./InstructionsDrawer"
@@ -268,9 +266,6 @@ export function AgentTemplateControl({
// Enable Save only when the draft actually differs from what we opened with (config or build-kit).
const sectionDirty = isCurrentSectionDirty()
- // Layout (accordion / tabs / cards) is a global persisted preference; the panel only reads it.
- const layout = useAtomValue(agentTemplateLayoutAtom)
-
// `config` IS the agent template (`parameters.agent`); `schema` is the `agent-template` type and
// decides which sections exist. Portable fields (instructions / llm / tools / mcps / skills) are
// FLAT; execution parts (harness / runner / sandbox) are nested sub-objects (see useModelHarness).
@@ -341,8 +336,8 @@ export function AgentTemplateControl({
// feeds both sections), so they live in their own hook that returns the summaries + bodies.
//
// TWO instances, on purpose:
- // - `mh` is bound to the LIVE entity — it drives the accordion header summaries + the inline
- // tabs bodies. Keeping it live means a section header NEVER reflects the drawer's unsaved draft
+ // - `mh` is bound to the LIVE entity — it drives the accordion header summaries. Keeping it live
+ // means a section header NEVER reflects the drawer's unsaved draft
// (the reported bug: editing in the open drawer updated the background summary).
// - The DRAFT instance (config + build-kit buffer) that drives the OPEN section drawer's body
// now lives inside `ModelHarnessSectionDrawerBody`, mounted only while the drawer is open, so
@@ -714,8 +709,7 @@ export function AgentTemplateControl({
)
- // Each config section as a descriptor, so it can be rendered in any layout (accordion /
- // tabs / cards) without duplicating the content. Schema-gated, like before.
+ // Each config section as a descriptor rendered by the accordion. Schema-gated, like before.
const sections = [
mh.hasModelOrHarness && {
key: "model-harness",
@@ -725,8 +719,6 @@ export function AgentTemplateControl({
indicator: headerIndicator("model-harness"),
defaultOpen: true,
onOpen: () => openSectionDrawer("model-harness"),
- content: mh.modelHarnessDrawerBody,
- inlineContent: mh.modelHarnessInline,
},
hasInstructions && {
key: "instructions",
@@ -852,8 +844,6 @@ export function AgentTemplateControl({
defaultOpen: false,
summary: mh.advancedSummary,
onOpen: () => openSectionDrawer("advanced"),
- content: mh.advancedDrawerBody,
- inlineContent: mh.advancedInline,
},
].filter(Boolean) as {
key: string
@@ -864,16 +854,10 @@ export function AgentTemplateControl({
indicator?: {tone: SectionIndicatorTone; tooltip?: string}
defaultOpen?: boolean
onOpen?: () => void
- content: React.ReactNode
- // Trimmed single-column body for the tabs layout (drawer sections only); falls back to
- // `content` when a section has no separate inline form.
- inlineContent?: React.ReactNode
+ // Only the inline (non-`onOpen`) sections render a body; drawer-opening sections omit it.
+ content?: React.ReactNode
}[]
- // Each config section is a contained card on the raised Config panel — the surface tokens give
- // it depth against the panel (see theme-variables.css "Agent Playground surface ladder").
- const sectionCardClass = "ag-surface-card rounded-[11px] px-4"
-
// Keep the item + instruction drawers MOUNTED while they animate closed. Their editing state
// goes null on close; retaining the last value and driving `open` off the live state lets the
// exit transition play (an unmount-on-close drawer just vanishes). Matches the SectionDrawers.
@@ -890,72 +874,6 @@ export function AgentTemplateControl({
No agent configuration fields are available for this schema.
- ) : layout === "tabs" ? (
- // Tabs renders each section's body inline (no drawer), so edits are live. Drawer
- // sections supply a trimmed `inlineContent` so the tab shows just their controls.
- ({
- key: s.key,
- label: (
-
-
-
- {s.icon}
- {s.indicator ? (
-
- ) : null}
-
-
- {s.title}
- {sectionBadge(s.key)}
-
- ),
- children: (
- // Render `extra` (the add-action) here too, else tab users can't add
- // items. Body is the trimmed `inlineContent` or `content`.
-
) : (
sections.map((s, index) => {
// Controlled keys drive `open`/`onOpenChange` so the agent can auto-expand them;
diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx
index 2e65eec892..8d6b6b6316 100644
--- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx
+++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx
@@ -689,10 +689,6 @@ export function useModelHarness({
{modelHarnessControls}
)
- // Trimmed body for the tabs layout: the same controls in one column, without the drawer's
- // two-panel split or side panel (which read as out-of-place chrome inside a tab).
- const modelHarnessInline =
{modelHarnessControls}
-
// Advanced header summary: sandbox only now — mode UI moved to the Provider credentials section.
const advancedSummary = sandbox.kind ? `Sandbox: ${String(sandbox.kind)}` : undefined
@@ -832,11 +828,6 @@ export function useModelHarness({
)
- // Trimmed body for the tabs layout: the grouped controls in one column, no side panel.
- const advancedInline = (
-
{advancedControls}
- )
-
return {
hasModelOrHarness,
mcpSupported,
@@ -848,12 +839,10 @@ export function useModelHarness({
modelUnsupported: !!modelId && !selectedKeepsModel,
modelSummary,
modelHarnessDrawerBody,
- modelHarnessInline,
// The capability-aware (two-panel) drawer is wider than the plain one.
modelHarnessDrawerWidth: capabilities ? 880 : 560,
hasAdvanced,
advancedSummary,
advancedDrawerBody,
- advancedInline,
}
}
diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplateLayout.ts b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplateLayout.ts
deleted file mode 100644
index 2f515956f0..0000000000
--- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplateLayout.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Agent config-panel layout preference.
- *
- * The agent config panel ({@link AgentTemplateControl}) can render its sections as an accordion,
- * tabs, or cards. The chosen layout is a global, persisted UI preference rather than per-variant
- * state, so the selector can live in the variant header menu (away from the panel itself) while the
- * panel reads the same value. Persisted to localStorage so it survives reloads.
- */
-import {atomWithStorage} from "jotai/utils"
-
-export type AgentTemplateLayout = "accordion" | "tabs" | "cards"
-
-/** The selectable layouts, in display order. Shared by the panel and the header-menu selector. */
-export const AGENT_TEMPLATE_LAYOUTS: {label: string; value: AgentTemplateLayout}[] = [
- {label: "Accordion", value: "accordion"},
- {label: "Tabs", value: "tabs"},
- {label: "Cards", value: "cards"},
-]
-
-export const agentTemplateLayoutAtom = atomWithStorage(
- "agenta:agent-config-layout",
- "accordion",
-)
diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts
index dbce314038..fabb730ebb 100644
--- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts
+++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/index.ts
@@ -104,8 +104,6 @@ export {McpServerFormView} from "./McpServerFormView"
export type {McpServerFormViewProps} from "./McpServerFormView"
export {SkillFormView} from "./SkillFormView"
export type {SkillFormViewProps} from "./SkillFormView"
-export {agentTemplateLayoutAtom, AGENT_TEMPLATE_LAYOUTS} from "./agentTemplateLayout"
-export type {AgentTemplateLayout} from "./agentTemplateLayout"
// ============================================================================
// COMPOSITE CONTROLS
diff --git a/web/packages/agenta-entity-ui/src/DrillInView/index.ts b/web/packages/agenta-entity-ui/src/DrillInView/index.ts
index a38b3f9054..f489604117 100644
--- a/web/packages/agenta-entity-ui/src/DrillInView/index.ts
+++ b/web/packages/agenta-entity-ui/src/DrillInView/index.ts
@@ -293,10 +293,6 @@ export type {
FieldsDetectionContextValue,
} from "./SchemaControls"
-// Agent config layout preference (read by the variant header menu's View selector).
-export {agentTemplateLayoutAtom, AGENT_TEMPLATE_LAYOUTS} from "./SchemaControls"
-export type {AgentTemplateLayout} from "./SchemaControls"
-
// Operational panel regions (Triggers, Mounts) — siblings of the Configuration section.
export {
AgentOperationsSections,
diff --git a/web/packages/agenta-playground/src/index.ts b/web/packages/agenta-playground/src/index.ts
index 0a6332ef9a..e6501fef93 100644
--- a/web/packages/agenta-playground/src/index.ts
+++ b/web/packages/agenta-playground/src/index.ts
@@ -74,7 +74,7 @@ export {
isAgentModeAtomFamily,
buildAgentRequest,
buildAgentReferences,
- agentChannelModeAtom,
+ agentChannelModeAtomFamily,
createNegotiatingFetch,
} from "./state"
export type {AgentRequest, AgentChannelMode, NegotiatingFetch} from "./state"
diff --git a/web/packages/agenta-playground/src/state/execution/agentRequest.ts b/web/packages/agenta-playground/src/state/execution/agentRequest.ts
index ac692dcde5..67d0e31ae1 100644
--- a/web/packages/agenta-playground/src/state/execution/agentRequest.ts
+++ b/web/packages/agenta-playground/src/state/execution/agentRequest.ts
@@ -34,7 +34,7 @@ import {projectIdAtom} from "@agenta/shared/state"
import {getDefaultStore} from "jotai"
import {withBuildKitOverlay} from "./buildKitOverlay"
-import {agentChannelModeAtom} from "./channelMode"
+import {agentChannelModeAtomFamily} from "./channelMode"
import {executionHeadersAtom} from "./webWorkerIntegration"
// Re-exported so existing consumers keep importing it from the request builder; the merge
@@ -380,7 +380,7 @@ export async function buildAgentRequest(
// send an explicit Accept.)
// Negotiation 2 (format): `x-ag-messages-format: vercel` selects the vercel adapter for
// the UIMessage request body (`data.inputs.messages`) and the response projection.
- const channelMode = store.get(agentChannelModeAtom)
+ const channelMode = store.get(agentChannelModeAtomFamily(opts.sessionId))
const headers: Record = {
Accept: channelMode === "batch" ? "application/json" : "text/event-stream",
"x-ag-messages-format": "vercel",
diff --git a/web/packages/agenta-playground/src/state/execution/channelMode.ts b/web/packages/agenta-playground/src/state/execution/channelMode.ts
index 2ceab6aca3..6d84ea270a 100644
--- a/web/packages/agenta-playground/src/state/execution/channelMode.ts
+++ b/web/packages/agenta-playground/src/state/execution/channelMode.ts
@@ -1,9 +1,10 @@
import {atom} from "jotai"
+import {atomFamily, atomWithStorage} from "jotai/utils"
export type AgentChannelMode = "stream" | "batch"
/**
- * How the agent playground talks to the agent `/invoke` endpoint:
+ * How the agent playground talks to the agent `/invoke` endpoint, PER SESSION:
* - `stream` (default): request the real-time SSE UIMessage stream `useChat` renders
* token-by-token. If the backend can't stream (the handler can only batch → 406), the
* transport's `createNegotiatingFetch` middleware transparently falls back to a batch.
@@ -11,7 +12,22 @@ export type AgentChannelMode = "stream" | "batch"
* front; the transport replays it as a one-shot UIMessage stream so it lands in one frame.
*
* This is a transport/controller concern (which channel the playground PREFERS), NOT revision
- * config — it is never persisted on the agent revision. `buildAgentRequest` reads it to set the
- * `Accept` header; the playground kebab menu writes it. Stream is the default for agents.
+ * config — it is never persisted on the agent revision. It is scoped to the conversation, so two
+ * sessions can prefer different channels; the Session Inspector's Response lens writes it and
+ * `buildAgentRequest` reads it (keyed by `session_id`) to set the `Accept` header. Persisted per
+ * session so the preference survives reloads. Stream is the default for agents.
*/
-export const agentChannelModeAtom = atom("stream")
+const channelModeBySessionAtom = atomWithStorage>(
+ "agenta:agent-channel-mode-by-session",
+ {},
+)
+
+export const agentChannelModeAtomFamily = atomFamily((sessionId: string) =>
+ atom(
+ (get) => get(channelModeBySessionAtom)[sessionId] ?? "stream",
+ (get, set, next: AgentChannelMode) => {
+ const all = get(channelModeBySessionAtom)
+ set(channelModeBySessionAtom, {...all, [sessionId]: next})
+ },
+ ),
+)
diff --git a/web/packages/agenta-playground/src/state/execution/index.ts b/web/packages/agenta-playground/src/state/execution/index.ts
index c59d582628..f6a0778ca5 100644
--- a/web/packages/agenta-playground/src/state/execution/index.ts
+++ b/web/packages/agenta-playground/src/state/execution/index.ts
@@ -359,7 +359,7 @@ export {
type AgentRequest,
} from "./agentRequest"
// Stream vs batch response channel for the agent lane (read by buildAgentRequest's Accept header).
-export {agentChannelModeAtom, type AgentChannelMode} from "./channelMode"
+export {agentChannelModeAtomFamily, type AgentChannelMode} from "./channelMode"
// Transport negotiation: try stream, fall back to batch on 406, error gracefully otherwise.
export {createNegotiatingFetch, type NegotiatingFetch} from "./agentNegotiation"
// Agent-lane HITL resume predicate (approve AND deny both resume the conversation).
diff --git a/web/packages/agenta-playground/src/state/index.ts b/web/packages/agenta-playground/src/state/index.ts
index af6690e74b..eee75406f1 100644
--- a/web/packages/agenta-playground/src/state/index.ts
+++ b/web/packages/agenta-playground/src/state/index.ts
@@ -181,7 +181,7 @@ export {
buildAgentReferences,
type AgentRequest,
} from "./execution"
-export {agentChannelModeAtom, type AgentChannelMode} from "./execution"
+export {agentChannelModeAtomFamily, type AgentChannelMode} from "./execution"
export {createNegotiatingFetch, type NegotiatingFetch} from "./execution"
export {agentShouldResumeAfterApproval} from "./execution"
export {buildRenderMap, renderKindFor, type RenderHintLike} from "./execution"
diff --git a/web/packages/agenta-playground/tests/unit/agentRequest.test.ts b/web/packages/agenta-playground/tests/unit/agentRequest.test.ts
index 6620e068cd..596975e814 100644
--- a/web/packages/agenta-playground/tests/unit/agentRequest.test.ts
+++ b/web/packages/agenta-playground/tests/unit/agentRequest.test.ts
@@ -54,7 +54,7 @@ import {
buildAgentRequest,
buildAgentReferences,
} from "../../src/state/execution/agentRequest"
-import {agentChannelModeAtom} from "../../src/state/execution/channelMode"
+import {agentChannelModeAtomFamily} from "../../src/state/execution/channelMode"
import {executionHeadersAtom} from "../../src/state/execution/webWorkerIntegration"
const REAL_APP = "11111111-1111-4111-8111-111111111111"
@@ -506,13 +506,13 @@ describe("buildAgentRequest", () => {
})
it("requests batch JSON via Accept when the channel toggle is `batch`", async () => {
- // Negotiation 1 (transport): the channel toggle drives Accept. `batch` asks /invoke
- // for a single WorkflowBatchResponse, which AgentChatTransport replays as one frame.
- store.set(agentChannelModeAtom, "batch")
+ // Negotiation 1 (transport): the per-session channel toggle drives Accept. `batch` asks
+ // /invoke for a single WorkflowBatchResponse, which AgentChatTransport replays as one frame.
+ store.set(agentChannelModeAtomFamily("s1"), "batch")
seed(store, "e", {})
const req = await buildAgentRequest("e", [], {sessionId: "s1", store})
expect(req!.headers.Accept).toBe("application/json")
- store.set(agentChannelModeAtom, "stream")
+ store.set(agentChannelModeAtomFamily("s1"), "stream")
})
it("declares the Vercel message format via x-ag-messages-format", async () => {