Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip title="Inspect session">
<Tooltip title={open ? "Hide inspector" : "Inspect session"}>
<Button
type="text"
type={open ? "primary" : "text"}
size="small"
icon={<MagnifyingGlass size={14} />}
disabled={!sessionId}
onClick={() => sessionId && openSession(sessionId)}
onClick={() => sessionId && toggleSession(sessionId)}
aria-label="Inspect session"
aria-pressed={open}
/>
</Tooltip>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -65,5 +66,6 @@ export function LensBody({
/>
)
if (lens === "context") return <ContextLens sessionId={sessionId} focusedTurn={focusedTurn} />
if (lens === "response") return <ResponseLens sessionId={sessionId} />
return <RuntimeLens sessionId={sessionId} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const LABEL: Record<InspectorLens, string> = {
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
Expand All @@ -21,6 +22,8 @@ const DESC: Record<InspectorLens, string> = {
"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({
Expand All @@ -32,7 +35,7 @@ export function LensRail({
}) {
return (
<div className="flex shrink-0 items-center gap-1 border-0 border-b border-solid border-colorSplit px-2 py-1.5">
{(["timeline", "context", "runtime"] as InspectorLens[]).map((l) => (
{(["timeline", "context", "runtime", "response"] as InspectorLens[]).map((l) => (
<Tooltip key={l} title={DESC[l]} placement="bottom" mouseEnterDelay={0.4}>
<Button
type="text"
Expand All @@ -52,7 +55,8 @@ export function LensRail({
)
}

/** The three lens bodies, prop-driven — reused by both hosts. */
/** The lens bodies, prop-driven — reused by both hosts. */
export {TimelineLens} from "./lenses/TimelineLens"
export {ContextLens} from "./lenses/ContextLens"
export {RuntimeLens} from "./lenses/RuntimeLens"
export {ResponseLens} from "./lenses/ResponseLens"
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* ResponseLens — the session's response-channel preference (stream vs batch). Session-scoped like
* Runtime: it sets how THIS conversation's `/invoke` calls negotiate transport (the `Accept` header
* `buildAgentRequest` sends), NOT revision config. Persisted per session via
* `agentChannelModeAtomFamily`. A focused turn doesn't change it — it's a session preference.
*/
import type {ReactNode} from "react"

import {agentChannelModeAtomFamily, type AgentChannelMode} from "@agenta/playground"
import {Broadcast, Package} from "@phosphor-icons/react"
import {Typography} from "antd"
import {useAtom} from "jotai"

const OPTIONS: {
value: AgentChannelMode
label: string
icon: ReactNode
blurb: string
}[] = [
{
value: "stream",
label: "Stream",
icon: <Broadcast size={18} />,
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: <Package size={18} />,
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 (
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-3 overflow-y-auto px-3 py-3">
<Typography.Text type="secondary" className="text-xs">
How this session receives replies from the agent. A transport preference for this
conversation only — it is not saved on the revision.
</Typography.Text>
<div role="radiogroup" aria-label="Response channel" className="flex flex-col gap-2">
{OPTIONS.map((opt) => {
const selected = mode === opt.value
return (
<button
key={opt.value}
type="button"
role="radio"
aria-checked={selected}
onClick={() => setMode(opt.value)}
className={`flex cursor-pointer items-start gap-3 rounded-lg border border-solid px-3 py-2.5 text-left transition-colors ${
selected
? "border-[var(--ag-colorPrimary)] bg-[var(--ag-colorPrimaryBg)]"
: "border-colorBorderSecondary bg-transparent hover:bg-colorFillTertiary"
}`}
>
<span
className={`mt-0.5 shrink-0 ${
selected
? "text-[var(--ag-colorPrimary)]"
: "text-colorTextSecondary"
}`}
>
{opt.icon}
</span>
<span className="flex min-w-0 flex-col gap-0.5">
<span
className={`text-xs font-medium ${
selected
? "text-[var(--ag-colorPrimary)]"
: "text-colorText"
}`}
>
{opt.label}
</span>
<span className="text-[11px] leading-snug text-colorTextTertiary">
{opt.blurb}
</span>
</span>
</button>
)
})}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -286,12 +274,6 @@ const PlaygroundHeader: React.FC<PlaygroundHeaderProps> = ({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()
Expand All @@ -304,42 +286,8 @@ const PlaygroundHeader: React.FC<PlaygroundHeaderProps> = ({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 ? (
<Check size={14} />
) : (
<span className="inline-block w-[14px]" />
),
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 ? (
<Check size={14} />
) : (
<span className="inline-block w-[14px]" />
),
onClick: () => setChannelMode(option.value),
})),
},
...(virtualizationAvailable
? [
{type: "divider" as const},
{
key: "virtualization",
type: "group" as const,
Expand Down Expand Up @@ -396,10 +344,6 @@ const PlaygroundHeader: React.FC<PlaygroundHeaderProps> = ({className, ...divPro
],
[
virtualizationAvailable,
layout,
setLayout,
channelMode,
setChannelMode,
virtualize,
setVirtualize,
overscan,
Expand Down Expand Up @@ -871,18 +815,20 @@ const PlaygroundHeader: React.FC<PlaygroundHeaderProps> = ({className, ...divPro
{label: "Chat", value: "chat"},
]}
/>
<Dropdown
trigger={["click"]}
placement="bottomRight"
styles={{root: {width: 180}}}
menu={{items: settingsMenuItems}}
>
<Button
type="text"
icon={<GearSix size={16} />}
aria-label="Playground settings"
/>
</Dropdown>
{(settingsMenuItems?.length ?? 0) > 0 && (
<Dropdown
trigger={["click"]}
placement="bottomRight"
styles={{root: {width: 180}}}
menu={{items: settingsMenuItems}}
>
<Button
type="text"
icon={<GearSix size={16} />}
aria-label="Playground settings"
/>
</Dropdown>
)}
</>
)}
</div>
Expand Down
Loading
Loading