Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 10 additions & 2 deletions web/oss/src/components/AgentChatSlice/AgentChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,22 @@ const AgentConversation = ({entityId, sessionId}: {entityId: string; sessionId:
// Teardown for the in-flight smooth scroll (removes its listeners + fallback timer).
const pinCleanupRef = useRef<(() => void) | null>(null)

// `useChat` pins its `Chat` (and thus this transport) for the life of the session `id`; it is
// NOT recreated when `entityId` changes (only on an `id` change). So the request builder must
// read the CURRENT entity through a ref — capturing `entityId` by value would send every turn
// with the revision that was displayed when the session first mounted, even after a switch or a
// self-commit. Reading `entityIdRef.current` at send time keeps runs on the live revision.
const entityIdRef = useRef(entityId)
entityIdRef.current = entityId

// Transport feeds the v6 stream request from the playground pipeline. `api` here is a
// placeholder that `prepareSendMessagesRequest` overrides per request.
const transport = useMemo(
() =>
new AgentChatTransport({
api: "",
prepareSendMessagesRequest: async ({messages, id}) => {
const req = await buildAgentRequest(entityId, messages, {
const req = await buildAgentRequest(entityIdRef.current, messages, {
sessionId: id ?? sessionId,
})
if (!req) {
Expand All @@ -246,7 +254,7 @@ const AgentConversation = ({entityId, sessionId}: {entityId: string; sessionId:
return {api: req.invocationUrl, headers: req.headers, body: req.requestBody}
},
}),
[entityId, sessionId],
[sessionId],
)

const {
Expand Down
52 changes: 47 additions & 5 deletions web/oss/src/components/Playground/Components/MainLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {memo, useCallback, useRef, type ReactNode} from "react"
import {memo, useCallback, useMemo, useRef, type ReactNode} from "react"

import {workflowMolecule} from "@agenta/entities/workflow"
import type {ConfigViewMode} from "@agenta/entity-ui"
import {
executionController,
Expand Down Expand Up @@ -128,6 +129,33 @@ const PlaygroundMainView = ({
// Which entity IDs to render config panels for
const configEntityIds = configEntityIdsOverride ?? layoutEntityIds

// ── Agent generation host: keep a stable key across revision switches ──
// The agent chat surface lives inside the generation panel below. Keying that panel by the
// revision id (`variantId`) tears the whole conversation down on every revision switch —
// whether the agent self-commits a new revision or the user picks one in the config header —
// which aborts the live stream ("connection lost") and drops the still-streaming turn
// (persistence is skipped mid-stream). Agents are single-entity (excluded from comparison),
// so we give the single-agent generation panel a stable key; a switch then flows through as an
// `entityId` prop update, not a remount, and the conversation (app/session-scoped) survives.
// A freshly committed revision's flags load a beat after the swap (workflowType falls back to
// "completion" until then), so we LATCH the agent host across that gap and only drop it once
// the single entity has loaded as a definitively non-agent workflow.
const singleEntityId =
!isComparisonView && layoutEntityIds.length === 1 ? layoutEntityIds[0]! : ""
const isSingleAgentEntity = useAtomValue(isAgentModeAtomFamily(singleEntityId))
const singleEntityQuery = useAtomValue(
useMemo(() => workflowMolecule.selectors.query(singleEntityId), [singleEntityId]),
)
const agentHostRef = useRef(false)
if (!singleEntityId) {
agentHostRef.current = false
} else if (isSingleAgentEntity) {
agentHostRef.current = true
} else if (!singleEntityQuery.isPending) {
agentHostRef.current = false
}
const renderAgentGenerationHost = agentHostRef.current

// The agent config panel is a compact read-only summary (editing happens in section drawers), so
// it stays narrow (550px) instead of the prompt config's 50/50 split. The default is a fixed px
// width rather than a percentage on purpose: antd applies `defaultSize` verbatim at mount and only
Expand Down Expand Up @@ -336,8 +364,22 @@ const PlaygroundMainView = ({
) : isComparisonView && hasDisplayedEntities ? (
<GenerationComparisonRenderer />
) : (
layoutEntityIds.map((variantId) =>
displayedEntities.includes(variantId) || isEvaluatorMode ? (
layoutEntityIds.map((variantId) => {
// Single-agent view: a stable key so a revision switch updates
// the entityId prop instead of remounting the live conversation.
// Rendered unconditionally (not gated on `displayedEntities`) so
// it can't blink to the placeholder during the atomic id swap.
if (renderAgentGenerationHost) {
return (
<ExecutionItems
key="agent-generation-host"
entityId={variantId}
renderTestsetActions={renderTestsetActions}
/>
)
}
return displayedEntities.includes(variantId) ||
isEvaluatorMode ? (
<ExecutionItems
key={variantId}
entityId={variantId}
Expand All @@ -347,8 +389,8 @@ const PlaygroundMainView = ({
<GenerationPanelPlaceholder
key={`generation-placeholder-${variantId}`}
/>
),
)
)
})
)}
</section>
</SplitterPanel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useMemo} from "react"
import {useMemo, useRef} from "react"

import {workflowMolecule} from "@agenta/entities/workflow"
import {executionController} from "@agenta/playground"
Expand Down Expand Up @@ -52,6 +52,35 @@ const PlaygroundGenerations: React.FC<PlaygroundGenerationsProps> = ({
const AgentGenerationPanel = usePlaygroundUIOptional()?.AgentGenerationPanel
const isExecutionLoading = runnableQuery.isPending || isChat === undefined

// Latch the agent surface so a revision switch never unmounts the live chat conversation.
// A switch (self-commit or the config-header picker) points `entityId` at a new revision whose
// flags load a beat later — `isAgent` flips false and the query goes pending, which would drop
// this panel to the skeleton and tear down the agent chat (aborting the live stream, losing the
// in-progress turn). Once an agent, we keep rendering the agent panel across that load gap so
// the switch is just an `entityId` prop update; we only release the latch when the entity has
// loaded as a definitively non-agent workflow. (Paired with the stable key in MainLayout, which
// keeps THIS component mounted so the latch survives the switch.)
const agentSurfaceRef = useRef(false)
if (isAgent) {
agentSurfaceRef.current = true
} else if (!runnableQuery.isPending) {
agentSurfaceRef.current = false
}
if (agentSurfaceRef.current && AgentGenerationPanel) {
return (
<div className="flex h-full min-h-0 w-full flex-col">
<ExecutionHeader
entityId={entityId}
renderTestsetActions={renderTestsetActions}
onRepeatCountChange={onRepeatCountChange}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
<div className="min-h-0 flex-1">
<AgentGenerationPanel entityId={entityId} />
</div>
</div>
)
}

if (isExecutionLoading) {
return (
<div className="w-full">
Expand Down
Loading