Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const AgentRevisionSelector = ({variantId}: {variantId: string}) => {
onChange={(value) => handleSwitchVariant(value)}
value={_variantId ?? undefined}
borderlessTrigger
versioning="linear"
/>
{variantRevision !== null && variantRevision !== undefined && (
<Tooltip
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import {useCallback, useRef, useState} from "react"

import {VariantDetailsWithStatus} from "@agenta/entity-ui/variant"
import {CopySimple} from "@phosphor-icons/react"
import {dayjs} from "@agenta/shared/utils"
import {Check, CopySimple} from "@phosphor-icons/react"
import {Button, Tooltip} from "antd"
import clsx from "clsx"

interface RevisionMetadata {
message?: string | null
commitMessage?: string | null
created_at?: string | null
createdAt?: string | null
updated_at?: string | null
updatedAt?: string | null
}

interface RevisionChildTitleProps {
revisionId: string
Expand All @@ -10,10 +23,52 @@ interface RevisionChildTitleProps {
isDisabled: boolean
showLatestTag: boolean
showAsCompare: boolean
showBadges?: boolean
linear?: boolean
isCurrent?: boolean
onCreateLocalCopy: (revisionId: string, e: React.MouseEvent) => void
latestRevisionId?: string | null
}

const OverflowMessage = ({message}: {message: string}) => {
const textRef = useRef<HTMLSpanElement>(null)
const [open, setOpen] = useState(false)

const handleOpenChange = useCallback((nextOpen: boolean) => {
if (!nextOpen) {
setOpen(false)
return
}

const text = textRef.current
setOpen(Boolean(text && text.scrollWidth > text.clientWidth))
}, [])

return (
<div className="w-full min-w-0 max-w-full overflow-hidden">
<Tooltip
open={open}
onOpenChange={handleOpenChange}
placement="left"
mouseEnterDelay={0.5}
styles={{root: {maxWidth: 300}}}
title={
<span className="line-clamp-6 whitespace-pre-wrap break-words text-xs leading-relaxed">
{message}
</span>
}
>
<span
ref={textRef}
className="block w-full min-w-0 max-w-full truncate text-xs text-[var(--ant-color-text-secondary)]"
>
{message}
</span>
</Tooltip>
</div>
)
}

const RevisionChildTitle = ({
revisionId,
variantName,
Expand All @@ -22,10 +77,62 @@ const RevisionChildTitle = ({
isDisabled,
showLatestTag,
showAsCompare,
showBadges = true,
linear = false,
isCurrent = false,
onCreateLocalCopy,
latestRevisionId,
}: RevisionChildTitleProps) => {
const isLatest = !!latestRevisionId && revisionId === latestRevisionId
const revision = variant as RevisionMetadata
const commitMessage = revision.message?.trim() || revision.commitMessage?.trim()
const timestamp =
revision.created_at ?? revision.createdAt ?? revision.updated_at ?? revision.updatedAt
const date = timestamp ? dayjs(timestamp) : null
const relativeTime = date?.isValid() ? date.fromNow() : null
const exactTime = date?.isValid() ? date.format("MMM D, YYYY, h:mm A") : null

if (linear) {
return (
<div className="group/revision flex min-h-12 w-full min-w-0 flex-col justify-center gap-1.5 overflow-hidden px-1.5 py-2">
<div className="flex w-full min-w-0 items-center gap-2">
<span className="shrink-0 rounded-md bg-[var(--ant-color-fill-secondary)] px-2 py-1 text-xs font-medium text-[var(--ant-color-text)]">
v{version}
</span>
<span className="flex-1" />
{isLatest && showLatestTag ? (
<span className="shrink-0 text-[10px] font-medium text-[var(--ant-color-primary)]">
Latest
</span>
) : null}
{relativeTime ? (
<time
className={clsx(
"shrink-0 whitespace-nowrap text-[11px] text-[var(--ant-color-text-tertiary)]",
{"mr-2": !isCurrent},
)}
dateTime={timestamp ?? undefined}
title={exactTime ?? undefined}
>
{relativeTime}
</time>
) : null}
{isCurrent ? (
<span className="mr-0.5 flex size-5 shrink-0 items-center justify-center text-[var(--ant-color-primary)]">
<Check size={14} aria-label="Current revision" />
</span>
) : null}
</div>
{commitMessage ? (
<OverflowMessage message={commitMessage} />
) : (
<span className="block w-full min-w-0 truncate text-xs italic text-[var(--ant-color-text-tertiary)]">
No commit message
</span>
)}
</div>
)
}

return (
<div
Expand All @@ -37,7 +144,7 @@ const RevisionChildTitle = ({
revision={version}
variant={variant as any}
hideName
showBadges
showBadges={showBadges}
showLatestTag={showLatestTag}
isLatest={isLatest}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ const SelectVariant = ({
customBrowseAdapter,
style,
borderlessTrigger = false,
versioning = "branching",
...props
}: SelectVariantProps) => {
const isLinearVersioning = versioning === "linear"
const selectedVariants = useAtomValue(playgroundController.selectors.entityIds())
const setSelectedVariants = useSetAtom(playgroundController.actions.setEntityIds)
const selectedAppId = useAtomValue(selectedAppIdAtom)
Expand Down Expand Up @@ -123,13 +125,14 @@ const SelectVariant = ({
selectedExistsInAppList,
])

const selectPlaceholder =
!showAsCompare &&
!!singleSelectedValue &&
selectedRevisionQuery.isPending &&
!hasSelectedDisplayName
? "Loading variant..."
: "Select variant"
const selectPlaceholder = isLinearVersioning
? "History"
: !showAsCompare &&
!!singleSelectedValue &&
selectedRevisionQuery.isPending &&
!hasSelectedDisplayName
? "Loading variant..."
: "Select variant"

// Scoped adapter: 2-level (Variant → Revision), scoped to the current app
const scopedAdapter = useMemo(
Expand Down Expand Up @@ -258,15 +261,26 @@ const SelectVariant = ({
variantName={c.variantName ?? c.name ?? ""}
version={c.version ?? 0}
variant={c}
isDisabled={disabledIds.has(c.id)}
isDisabled={!isLinearVersioning && disabledIds.has(c.id)}
showLatestTag={showLatestTag}
showAsCompare={showAsCompare}
showBadges={!isLinearVersioning}
linear={isLinearVersioning}
isCurrent={isLinearVersioning && selectedValueForControl === c.id}
onCreateLocalCopy={handleCreateLocalCopy}
latestRevisionId={latestRevisionId}
/>
)
},
[showLatestTag, showAsCompare, handleCreateLocalCopy, disabledIds, latestRevisionId],
[
showLatestTag,
showAsCompare,
handleCreateLocalCopy,
disabledIds,
latestRevisionId,
isLinearVersioning,
selectedValueForControl,
],
)

const renderSelectedLabel = useCallback(
Expand Down Expand Up @@ -403,6 +417,7 @@ const SelectVariant = ({
// Uses singleSelectedValue directly (not selectedValueForControl which
// may be undefined while the existence check resolves).
const triggerLabel = useMemo(() => {
if (isLinearVersioning && mode === "scoped") return "History"
if (!singleSelectedValue) return selectPlaceholder
if (isLocalDraftId(singleSelectedValue)) {
return selectedVariantName ?? selectedRevisionData?.name ?? "Draft"
Expand Down Expand Up @@ -433,6 +448,7 @@ const SelectVariant = ({
selectPlaceholder,
mode,
workflowName,
isLinearVersioning,
])

// Initial expanded keys for browse mode — expand the parent workflow of the selected revision
Expand Down Expand Up @@ -503,7 +519,7 @@ const SelectVariant = ({
adapter={browseAdapter}
onSelect={handleBrowseSelect}
selectedValue={selectedValueForControl}
disabledChildIds={disabledIds}
disabledChildIds={isLinearVersioning ? undefined : disabledIds}
renderChildTitle={renderChildTitle}
renderSelectedLabel={renderSelectedLabel}
defaultExpandAll={false}
Expand Down Expand Up @@ -547,12 +563,17 @@ const SelectVariant = ({
adapter={scopedAdapter}
onSelect={handleSingleSelect}
selectedValue={selectedValueForControl}
disabledChildIds={disabledIds}
renderParentTitle={renderParentTitle}
disabledChildIds={isLinearVersioning ? undefined : disabledIds}
renderParentTitle={
isLinearVersioning ? undefined : renderParentTitle
}
renderChildTitle={renderChildTitle}
renderSelectedLabel={renderSelectedLabel}
width={280}
maxHeight={400}
width={280}
showSearch={!isLinearVersioning}
flattenSingleParent={isLinearVersioning}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export interface SelectVariantProps extends TreeSelectProps {
* header so the revision picker reads as an identity, not a form switch.
*/
borderlessTrigger?: boolean
/**
* Controls how revisions are presented. Linear versioning hides the persisted
* variant level and deployment badges and labels the control as History.
*/
versioning?: "linear" | "branching"
}

export interface TreeSelectItemRendererProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
clearRegistryVariantNameCache,
} from "@/oss/components/VariantsComponents/store/registryStore"
import {selectedAppIdAtom} from "@/oss/state/app"
import {currentWorkflowAtom} from "@/oss/state/workflow"

import {CommitVariantChangesModalProps} from "./assets/types"

Expand All @@ -43,6 +44,8 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({
const isEphemeral = useAtomValue(workflowMolecule.selectors.isEphemeral(variantId || ""))
const isEvaluator = useAtomValue(workflowMolecule.selectors.isEvaluator(variantId || ""))
const isApplication = useAtomValue(workflowMolecule.selectors.isApplication(variantId || ""))
const isAgent = useAtomValue(workflowMolecule.selectors.isAgent(variantId || ""))
const currentWorkflow = useAtomValue(currentWorkflowAtom)

const appId = useAtomValue(selectedAppIdAtom)
const commitRevision = useSetAtom(playgroundController.actions.commitRevision)
Expand All @@ -51,6 +54,7 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({
const {mutateAsync: publish} = useAtomValue(publishMutationAtom)

const variantName = runnableData?.name || "Variant"
const commitTargetName = isAgent ? currentWorkflow?.name || "Agent" : variantName
const variantSlug = runnableData?.slug

// Environments offered in the footer's "Commit & deploy to …" split-button dropdown.
Expand All @@ -73,7 +77,7 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({
deployEnvironments: string[] | undefined,
deployMessage?: string,
) => {
if (!deployEnvironments || deployEnvironments.length === 0) return
if (isAgent || !deployEnvironments || deployEnvironments.length === 0) return
const newRevisionData = workflowMolecule.get.data(newRevisionId)
const refs = {
revisionId: newRevisionId,
Expand Down Expand Up @@ -105,7 +109,7 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({
message.error(`Couldn't publish ${label} to ${failed.join(", ")}`)
}
},
[publish, runnableData, appId],
[publish, runnableData, appId, isAgent],
)

const handleSubmit = useCallback(
Expand Down Expand Up @@ -145,7 +149,9 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({
return {success: true, newRevisionId: result.newRevisionId}
}

const selectedMode = mode === "variant" ? "variant" : "version"
// Agents always append to their one persisted timeline. Ignore a stale
// or externally supplied branching mode as a mechanical guardrail.
const selectedMode = !isAgent && mode === "variant" ? "variant" : "version"
const note = commitMessage ?? undefined

if (selectedMode === "variant") {
Expand Down Expand Up @@ -221,6 +227,7 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({
},
[
isEphemeral,
isAgent,
createFromEphemeral,
createVariant,
variantId,
Expand All @@ -234,13 +241,13 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({

const commitModes = useMemo(
() =>
isEvaluator
isEvaluator || isAgent
? [{id: "version", label: "As a new version"}]
: [
{id: "version", label: `Update ${variantName}`},
{id: "variant", label: "Save as a new variant"},
],
[isEvaluator, variantName],
[isEvaluator, isAgent, variantName],
)

// For ephemeral entities, render a simplified "Create" modal with editable name.
Expand Down Expand Up @@ -284,11 +291,11 @@ const CommitVariantChangesModal: React.FC<CommitVariantChangesModalProps> = ({
entity={{
type: "variant",
id: variantId,
name: variantName,
name: commitTargetName,
}}
commitModes={commitModes}
defaultCommitMode="version"
commitDeployOptions={isEvaluator ? undefined : commitDeployOptions}
commitDeployOptions={isEvaluator || isAgent ? undefined : commitDeployOptions}
canSubmit={({mode, entityName}) => {
if (mode === "variant") {
if (!entityName?.trim()) return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,16 +296,8 @@ const PlaygroundVariantConfigHeader = ({
</>
) : (
<>
{!embedded && !isEvaluatorEntity && (
// Agents get a labeled secondary "Deploy" so the action row reads as a
// hierarchy (primary Commit, secondary Deploy, ghost kebab); other
// surfaces keep the icon-only deploy.
<DeployVariantButton
revisionId={variantId}
{...(isAgentEffective
? ({label: "Deploy", type: "default", size: "small"} as const)
: {})}
/>
{!embedded && !isEvaluatorEntity && !isAgentEffective && (
<DeployVariantButton revisionId={variantId} />
)}

<CommitVariantChangesButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export const useSidebarConfig = (): MainSidebarItems => {
icon: <LightningIcon size={14} />,
disabled: !hasProjectURL,
dataTour: "registry-nav",
workflowCategories: ["app", "agent"],
workflowCategories: ["app"],
},
{
key: "app-evaluations-link",
Expand Down
Loading
Loading