diff --git a/web/oss/src/components/Playground/Components/AgentRevisionSelector/index.tsx b/web/oss/src/components/Playground/Components/AgentRevisionSelector/index.tsx index 074d242e33..782020dad6 100644 --- a/web/oss/src/components/Playground/Components/AgentRevisionSelector/index.tsx +++ b/web/oss/src/components/Playground/Components/AgentRevisionSelector/index.tsx @@ -63,6 +63,7 @@ const AgentRevisionSelector = ({variantId}: {variantId: string}) => { onChange={(value) => handleSwitchVariant(value)} value={_variantId ?? undefined} borderlessTrigger + versioning="linear" /> {variantRevision !== null && variantRevision !== undefined && ( void latestRevisionId?: string | null } +const OverflowMessage = ({message}: {message: string}) => { + const textRef = useRef(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 ( +
+ + {message} + + } + > + + {message} + + +
+ ) +} + const RevisionChildTitle = ({ revisionId, variantName, @@ -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 ( +
+
+ + v{version} + + + {isLatest && showLatestTag ? ( + + Latest + + ) : null} + {relativeTime ? ( + + ) : null} + {isCurrent ? ( + + + + ) : null} +
+ {commitMessage ? ( + + ) : ( + + No commit message + + )} +
+ ) + } return (
diff --git a/web/oss/src/components/Playground/Components/Menus/SelectVariant/index.tsx b/web/oss/src/components/Playground/Components/Menus/SelectVariant/index.tsx index 8c5f634978..1ae4ae1c69 100644 --- a/web/oss/src/components/Playground/Components/Menus/SelectVariant/index.tsx +++ b/web/oss/src/components/Playground/Components/Menus/SelectVariant/index.tsx @@ -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) @@ -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( @@ -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( @@ -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" @@ -388,6 +403,7 @@ const SelectVariant = ({ selectPlaceholder, mode, workflowName, + isLinearVersioning, ]) // Initial expanded keys for browse mode — expand the parent workflow of the selected revision @@ -458,7 +474,7 @@ const SelectVariant = ({ adapter={browseAdapter} onSelect={handleBrowseSelect} selectedValue={selectedValueForControl} - disabledChildIds={disabledIds} + disabledChildIds={isLinearVersioning ? undefined : disabledIds} renderChildTitle={renderChildTitle} renderSelectedLabel={renderSelectedLabel} defaultExpandAll={false} @@ -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} /> )}
diff --git a/web/oss/src/components/Playground/Components/Menus/SelectVariant/types.d.ts b/web/oss/src/components/Playground/Components/Menus/SelectVariant/types.d.ts index 58d9bfcc61..beba367bb2 100644 --- a/web/oss/src/components/Playground/Components/Menus/SelectVariant/types.d.ts +++ b/web/oss/src/components/Playground/Components/Menus/SelectVariant/types.d.ts @@ -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 { diff --git a/web/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/index.tsx b/web/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/index.tsx index 5e0e2b245a..4e4311612e 100644 --- a/web/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/index.tsx +++ b/web/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/index.tsx @@ -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" @@ -43,6 +44,8 @@ const CommitVariantChangesModal: React.FC = ({ 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) @@ -51,6 +54,7 @@ const CommitVariantChangesModal: React.FC = ({ 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. @@ -73,7 +77,7 @@ const CommitVariantChangesModal: React.FC = ({ 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, @@ -105,7 +109,7 @@ const CommitVariantChangesModal: React.FC = ({ message.error(`Couldn't publish ${label} to ${failed.join(", ")}`) } }, - [publish, runnableData, appId], + [publish, runnableData, appId, isAgent], ) const handleSubmit = useCallback( @@ -145,7 +149,9 @@ const CommitVariantChangesModal: React.FC = ({ 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") { @@ -221,6 +227,7 @@ const CommitVariantChangesModal: React.FC = ({ }, [ isEphemeral, + isAgent, createFromEphemeral, createVariant, variantId, @@ -234,13 +241,13 @@ const CommitVariantChangesModal: React.FC = ({ 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. @@ -284,11 +291,11 @@ const CommitVariantChangesModal: React.FC = ({ 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 diff --git a/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/assets/PlaygroundVariantConfigHeader.tsx b/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/assets/PlaygroundVariantConfigHeader.tsx index d77f49fed8..2d4128131c 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/assets/PlaygroundVariantConfigHeader.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundVariantConfig/assets/PlaygroundVariantConfigHeader.tsx @@ -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. - + {!embedded && !isEvaluatorEntity && !isAgentEffective && ( + )} { icon: , disabled: !hasProjectURL, dataTour: "registry-nav", - workflowCategories: ["app", "agent"], + workflowCategories: ["app"], }, { key: "app-evaluations-link", diff --git a/web/oss/src/state/workflow/hooks.ts b/web/oss/src/state/workflow/hooks.ts index 3779742638..5ad21a5238 100644 --- a/web/oss/src/state/workflow/hooks.ts +++ b/web/oss/src/state/workflow/hooks.ts @@ -82,6 +82,7 @@ export const useWorkflowsData = () => { currentWorkflow: currentWorkflow ?? null, workflowKind: ctx.workflowKind, isApp: ctx.workflowKind === "app", + isAgent: ctx.workflowKind === "app" && currentWorkflow?.flags?.is_agent === true, isEvaluator: ctx.workflowKind === "evaluator", isSnippet: ctx.workflowKind === "snippet", // Terminal states (mutually exclusive). diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/TreeSelectPopupContent.tsx b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/TreeSelectPopupContent.tsx index aa3959cf2d..e2f07cb267 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/TreeSelectPopupContent.tsx +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/TreeSelectPopupContent.tsx @@ -44,6 +44,8 @@ export type TreeSelectPopupContentProps = Om * Children for these parents will be fetched on mount. */ initialExpandedKeys?: string[] + /** Hide a sole structural parent and render its children as a flat list. */ + flattenSingleParent?: boolean } // ============================================================================ @@ -77,6 +79,7 @@ export function TreeSelectPopupContent({ width = 280, showDate = false, initialExpandedKeys, + flattenSingleParent = false, }: TreeSelectPopupContentProps) { const generatedId = useId() const instanceId = providedInstanceId ?? generatedId @@ -138,18 +141,21 @@ export function TreeSelectPopupContent({ ".ant-tree-treenode-selected, .ant-tree-treenode-checkbox-checked", ) if (hit) { - hit.scrollIntoView({block: "center"}) + hit.scrollIntoView({block: "center", inline: "nearest"}) + if (flattenSingleParent) el.scrollLeft = 0 return } if (++scrollRetryRef.current < 5) rafId = requestAnimationFrame(attempt) } rafId = requestAnimationFrame(attempt) return () => cancelAnimationFrame(rafId) - }, [selectedValueProp, treeData]) + }, [flattenSingleParent, selectedValueProp, treeData]) // Get display messages const displayEmptyMessage = emptyMessage ?? resolvedAdapter.emptyMessage ?? "No items found" const displayLoadingMessage = loadingMessage ?? resolvedAdapter.loadingMessage ?? "Loading..." + const displayTreeData = + flattenSingleParent && treeData.length === 1 ? (treeData[0].children ?? []) : treeData // Handle tree node selection const handleTreeSelect = useCallback( @@ -195,7 +201,7 @@ export function TreeSelectPopupContent({ {popupHeader} {/* Loading state — only shown when no tree data is available yet */} - {(isLoadingParents || isLoadingChildren) && treeData.length === 0 && ( + {(isLoadingParents || isLoadingChildren) && displayTreeData.length === 0 && (
{displayLoadingMessage} @@ -208,22 +214,38 @@ export function TreeSelectPopupContent({ )} {/* Empty state */} - {!isLoadingParents && !parentsError && treeData.length === 0 && ( -
- {displayEmptyMessage} -
- )} + {!isLoadingParents && + !isLoadingChildren && + !parentsError && + displayTreeData.length === 0 && ( +
+ {displayEmptyMessage} +
+ )} {/* Tree list */} - {!isLoadingParents && !parentsError && treeData.length > 0 && ( + {!isLoadingParents && !parentsError && displayTreeData.length > 0 && (