Skip to content
Open
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
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 @@ -57,8 +57,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 @@ -122,13 +124,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 @@ -257,15 +260,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 @@ -363,6 +377,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 All @@ -388,6 +403,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 @@ -458,7 +474,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 @@ -502,12 +518,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}
popupMinWidth={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 @@ -169,7 +169,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