Compare commits

...

30 Commits

Author SHA1 Message Date
Claude
8d98f349e9 Merge branch 'dev' into feat/publish-newer-agent-marketplace
Resolve conflicts after reverting PR #11655:
- Update backend store imports to use new api.features.store paths
- Keep include_changelog parameter functionality from feature branch
- Simplify frontend PublishAgentModal to match dev structure
- Remove old server/v2/store structure in favor of api/features/store
2025-12-21 10:18:25 +00:00
Claude
dd3c006f31 Revert "feat(backend): add agent mode support to SmartDecisionMakerBlock with autonomous tool execution loops (#11547) (#11655)"
This reverts commit 51c10fa224.
2025-12-21 10:14:56 +00:00
Zamil Majdy
51c10fa224 feat(backend): add agent mode support to SmartDecisionMakerBlock with autonomous tool execution loops (#11547) (#11655)
## Summary

<img width="2072" height="1836" alt="image"

src="https://github.com/user-attachments/assets/9d231a77-6309-46b9-bc11-befb5d8e9fcc"
/>

**🚀 Major Feature: Agent Mode Support**

Adds autonomous agent mode to SmartDecisionMakerBlock, enabling it to
execute tools directly in loops until tasks are completed, rather than
just yielding tool calls for external execution.

##  **Key New Features**

### 🤖 **Agent Mode with Tool Execution Loops**
- **New `agent_mode_max_iterations` parameter** controls execution
behavior:
  - `0` = Traditional mode (single LLM call, yield tool calls)
  - `1+` = Agent mode with iteration limit
  - `-1` = Infinite agent mode (loop until finished)

### 🔄 **Autonomous Tool Execution**  
- **Direct tool execution** instead of yielding for external handling
- **Multi-iteration loops** with conversation state management
- **Automatic completion detection** when LLM stops making tool calls
- **Iteration limit handling** with graceful completion messages

### 🏗️ **Proper Database Operations**
- **Replace manual execution ID generation** with proper
`upsert_execution_input`/`upsert_execution_output`
- **Real NodeExecutionEntry objects** from database results
- **Proper execution status management**: QUEUED → RUNNING →
COMPLETED/FAILED

### 🔧 **Enhanced Type Safety**
- **Pydantic models** replace TypedDict: `ToolInfo` and
`ExecutionParams`
- **Runtime validation** with better error messages
- **Improved developer experience** with IDE support

## 🔧 **Technical Implementation**

### Agent Mode Flow:
```python
# Agent mode enabled with iterations
if input_data.agent_mode_max_iterations != 0:
    async for result in self._execute_tools_agent_mode(...):
        yield result  # "conversations", "finished"
    return

# Traditional mode (existing behavior)  
# Single LLM call + yield tool calls for external execution
```

### Tool Execution with Database Operations:
```python
# Before: Manual execution IDs
tool_exec_id = f"{node_exec_id}_tool_{sink_node_id}_{len(input_data)}"

# After: Proper database operations
node_exec_result, final_input_data = await db_client.upsert_execution_input(
    node_id=sink_node_id,
    graph_exec_id=execution_params.graph_exec_id,
    input_name=input_name, 
    input_data=input_value,
)
```

### Type Safety with Pydantic:
```python
# Before: Dict access prone to errors
execution_params["user_id"]  

# After: Validated model access
execution_params.user_id  # Runtime validation + IDE support
```

## 🧪 **Comprehensive Test Coverage**

- **Agent mode execution tests** with multi-iteration scenarios
- **Database operation verification** 
- **Type safety validation**
- **Backward compatibility** for traditional mode
- **Enhanced dynamic fields tests**

## 📊 **Usage Examples**

### Traditional Mode (Existing Behavior):
```python
SmartDecisionMakerBlock.Input(
    prompt="Search for keywords",
    agent_mode_max_iterations=0  # Default
)
# → Yields tool calls for external execution
```

### Agent Mode (New Feature):
```python  
SmartDecisionMakerBlock.Input(
    prompt="Complete this task using available tools",
    agent_mode_max_iterations=5  # Max 5 iterations
)
# → Executes tools directly until task completion or iteration limit
```

### Infinite Agent Mode:
```python
SmartDecisionMakerBlock.Input(
    prompt="Analyze and process this data thoroughly", 
    agent_mode_max_iterations=-1  # No limit, run until finished
)
# → Executes tools autonomously until LLM indicates completion
```

##  **Backward Compatibility**

- **Zero breaking changes** to existing functionality
- **Traditional mode remains default** (`agent_mode_max_iterations=0`)
- **All existing tests pass**
- **Same API for tool definitions and execution**

This transforms the SmartDecisionMakerBlock from a simple tool call
generator into a powerful autonomous agent capable of complex multi-step
task execution! 🎯

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Ubbe <hi@ubbe.dev>
Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-21 11:10:05 +01:00
Zamil Majdy
280d92a76f chore: apply prettier formatting to SelectedRunView 2025-12-20 18:42:00 +01:00
Zamil Majdy
e897f55dc3 fix: restore settings functionality to SelectedRunView 2025-12-20 18:35:45 +01:00
Zamil Majdy
e0e3850097 fix: correct SelectedViewLayout prop passing to use agent object 2025-12-20 17:38:02 +01:00
Zamil Majdy
d31e4dba2c chore: apply prettier formatting 2025-12-20 17:32:16 +01:00
Zamil Majdy
a1b3b27d27 feat(frontend): merge marketplace updates with HITL integration
- Integrated marketplace update banners with HITL reviews
- Preserved settings page implementation with breadcrumbs
- Merged templates and triggers functionality from dev
- Kept HITL reviews as first tab when present in ScrollableTabs
- Combined banner prop approach with settings integration
- Fixed SafeModeToggle component imports
- Maintained all HITL functionality in summary section

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-20 17:31:47 +01:00
Lluis Agusti
866e54c39c Merge remote-tracking branch 'origin/dev' into feat/publish-newer-agent-marketplace 2025-12-20 16:09:12 +01:00
Zamil Majdy
79f4706f83 fix(backend): update test to expect include_changelog parameter in get_store_agent_details call 2025-12-20 09:44:19 +01:00
Zamil Majdy
8a384a47d7 fix failing test 2025-12-19 21:00:38 +01:00
Zamil Majdy
884d717743 fix failing test 2025-12-19 20:56:24 +01:00
Zamil Majdy
dc93f214f1 fix(frontend): fix changesSummary rendering in dashboard and admin review
- Fix admin review showing changesSummary in marketplace review table
- Fix edit submission falling back to "Update Submission" hardcoded text
- Add changes_summary prop through AgentTableRow component chain
- Update MainDashboardPage to pass submission.changes_summary data
- Pre-populate changesSummary from existing submission data in modal

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 20:52:33 +01:00
Zamil Majdy
6c00c15383 refactor(frontend): move MarketplaceBanners to contextual components
Moved MarketplaceBanners from app/(platform)/components to src/components/contextual
since it's reused across multiple screens (marketplace and library views).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 20:38:03 +01:00
Zamil Majdy
59528235f3 fix failing test 2025-12-19 20:33:53 +01:00
Zamil Majdy
4350ad95d7 fix(frontend): fix infinite loop in PublishAgentModal and simplify useThumbnailImages
- Changed updateState to setCurrentState in usePublishAgentModal to prevent circular dependency
- Simplified useThumbnailImages by using JSON.stringify for stable dependency tracking instead of manual refs
- Removed duplicate helpers.ts file from MainAgentPage
- All changes preserve functionality while reducing code complexity

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:53:42 +01:00
Zamil Majdy
8769104242 style(frontend): remove unnecessary !important text color overrides
- Remove !important from text-neutral-500 classes where component styling is sufficient
- Fix AgentSelectStep: 2 instances of !text-neutral-500 → text-neutral-500
- Fix AgentReviewStep: 2 instances of !text-neutral-500 → text-neutral-500
- Total: 4 unnecessary CSS overrides removed for cleaner styling
- Components now rely on natural text color inheritance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:18:09 +01:00
Zamil Majdy
22c486583a fix(frontend): replace all 'any' types with proper pre-generated TypeScript types
- Replace PublishState.submissionData: any → StoreSubmission | null
- Replace publishedSubmissionData?: any → StoreSubmission | null in all function signatures
- Replace MyAgent and StoreSubmission parameter types throughout components
- Fix array method parameter types (map, filter, sort, reduce) with proper types
- Remove redundant text-xs class when variant="small" is already set
- Total: ~20 'any' types replaced with safer pre-generated TypeScript types

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:16:00 +01:00
Zamil Majdy
87d4bcd025 fix(frontend): replace remaining status !== 200 checks with okData() helper
- Replace 2 status !== 200 checks in useMarketplaceUpdate.ts with okData()
- Replace 2 status !== 200 checks in usePublishAgentModal.ts with okData()
- Total: 3 additional status checks replaced (now 9 total)
- All manual status checks now use safer okData() pattern

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:08:48 +01:00
Zamil Majdy
c2c7cc94cf fix(frontend): replace status === 200 checks with okData() helper in marketplace files
- Replace 4 manual status checks in useAgentSelectStep.ts with okData() helper
- Replace 1 manual status check in useMarketplaceUpdate.ts with okData() helper
- Replace 1 manual status check in AgentVersionChangelog.tsx with okData() helper
- Total: 6 status === 200 checks replaced with safer okData() pattern
- Add proper TypeScript typing with StoreAgentDetails type
- Maintain backward compatibility while improving code safety

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 18:04:29 +01:00
Zamil Majdy
e6eef0379e feat(frontend): implement real changelog and fix marketplace agent display
- Fix AgentInfo frontend crash by using okData() helper for safe data access
- Implement real changelog functionality with include_changelog API parameter
- Add ChangelogEntry backend model and proper API typing
- Fix version sorting to use Math.max instead of array indexing
- Replace manual status checks with okData() helper throughout
- Create reusable marketplace helper functions in src/components/contextual
- Fix TypeScript errors by replacing 'any' types with proper generated types
- Improve accessibility by making agent cards properly clickable buttons
- Remove unnecessary useMemo and simplify complex helper functions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 17:56:03 +01:00
Zamil Majdy
cc2c183a5f feat(frontend): polish changelog Read more button to match design specs
- Move chevron to left side of text matching reference design
- Update to proper 16x16 icon size with refined stroke styling
- Replace inline styles with proper Tailwind classes
- Use design system colors (text-neutral-900) instead of manual styling
- Add hover states and dark mode support
- Maintain incremental loading (3 versions at a time)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 11:53:58 +01:00
Zamil Majdy
73fcadf0e8 Merge branch 'dev' into feat/publish-newer-agent-marketplace 2025-12-19 11:35:26 +01:00
Zamil Majdy
44d17885ed fix(frontend): fix infinite loop in thumbnail images hook
- Use refs to track previous prop values and prevent infinite re-renders
- Only update images state when initialImages actually changes
- Fix maximum update depth exceeded error in PublishAgentModal
- Ensure thumbnail images load correctly on first modal open

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 11:31:49 +01:00
Zamil Majdy
21652a5422 feat(frontend): fix marketplace banner layout and placement
- Fix banner layout from horizontal to vertical using flex-col
- Move banner to correct breadcrumb location inside SelectedViewLayout
- Remove duplicate breadcrumb from top level in NewAgentLibraryView
- Add marketplace publish functionality to builder actions
- Create unified MarketplaceBanners component for consistency
- Fix apostrophe escaping in banner text for ESLint compliance
- Remove unused isSaving property from PublishToMarketplace hook

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 10:59:56 +01:00
Zamil Majdy
1a8ed4c291 fix(frontend): simplify marketplace version history to clean list
Replace card-based version display with simple list format:
- Single line per version: version number + current badge + description
- Current version highlighted in blue text
- No borders or background styling on items
- Clean, scannable layout with minimal visual noise
- Fixed TypeScript issues with proper data type checking

This provides the same information in a much cleaner, less
overwhelming format that's easier to read at a glance.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 08:51:10 +01:00
Zamil Majdy
c5acb0d4cc feat(frontend): add inline version changelog to marketplace agent pages
Add version history display directly to marketplace agent pages showing
all available versions with visual indicators for the current version.

This provides users with immediate visibility into agent version history
without needing to click a separate button, similar to the library page
changelog functionality but rendered inline.

Features:
- Fetches agentGraphVersions from store API
- Displays versions in descending order (newest first)
- Highlights current version with blue styling and 'Current' badge
- Shows 'Published marketplace version' description for each version
- Gracefully falls back to basic version display if data unavailable
- Maintains existing 'Last updated' information

This enhances marketplace discoverability by making version information
immediately visible to users browsing agent details.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 08:48:08 +01:00
Zamil Majdy
fc0d0903f2 fix(frontend): correct submission version field name
Fix pending submission detection by using 'agent_version' field instead
of 'version' to match the StoreSubmission interface. This ensures
creators don't see duplicate publish update banners when they have
pending submissions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 08:41:20 +01:00
Zamil Majdy
ca53b752d2 feat(platform): add marketplace update notification banner
Add marketplace update notification banner that shows when newer agent versions
are available on the marketplace. This enables non-creator users to see and
update to newer agent versions.

Key features:
- Update banner appears when marketplace has newer agent version
- Version History modal shows all available versions
- Direct update to latest version with explicit graph_version parameter
- Support for both creator and non-creator users
- Added database fields for graph versions and agent graph ID

Frontend changes:
- New useMarketplaceUpdate hook for marketplace version detection
- AgentVersionChangelog component for version history display
- Switch from submissions API to store API for non-creator access
- Clean update mechanism using graph_version parameter

Backend changes:
- Added agentGraphVersions and agentGraphId fields to StoreAgent view
- Updated LibraryAgentUpdateRequest model with graph_version field
- Enhanced store endpoints to expose graph version data
- Fixed all test cases to include new required fields

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-17 21:22:03 +01:00
Zamil Majdy
1d3d60f7ef feat(frontend): add marketplace update notification banner
Add notification banner when user's local agent version is newer than published version.
Allow direct publishing of agent updates via banner button.
Skip agent selection step and pre-populate form when publishing updates.
Fix database view to use correct agent graph version instead of store listing version.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 19:32:00 +01:00
47 changed files with 1610 additions and 302 deletions

View File

@@ -538,6 +538,7 @@ async def update_library_agent(
library_agent_id: str,
user_id: str,
auto_update_version: Optional[bool] = None,
graph_version: Optional[int] = None,
is_favorite: Optional[bool] = None,
is_archived: Optional[bool] = None,
is_deleted: Optional[Literal[False]] = None,
@@ -550,6 +551,7 @@ async def update_library_agent(
library_agent_id: The ID of the LibraryAgent to update.
user_id: The owner of this LibraryAgent.
auto_update_version: Whether the agent should auto-update to active version.
graph_version: Specific graph version to update to.
is_favorite: Whether this agent is marked as a favorite.
is_archived: Whether this agent is archived.
settings: User-specific settings for this library agent.
@@ -563,8 +565,8 @@ async def update_library_agent(
"""
logger.debug(
f"Updating library agent {library_agent_id} for user {user_id} with "
f"auto_update_version={auto_update_version}, is_favorite={is_favorite}, "
f"is_archived={is_archived}, settings={settings}"
f"auto_update_version={auto_update_version}, graph_version={graph_version}, "
f"is_favorite={is_favorite}, is_archived={is_archived}, settings={settings}"
)
update_fields: prisma.types.LibraryAgentUpdateManyMutationInput = {}
if auto_update_version is not None:
@@ -581,10 +583,23 @@ async def update_library_agent(
update_fields["isDeleted"] = is_deleted
if settings is not None:
update_fields["settings"] = SafeJson(settings.model_dump())
if not update_fields:
raise ValueError("No values were passed to update")
try:
# If graph_version is provided, update to that specific version
if graph_version is not None:
# Get the current agent to find its graph_id
agent = await get_library_agent(id=library_agent_id, user_id=user_id)
# Update to the specified version using existing function
return await update_agent_version_in_library(
user_id=user_id,
agent_graph_id=agent.graph_id,
agent_graph_version=graph_version,
)
# Otherwise, just update the simple fields
if not update_fields:
raise ValueError("No values were passed to update")
n_updated = await prisma.models.LibraryAgent.prisma().update_many(
where={"id": library_agent_id, "userId": user_id},
data=update_fields,

View File

@@ -385,6 +385,9 @@ class LibraryAgentUpdateRequest(pydantic.BaseModel):
auto_update_version: Optional[bool] = pydantic.Field(
default=None, description="Auto-update the agent version"
)
graph_version: Optional[int] = pydantic.Field(
default=None, description="Specific graph version to update to"
)
is_favorite: Optional[bool] = pydantic.Field(
default=None, description="Mark the agent as a favorite"
)

View File

@@ -285,6 +285,7 @@ async def update_library_agent(
library_agent_id=library_agent_id,
user_id=user_id,
auto_update_version=payload.auto_update_version,
graph_version=payload.graph_version,
is_favorite=payload.is_favorite,
is_archived=payload.is_archived,
settings=payload.settings,

View File

@@ -43,10 +43,12 @@ async def _get_cached_store_agents(
# Cache individual agent details for 15 minutes
@cached(maxsize=200, ttl_seconds=300, shared_cache=True)
async def _get_cached_agent_details(username: str, agent_name: str):
async def _get_cached_agent_details(
username: str, agent_name: str, include_changelog: bool = False
):
"""Cached helper to get agent details."""
return await store_db.get_store_agent_details(
username=username, agent_name=agent_name
username=username, agent_name=agent_name, include_changelog=include_changelog
)

View File

@@ -257,7 +257,7 @@ async def log_search_term(search_query: str):
async def get_store_agent_details(
username: str, agent_name: str
username: str, agent_name: str, include_changelog: bool = False
) -> store_model.StoreAgentDetails:
"""Get PUBLIC store agent details from the StoreAgent view"""
logger.debug(f"Getting store agent details for {username}/{agent_name}")
@@ -322,6 +322,27 @@ async def get_store_agent_details(
else:
recommended_schedule_cron = None
# Fetch changelog data if requested
changelog_data = None
if include_changelog and store_listing:
changelog_versions = (
await prisma.models.StoreListingVersion.prisma().find_many(
where={
"storeListingId": store_listing.id,
"submissionStatus": prisma.enums.SubmissionStatus.APPROVED,
},
order=[{"version": "desc"}],
)
)
changelog_data = [
backend.server.v2.store.model.ChangelogEntry(
version=str(version.version),
changes_summary=version.changesSummary or "No changes recorded",
date=version.createdAt,
)
for version in changelog_versions
]
logger.debug(f"Found agent details for {username}/{agent_name}")
return store_model.StoreAgentDetails(
store_listing_version_id=agent.storeListingVersionId,
@@ -338,10 +359,13 @@ async def get_store_agent_details(
runs=agent.runs,
rating=agent.rating,
versions=agent.versions,
agentGraphVersions=agent.agentGraphVersions,
agentGraphId=agent.agentGraphId,
last_updated=agent.updated_at,
active_version_id=active_version_id,
has_approved_version=has_approved_version,
recommended_schedule_cron=recommended_schedule_cron,
changelog=changelog_data,
)
except store_exceptions.AgentNotFoundError:
raise
@@ -409,6 +433,8 @@ async def get_store_agent_by_version_id(
runs=agent.runs,
rating=agent.rating,
versions=agent.versions,
agentGraphVersions=agent.agentGraphVersions,
agentGraphId=agent.agentGraphId,
last_updated=agent.updated_at,
)
except store_exceptions.AgentNotFoundError:

View File

@@ -40,6 +40,8 @@ async def test_get_store_agents(mocker):
runs=10,
rating=4.5,
versions=["1.0"],
agentGraphVersions=["1"],
agentGraphId="test-graph-id",
updated_at=datetime.now(),
is_available=False,
useForOnboarding=False,
@@ -83,6 +85,8 @@ async def test_get_store_agent_details(mocker):
runs=10,
rating=4.5,
versions=["1.0"],
agentGraphVersions=["1"],
agentGraphId="test-graph-id",
updated_at=datetime.now(),
is_available=False,
useForOnboarding=False,
@@ -105,6 +109,8 @@ async def test_get_store_agent_details(mocker):
runs=15,
rating=4.8,
versions=["1.0", "2.0"],
agentGraphVersions=["1", "2"],
agentGraphId="test-graph-id-active",
updated_at=datetime.now(),
is_available=True,
useForOnboarding=False,

View File

@@ -7,6 +7,12 @@ import pydantic
from backend.util.models import Pagination
class ChangelogEntry(pydantic.BaseModel):
version: str
changes_summary: str
date: datetime.datetime
class MyAgent(pydantic.BaseModel):
agent_id: str
agent_version: int
@@ -55,12 +61,17 @@ class StoreAgentDetails(pydantic.BaseModel):
runs: int
rating: float
versions: list[str]
agentGraphVersions: list[str]
agentGraphId: str
last_updated: datetime.datetime
recommended_schedule_cron: str | None = None
active_version_id: str | None = None
has_approved_version: bool = False
# Optional changelog data when include_changelog=True
changelog: list[ChangelogEntry] | None = None
class Creator(pydantic.BaseModel):
name: str

View File

@@ -72,6 +72,8 @@ def test_store_agent_details():
runs=50,
rating=4.5,
versions=["1.0", "2.0"],
agentGraphVersions=["1", "2"],
agentGraphId="test-graph-id",
last_updated=datetime.datetime.now(),
)
assert details.slug == "test-agent"

View File

@@ -152,7 +152,11 @@ async def get_agents(
tags=["store", "public"],
response_model=store_model.StoreAgentDetails,
)
async def get_agent(username: str, agent_name: str):
async def get_agent(
username: str,
agent_name: str,
include_changelog: bool = fastapi.Query(default=False),
):
"""
This is only used on the AgentDetails Page.
@@ -162,7 +166,7 @@ async def get_agent(username: str, agent_name: str):
# URL decode the agent name since it comes from the URL path
agent_name = urllib.parse.unquote(agent_name).lower()
agent = await store_cache._get_cached_agent_details(
username=username, agent_name=agent_name
username=username, agent_name=agent_name, include_changelog=include_changelog
)
return agent

View File

@@ -374,6 +374,8 @@ def test_get_agent_details(
runs=100,
rating=4.5,
versions=["1.0.0", "1.1.0"],
agentGraphVersions=["1", "2"],
agentGraphId="test-graph-id",
last_updated=FIXED_NOW,
)
mock_db_call = mocker.patch("backend.api.features.store.db.get_store_agent_details")
@@ -387,7 +389,9 @@ def test_get_agent_details(
assert data.creator == "creator1"
snapshot.snapshot_dir = "snapshots"
snapshot.assert_match(json.dumps(response.json(), indent=2), "agt_details")
mock_db_call.assert_called_once_with(username="creator1", agent_name="test-agent")
mock_db_call.assert_called_once_with(
username="creator1", agent_name="test-agent", include_changelog=False
)
def test_get_creators_defaults(

View File

@@ -442,6 +442,8 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
runs=agent.runs,
rating=agent.rating,
versions=agent.versions,
agentGraphVersions=agent.agentGraphVersions,
agentGraphId=agent.agentGraphId,
last_updated=agent.updated_at,
)
for agent in recommended_agents

View File

@@ -0,0 +1,45 @@
-- Fix StoreSubmission view to use agentGraphVersion instead of version for agent_version field
-- This ensures that submission.agent_version returns the actual agent graph version, not the store listing version number
BEGIN;
-- Recreate the view with the corrected agent_version field (using agentGraphVersion instead of version)
CREATE OR REPLACE VIEW "StoreSubmission" AS
SELECT
sl.id AS listing_id,
sl."owningUserId" AS user_id,
slv."agentGraphId" AS agent_id,
slv."agentGraphVersion" AS agent_version,
sl.slug,
COALESCE(slv.name, '') AS name,
slv."subHeading" AS sub_heading,
slv.description,
slv.instructions,
slv."imageUrls" AS image_urls,
slv."submittedAt" AS date_submitted,
slv."submissionStatus" AS status,
COALESCE(ar.run_count, 0::bigint) AS runs,
COALESCE(avg(sr.score::numeric), 0.0)::double precision AS rating,
slv.id AS store_listing_version_id,
slv."reviewerId" AS reviewer_id,
slv."reviewComments" AS review_comments,
slv."internalComments" AS internal_comments,
slv."reviewedAt" AS reviewed_at,
slv."changesSummary" AS changes_summary,
slv."videoUrl" AS video_url,
slv.categories
FROM "StoreListing" sl
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
LEFT JOIN (
SELECT "AgentGraphExecution"."agentGraphId", count(*) AS run_count
FROM "AgentGraphExecution"
GROUP BY "AgentGraphExecution"."agentGraphId"
) ar ON ar."agentGraphId" = slv."agentGraphId"
WHERE sl."isDeleted" = false
GROUP BY sl.id, sl."owningUserId", slv.id, slv."agentGraphId", slv."agentGraphVersion", sl.slug, slv.name,
slv."subHeading", slv.description, slv.instructions, slv."imageUrls", slv."submittedAt",
slv."submissionStatus", slv."reviewerId", slv."reviewComments", slv."internalComments",
slv."reviewedAt", slv."changesSummary", slv."videoUrl", slv.categories, ar.run_count;
COMMIT;

View File

@@ -0,0 +1,81 @@
-- Add agentGraphVersions field to StoreAgent view for consistent version comparison
-- This keeps the existing versions field unchanged and adds a new field with graph versions
-- This makes it safe for version comparison with LibraryAgent.graph_version
BEGIN;
-- Drop and recreate the StoreAgent view with new agentGraphVersions field
DROP VIEW IF EXISTS "StoreAgent";
CREATE OR REPLACE VIEW "StoreAgent" AS
WITH latest_versions AS (
SELECT
"storeListingId",
MAX(version) AS max_version
FROM "StoreListingVersion"
WHERE "submissionStatus" = 'APPROVED'
GROUP BY "storeListingId"
),
agent_versions AS (
SELECT
"storeListingId",
array_agg(DISTINCT version::text ORDER BY version::text) AS versions
FROM "StoreListingVersion"
WHERE "submissionStatus" = 'APPROVED'
GROUP BY "storeListingId"
),
agent_graph_versions AS (
SELECT
"storeListingId",
array_agg(DISTINCT "agentGraphVersion"::text ORDER BY "agentGraphVersion"::text) AS graph_versions
FROM "StoreListingVersion"
WHERE "submissionStatus" = 'APPROVED'
GROUP BY "storeListingId"
)
SELECT
sl.id AS listing_id,
slv.id AS "storeListingVersionId",
slv."createdAt" AS updated_at,
sl.slug,
COALESCE(slv.name, '') AS agent_name,
slv."videoUrl" AS agent_video,
slv."agentOutputDemoUrl" AS agent_output_demo,
COALESCE(slv."imageUrls", ARRAY[]::text[]) AS agent_image,
slv."isFeatured" AS featured,
p.username AS creator_username, -- Allow NULL for malformed sub-agents
p."avatarUrl" AS creator_avatar, -- Allow NULL for malformed sub-agents
slv."subHeading" AS sub_heading,
slv.description,
slv.categories,
slv.search,
COALESCE(ar.run_count, 0::bigint) AS runs,
COALESCE(rs.avg_rating, 0.0)::double precision AS rating,
COALESCE(av.versions, ARRAY[slv.version::text]) AS versions,
COALESCE(agv.graph_versions, ARRAY[slv."agentGraphVersion"::text]) AS "agentGraphVersions",
slv."agentGraphId",
slv."isAvailable" AS is_available,
COALESCE(sl."useForOnboarding", false) AS "useForOnboarding"
FROM "StoreListing" sl
JOIN latest_versions lv
ON sl.id = lv."storeListingId"
JOIN "StoreListingVersion" slv
ON slv."storeListingId" = lv."storeListingId"
AND slv.version = lv.max_version
AND slv."submissionStatus" = 'APPROVED'
JOIN "AgentGraph" a
ON slv."agentGraphId" = a.id
AND slv."agentGraphVersion" = a.version
LEFT JOIN "Profile" p
ON sl."owningUserId" = p."userId"
LEFT JOIN "mv_review_stats" rs
ON sl.id = rs."storeListingId"
LEFT JOIN "mv_agent_run_counts" ar
ON a.id = ar."agentGraphId"
LEFT JOIN agent_versions av
ON sl.id = av."storeListingId"
LEFT JOIN agent_graph_versions agv
ON sl.id = agv."storeListingId"
WHERE sl."isDeleted" = false
AND sl."hasApprovedVersion" = true;
COMMIT;

View File

@@ -734,11 +734,13 @@ view StoreAgent {
description String
categories String[]
search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
runs Int
rating Float
versions String[]
is_available Boolean @default(true)
useForOnboarding Boolean @default(false)
runs Int
rating Float
versions String[]
agentGraphVersions String[]
agentGraphId String
is_available Boolean @default(true)
useForOnboarding Boolean @default(false)
// Materialized views used (refreshed every 15 minutes via pg_cron):
// - mv_agent_run_counts - Pre-aggregated agent execution counts by agentGraphId

View File

@@ -23,8 +23,14 @@
"1.0.0",
"1.1.0"
],
"agentGraphVersions": [
"1",
"2"
],
"agentGraphId": "test-graph-id",
"last_updated": "2023-01-01T00:00:00",
"recommended_schedule_cron": null,
"active_version_id": null,
"has_approved_version": false
"has_approved_version": false,
"changelog": null
}

View File

@@ -102,7 +102,7 @@ export function ExpandableRow({
<TableRow>
<TableHead>Version</TableHead>
<TableHead>Status</TableHead>
{/* <TableHead>Changes</TableHead> */}
<TableHead>Changes</TableHead>
<TableHead>Submitted</TableHead>
<TableHead>Reviewed</TableHead>
<TableHead>External Comments</TableHead>
@@ -127,9 +127,9 @@ export function ExpandableRow({
)}
</TableCell>
<TableCell>{getStatusBadge(version.status)}</TableCell>
{/* <TableCell>
<TableCell>
{version.changes_summary || "No summary"}
</TableCell> */}
</TableCell>
<TableCell>
{version.date_submitted
? formatDistanceToNow(

View File

@@ -2,6 +2,7 @@ import { parseAsString, useQueryStates } from "nuqs";
import { AgentOutputs } from "./components/AgentOutputs/AgentOutputs";
import { RunGraph } from "./components/RunGraph/RunGraph";
import { ScheduleGraph } from "./components/ScheduleGraph/ScheduleGraph";
import { PublishToMarketplace } from "./components/PublishToMarketplace/PublishToMarketplace";
import { memo } from "react";
export const BuilderActions = memo(() => {
@@ -13,6 +14,7 @@ export const BuilderActions = memo(() => {
<AgentOutputs flowID={flowID} />
<RunGraph flowID={flowID} />
<ScheduleGraph flowID={flowID} />
<PublishToMarketplace flowID={flowID} />
</div>
);
});

View File

@@ -0,0 +1,36 @@
import { ShareIcon } from "@phosphor-icons/react";
import { BuilderActionButton } from "../BuilderActionButton";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { usePublishToMarketplace } from "./usePublishToMarketplace";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
export const PublishToMarketplace = ({ flowID }: { flowID: string | null }) => {
const { handlePublishToMarketplace, publishState, handleStateChange } =
usePublishToMarketplace({ flowID });
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<BuilderActionButton
onClick={handlePublishToMarketplace}
disabled={!flowID}
>
<ShareIcon className="size-6 drop-shadow-sm" />
</BuilderActionButton>
</TooltipTrigger>
<TooltipContent>Publish to Marketplace</TooltipContent>
</Tooltip>
<PublishAgentModal
targetState={publishState}
onStateChange={handleStateChange}
preSelectedAgentId={flowID || undefined}
/>
</>
);
};

View File

@@ -0,0 +1,48 @@
import { useState, useCallback } from "react";
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
export type PublishStep = "select" | "info" | "review";
export type PublishState = {
isOpen: boolean;
step: PublishStep;
submissionData: StoreSubmission | null;
};
const defaultPublishState: PublishState = {
isOpen: false,
step: "select",
submissionData: null,
};
interface UsePublishToMarketplaceProps {
flowID: string | null;
}
export function usePublishToMarketplace({
flowID,
}: UsePublishToMarketplaceProps) {
const [publishState, setPublishState] =
useState<PublishState>(defaultPublishState);
const handlePublishToMarketplace = () => {
if (!flowID) return;
// Open the publish modal starting with the select step
setPublishState({
isOpen: true,
step: "select",
submissionData: null,
});
};
const handleStateChange = useCallback((newState: PublishState) => {
setPublishState(newState);
}, []);
return {
handlePublishToMarketplace,
publishState,
handleStateChange,
};
}

View File

@@ -5,8 +5,13 @@ import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { cn } from "@/lib/utils";
import { PlusIcon } from "@phosphor-icons/react";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
import { MarketplaceBanners } from "@/components/contextual/MarketplaceBanners/MarketplaceBanners";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import { AgentSettingsButton } from "./components/other/AgentSettingsButton";
import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
import { EmptySchedules } from "./components/other/EmptySchedules";
import { EmptyTasks } from "./components/other/EmptyTasks";
@@ -16,9 +21,9 @@ import { SectionWrap } from "./components/other/SectionWrap";
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
import { SelectedSettingsView } from "./components/selected-views/SelectedSettingsView/SelectedSettingsView";
import { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
import { SelectedTriggerView } from "./components/selected-views/SelectedTriggerView/SelectedTriggerView";
import { SelectedSettingsView } from "./components/selected-views/SelectedSettingsView/SelectedSettingsView";
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
@@ -26,6 +31,7 @@ import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
export function NewAgentLibraryView() {
const {
agentId,
agent,
ready,
activeTemplate,
@@ -39,18 +45,79 @@ export function NewAgentLibraryView() {
handleSelectRun,
handleCountsChange,
handleClearSelectedRun,
onRunInitiated,
handleSelectSettings,
onRunInitiated,
onTriggerSetup,
onScheduleCreated,
} = useNewAgentLibraryView();
const {
hasAgentMarketplaceUpdate,
hasMarketplaceUpdate,
latestMarketplaceVersion,
isUpdating,
modalOpen,
setModalOpen,
handlePublishUpdate,
handleUpdateToLatest,
} = useMarketplaceUpdate({ agent });
const [changelogOpen, setChangelogOpen] = useState(false);
useEffect(() => {
if (agent) {
document.title = `${agent.name} - Library - AutoGPT Platform`;
}
}, [agent]);
function renderMarketplaceUpdateBanner() {
return (
<MarketplaceBanners
hasUpdate={!!hasMarketplaceUpdate}
latestVersion={latestMarketplaceVersion}
hasUnpublishedChanges={!!hasAgentMarketplaceUpdate}
currentVersion={agent?.graph_version}
isUpdating={isUpdating}
onUpdate={handleUpdateToLatest}
onPublish={handlePublishUpdate}
onViewChanges={() => setChangelogOpen(true)}
/>
);
}
function renderPublishAgentModal() {
if (!modalOpen || !agent) return null;
return (
<PublishAgentModal
targetState={{
isOpen: true,
step: "info",
submissionData: { isMarketplaceUpdate: true } as any,
}}
preSelectedAgentId={agent.graph_id}
preSelectedAgentVersion={agent.graph_version}
onStateChange={(state) => {
if (!state.isOpen) {
setModalOpen(false);
}
}}
/>
);
}
function renderVersionChangelog() {
if (!agent) return null;
return (
<AgentVersionChangelog
agent={agent}
isOpen={changelogOpen}
onClose={() => setChangelogOpen(false)}
/>
);
}
if (error) {
return (
<ErrorCard
@@ -68,143 +135,160 @@ export function NewAgentLibraryView() {
if (!sidebarLoading && !hasAnyItems) {
return (
<div className="flex h-full flex-col">
<div className="mx-6 pt-4">
<div className="relative flex items-center gap-2">
<>
<div className="flex h-full flex-col">
<div className="mx-6 pt-4">
<Breadcrumbs
items={[
{ name: "My Library", link: "/library" },
{ name: agent.name },
{ name: agent.name, link: `/library/agents/${agentId}` },
]}
/>
</div>
<div className="flex min-h-0 flex-1">
<EmptyTasks
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</div>
</div>
<div className="flex min-h-0 flex-1">
<EmptyTasks
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</div>
</div>
{renderPublishAgentModal()}
{renderVersionChangelog()}
</>
);
}
return (
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
<SectionWrap className="mb-3 block">
<div
className={cn(
"border-b border-zinc-100 pb-5",
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<RunAgentModal
triggerSlot={
<Button
variant="primary"
size="large"
className="w-full"
disabled={isTemplateLoading && activeTab === "templates"}
>
<PlusIcon size={20} /> New task
</Button>
}
agent={agent}
onRunCreated={onRunInitiated}
onScheduleCreated={onScheduleCreated}
onTriggerSetup={onTriggerSetup}
initialInputValues={activeTemplate?.inputs}
initialInputCredentials={activeTemplate?.credentials}
/>
</div>
<>
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
<SectionWrap className="mb-3 block">
<div
className={cn(
"border-b border-zinc-100 pb-5",
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<div className="flex items-center gap-2">
<RunAgentModal
triggerSlot={
<Button
variant="primary"
size="large"
className="flex-1"
disabled={isTemplateLoading && activeTab === "templates"}
>
<PlusIcon size={20} /> New task
</Button>
}
agent={agent}
onRunCreated={onRunInitiated}
onScheduleCreated={onScheduleCreated}
onTriggerSetup={onTriggerSetup}
initialInputValues={activeTemplate?.inputs}
initialInputCredentials={activeTemplate?.credentials}
/>
<AgentSettingsButton
agent={agent}
onSelectSettings={handleSelectSettings}
selected={activeItem === "settings"}
/>
</div>
</div>
<SidebarRunsList
agent={agent}
selectedRunId={activeItem ?? undefined}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
onTabChange={setActiveTab}
onCountsChange={handleCountsChange}
/>
</SectionWrap>
{activeItem ? (
activeItem === "settings" ? (
<SelectedSettingsView
<SidebarRunsList
agent={agent}
onClearSelectedRun={handleClearSelectedRun}
/>
) : activeTab === "scheduled" ? (
<SelectedScheduleView
agent={agent}
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
/>
) : activeTab === "templates" ? (
<SelectedTemplateView
agent={agent}
templateId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onRunCreated={(execution) => handleSelectRun(execution.id, "runs")}
onSwitchToRunsTab={() => setActiveTab("runs")}
/>
) : activeTab === "triggers" ? (
<SelectedTriggerView
agent={agent}
triggerId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onSwitchToRunsTab={() => setActiveTab("runs")}
/>
) : (
<SelectedRunView
agent={agent}
runId={activeItem}
selectedRunId={activeItem ?? undefined}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
onSelectSettings={handleSelectSettings}
onTabChange={setActiveTab}
onCountsChange={handleCountsChange}
/>
)
) : sidebarLoading ? (
<LoadingSelectedContent
agent={agent}
onSelectSettings={handleSelectSettings}
/>
) : activeTab === "scheduled" ? (
<SelectedViewLayout
agent={agent}
onSelectSettings={handleSelectSettings}
>
<EmptySchedules />
</SelectedViewLayout>
) : activeTab === "templates" ? (
<SelectedViewLayout
agent={agent}
onSelectSettings={handleSelectSettings}
>
<EmptyTemplates />
</SelectedViewLayout>
) : activeTab === "triggers" ? (
<SelectedViewLayout
agent={agent}
onSelectSettings={handleSelectSettings}
>
<EmptyTriggers />
</SelectedViewLayout>
) : (
<SelectedViewLayout
agent={agent}
onSelectSettings={handleSelectSettings}
>
<EmptyTasks
</SectionWrap>
{activeItem ? (
activeItem === "settings" ? (
<SelectedSettingsView
agent={agent}
onClearSelectedRun={handleClearSelectedRun}
/>
) : activeTab === "scheduled" ? (
<SelectedScheduleView
agent={agent}
scheduleId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
banner={renderMarketplaceUpdateBanner()}
/>
) : activeTab === "templates" ? (
<SelectedTemplateView
agent={agent}
templateId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onRunCreated={(execution) =>
handleSelectRun(execution.id, "runs")
}
onSwitchToRunsTab={() => setActiveTab("runs")}
banner={renderMarketplaceUpdateBanner()}
/>
) : activeTab === "triggers" ? (
<SelectedTriggerView
agent={agent}
triggerId={activeItem}
onClearSelectedRun={handleClearSelectedRun}
onSwitchToRunsTab={() => setActiveTab("runs")}
banner={renderMarketplaceUpdateBanner()}
/>
) : (
<SelectedRunView
agent={agent}
runId={activeItem}
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
banner={renderMarketplaceUpdateBanner()}
onSelectSettings={handleSelectSettings}
selectedSettings={activeItem === "settings"}
/>
)
) : sidebarLoading ? (
<LoadingSelectedContent agent={agent} />
) : activeTab === "scheduled" ? (
<SelectedViewLayout
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</SelectedViewLayout>
)}
</div>
banner={renderMarketplaceUpdateBanner()}
>
<EmptySchedules />
</SelectedViewLayout>
) : activeTab === "templates" ? (
<SelectedViewLayout
agent={agent}
banner={renderMarketplaceUpdateBanner()}
>
<EmptyTemplates />
</SelectedViewLayout>
) : activeTab === "triggers" ? (
<SelectedViewLayout
agent={agent}
banner={renderMarketplaceUpdateBanner()}
>
<EmptyTriggers />
</SelectedViewLayout>
) : (
<SelectedViewLayout
agent={agent}
banner={renderMarketplaceUpdateBanner()}
>
<EmptyTasks
agent={agent}
onRun={onRunInitiated}
onTriggerSetup={onTriggerSetup}
onScheduleCreated={onScheduleCreated}
/>
</SelectedViewLayout>
)}
</div>
{renderPublishAgentModal()}
{renderVersionChangelog()}
</>
);
}

View File

@@ -0,0 +1,137 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers";
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import React from "react";
interface AgentVersionChangelogProps {
agent: LibraryAgent;
isOpen: boolean;
onClose: () => void;
}
interface VersionInfo {
version: number;
isCurrentVersion: boolean;
}
export function AgentVersionChangelog({
agent,
isOpen,
onClose,
}: AgentVersionChangelogProps) {
// Get marketplace data if agent has marketplace listing
const { data: storeAgentData, isLoading } = useGetV2GetSpecificAgent(
agent?.marketplace_listing?.creator.slug || "",
agent?.marketplace_listing?.slug || "",
{},
{
query: {
enabled: !!(
agent?.marketplace_listing?.creator.slug &&
agent?.marketplace_listing?.slug
),
},
},
);
// Create version info from available graph versions
const storeData = okData<StoreAgentDetails>(storeAgentData);
const agentVersions: VersionInfo[] = storeData?.agentGraphVersions
? storeData.agentGraphVersions
.map((versionStr: string) => parseInt(versionStr, 10))
.sort((a: number, b: number) => b - a) // Sort descending (newest first)
.map((version: number) => ({
version,
isCurrentVersion: version === agent.graph_version,
}))
: [];
const renderVersionItem = (versionInfo: VersionInfo) => {
return (
<div
key={versionInfo.version}
className={`rounded-lg border p-4 ${
versionInfo.isCurrentVersion
? "border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950"
: "border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Text variant="body" className="font-semibold">
v{versionInfo.version}
</Text>
{versionInfo.isCurrentVersion && (
<span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-100">
Current
</span>
)}
</div>
</div>
<Text
variant="small"
className="mt-1 text-neutral-600 dark:text-neutral-400"
>
Available marketplace version
</Text>
</div>
);
};
return (
<Dialog
title={`Version History - ${agent.name}`}
styling={{
maxWidth: "45rem",
}}
controlled={{
isOpen: isOpen,
set: (isOpen) => {
if (!isOpen) {
onClose();
}
},
}}
>
<Dialog.Content>
<div className="max-h-[70vh] overflow-y-auto">
{isLoading ? (
<div className="space-y-4">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
<Skeleton className="h-20 w-full" />
</div>
) : agentVersions.length > 0 ? (
<div className="space-y-4">
<Text
variant="small"
className="text-neutral-600 dark:text-neutral-400"
>
View changes and updates across different versions of this
agent.
</Text>
{agentVersions.map(renderVersionItem)}
</div>
) : (
<div className="py-8 text-center">
<Text
variant="body"
className="text-neutral-600 dark:text-neutral-400"
>
No version history available for this agent.
</Text>
</div>
)}
</div>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -6,9 +6,14 @@ import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
interface Props {
agent: LibraryAgent;
onSelectSettings: () => void;
selected?: boolean;
}
export function AgentSettingsButton({ agent, onSelectSettings }: Props) {
export function AgentSettingsButton({
agent,
onSelectSettings,
selected,
}: Props) {
const { hasHITLBlocks } = useAgentSafeMode(agent);
if (!hasHITLBlocks) {
@@ -17,13 +22,16 @@ export function AgentSettingsButton({ agent, onSelectSettings }: Props) {
return (
<Button
variant="ghost"
variant={selected ? "secondary" : "ghost"}
size="small"
className="m-0 min-w-0 rounded-full p-0 px-1"
onClick={onSelectSettings}
aria-label="Agent Settings"
>
<GearIcon size={18} className="text-zinc-600" />
<GearIcon
size={18}
className={selected ? "text-zinc-900" : "text-zinc-600"}
/>
</Button>
);
}

View File

@@ -32,6 +32,7 @@ interface Props {
runId: string;
onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void;
banner?: React.ReactNode;
onSelectSettings?: () => void;
selectedSettings?: boolean;
}
@@ -41,7 +42,9 @@ export function SelectedRunView({
runId,
onSelectRun,
onClearSelectedRun,
banner,
onSelectSettings,
selectedSettings,
}: Props) {
const { run, preset, isLoading, responseError, httpError } =
useSelectedRunView(agent.graph_id, runId);
@@ -81,7 +84,12 @@ export function SelectedRunView({
return (
<div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agent={agent} onSelectSettings={onSelectSettings}>
<SelectedViewLayout
agent={agent}
banner={banner}
onSelectSettings={onSelectSettings}
selectedSettings={selectedSettings}
>
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={run} />
@@ -105,7 +113,7 @@ export function SelectedRunView({
)}
<ScrollableTabs
defaultValue="output"
defaultValue={withReviews ? "reviews" : "output"}
className="-mt-2 flex flex-col"
>
<ScrollableTabsList className="px-4">
@@ -130,20 +138,22 @@ export function SelectedRunView({
{/* Human-in-the-Loop Reviews Section */}
{withReviews && (
<ScrollableTabsContent value="reviews">
<div id="reviews" className="scroll-mt-4 px-4">
{reviewsLoading ? (
<LoadingSpinner size="small" />
) : pendingReviews.length > 0 ? (
<PendingReviewsList
reviews={pendingReviews}
onReviewComplete={refetchReviews}
emptyMessage="No pending reviews for this execution"
/>
) : (
<Text variant="body" className="text-zinc-600">
No pending reviews for this execution
</Text>
)}
<div className="scroll-mt-4">
<RunDetailCard>
{reviewsLoading ? (
<LoadingSpinner size="small" />
) : pendingReviews.length > 0 ? (
<PendingReviewsList
reviews={pendingReviews}
onReviewComplete={refetchReviews}
emptyMessage="No pending reviews for this execution"
/>
) : (
<Text variant="body" className="text-zinc-700">
No pending reviews for this execution
</Text>
)}
</RunDetailCard>
</div>
</ScrollableTabsContent>
)}

View File

@@ -15,7 +15,6 @@ import { SelectedActionsWrap } from "../../../SelectedActionsWrap";
import { ShareRunButton } from "../../../ShareRunButton/ShareRunButton";
import { CreateTemplateModal } from "../CreateTemplateModal/CreateTemplateModal";
import { useSelectedRunActions } from "./useSelectedRunActions";
import { SafeModeToggle } from "../SafeModeToggle";
type Props = {
agent: LibraryAgent;
@@ -113,7 +112,6 @@ export function SelectedRunActions({
shareToken={run.share_token}
/>
)}
<SafeModeToggle graph={agent} fullWidth={false} />
{canRunManually && (
<>
<Button

View File

@@ -20,6 +20,7 @@ interface Props {
agent: LibraryAgent;
scheduleId: string;
onClearSelectedRun?: () => void;
banner?: React.ReactNode;
onSelectSettings?: () => void;
selectedSettings?: boolean;
}
@@ -28,6 +29,7 @@ export function SelectedScheduleView({
agent,
scheduleId,
onClearSelectedRun,
banner,
onSelectSettings,
selectedSettings,
}: Props) {
@@ -76,6 +78,7 @@ export function SelectedScheduleView({
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout
agent={agent}
banner={banner}
onSelectSettings={onSelectSettings}
selectedSettings={selectedSettings}
>

View File

@@ -24,6 +24,7 @@ interface Props {
onClearSelectedRun?: () => void;
onRunCreated?: (execution: GraphExecutionMeta) => void;
onSwitchToRunsTab?: () => void;
banner?: React.ReactNode;
}
export function SelectedTemplateView({
@@ -32,6 +33,7 @@ export function SelectedTemplateView({
onClearSelectedRun,
onRunCreated,
onSwitchToRunsTab,
banner,
}: Props) {
const {
template,
@@ -100,7 +102,7 @@ export function SelectedTemplateView({
return (
<div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agent={agent}>
<SelectedViewLayout agent={agent} banner={banner}>
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={undefined} />

View File

@@ -22,6 +22,7 @@ interface Props {
triggerId: string;
onClearSelectedRun?: () => void;
onSwitchToRunsTab?: () => void;
banner?: React.ReactNode;
}
export function SelectedTriggerView({
@@ -29,6 +30,7 @@ export function SelectedTriggerView({
triggerId,
onClearSelectedRun,
onSwitchToRunsTab,
banner,
}: Props) {
const {
trigger,
@@ -93,7 +95,7 @@ export function SelectedTriggerView({
return (
<div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agent={agent}>
<SelectedViewLayout agent={agent} banner={banner}>
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={undefined} />

View File

@@ -7,6 +7,7 @@ import { SectionWrap } from "../other/SectionWrap";
interface Props {
agent: LibraryAgent;
children: React.ReactNode;
banner?: React.ReactNode;
additionalBreadcrumb?: { name: string; link?: string };
onSelectSettings?: () => void;
selectedSettings?: boolean;
@@ -18,13 +19,16 @@ export function SelectedViewLayout(props: Props) {
<div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
>
{props.banner && <div className="mb-4">{props.banner}</div>}
<div className="relative flex w-fit items-center gap-2">
<Breadcrumbs
items={[
{ name: "My Library", link: "/library" },
{
name: props.agent.name,
link: `/library/agents/${props.agent.id}`,
},
...(props.additionalBreadcrumb ? [props.additionalBreadcrumb] : []),
]}
/>
{props.agent && props.onSelectSettings && (
@@ -32,6 +36,7 @@ export function SelectedViewLayout(props: Props) {
<AgentSettingsButton
agent={props.agent}
onSelectSettings={props.onSelectSettings}
selected={props.selectedSettings}
/>
</div>
)}

View File

@@ -0,0 +1,163 @@
import {
useGetV2GetSpecificAgent,
useGetV2ListMySubmissions,
} from "@/app/api/__generated__/endpoints/store/store";
import {
usePatchV2UpdateLibraryAgent,
getGetV2GetLibraryAgentQueryKey,
} from "@/app/api/__generated__/endpoints/library/library";
import { useToast } from "@/components/molecules/Toast/use-toast";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useQueryClient } from "@tanstack/react-query";
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
import { okData } from "@/app/api/helpers";
import type { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import * as React from "react";
import { useState } from "react";
interface UseMarketplaceUpdateProps {
agent: LibraryAgent | null | undefined;
}
export function useMarketplaceUpdate({ agent }: UseMarketplaceUpdateProps) {
const [modalOpen, setModalOpen] = useState(false);
const { toast } = useToast();
const queryClient = useQueryClient();
const user = useSupabaseStore((state) => state.user);
// Get marketplace data if agent has marketplace listing
const { data: storeAgentData } = useGetV2GetSpecificAgent(
agent?.marketplace_listing?.creator.slug || "",
agent?.marketplace_listing?.slug || "",
{},
{
query: {
enabled: !!(
agent?.marketplace_listing?.creator.slug &&
agent?.marketplace_listing?.slug
),
},
},
);
// Get user's submissions to check for pending submissions
const { data: submissionsData } = useGetV2ListMySubmissions(
{ page: 1, page_size: 50 }, // Get enough to cover recent submissions
{
query: {
enabled: !!user?.id, // Only fetch if user is authenticated
},
},
);
const updateToLatestMutation = usePatchV2UpdateLibraryAgent({
mutation: {
onError: (err) => {
toast({
title: "Update Failed",
description: "Failed to update agent to latest version",
variant: "destructive",
});
console.error("Failed to update agent:", err);
},
onSuccess: () => {
toast({
title: "Agent Updated",
description: "Agent updated to latest version successfully",
});
// Invalidate to get the updated agent data from the server
if (agent?.id) {
queryClient.invalidateQueries({
queryKey: getGetV2GetLibraryAgentQueryKey(agent.id),
});
}
},
},
});
// Check if marketplace has a newer version than user's current version
const marketplaceUpdateInfo = React.useMemo(() => {
const storeAgent = okData(storeAgentData) as any;
if (!agent || !storeAgent) {
return {
hasUpdate: false,
latestVersion: undefined,
isUserCreator: false,
};
}
// Get the latest version from the marketplace
// agentGraphVersions array contains graph version numbers as strings, get the highest one
const latestMarketplaceVersion =
storeAgent.agentGraphVersions?.length > 0
? Math.max(
...storeAgent.agentGraphVersions.map((v: string) =>
parseInt(v, 10),
),
)
: undefined;
// Determine if the user is the creator of this agent
// Compare current user ID with the marketplace listing creator ID
const isUserCreator =
user?.id && agent.marketplace_listing?.creator.id === user.id;
// Check if there's a pending submission for this specific agent version
const submissionsResponse = okData(submissionsData) as any;
const hasPendingSubmissionForCurrentVersion =
isUserCreator &&
submissionsResponse?.submissions?.some(
(submission: StoreSubmission) =>
submission.agent_id === agent.graph_id &&
submission.agent_version === agent.graph_version &&
submission.status === "PENDING",
);
// If user is creator and their version is newer than marketplace, show publish update banner
// BUT only if there's no pending submission for this version
const hasPublishUpdate =
isUserCreator &&
!hasPendingSubmissionForCurrentVersion &&
latestMarketplaceVersion !== undefined &&
agent.graph_version > latestMarketplaceVersion;
// If marketplace version is newer than user's version, show update banner
// This applies to both creators and non-creators
const hasMarketplaceUpdate =
latestMarketplaceVersion !== undefined &&
latestMarketplaceVersion > agent.graph_version;
return {
hasUpdate: hasMarketplaceUpdate,
latestVersion: latestMarketplaceVersion,
isUserCreator,
hasPublishUpdate,
};
}, [agent, storeAgentData, user, submissionsData]);
const handlePublishUpdate = () => {
setModalOpen(true);
};
const handleUpdateToLatest = () => {
if (!agent || marketplaceUpdateInfo.latestVersion === undefined) return;
// Update to the specific marketplace version using the new graph_version parameter
updateToLatestMutation.mutate({
libraryAgentId: agent.id,
data: {
graph_version: marketplaceUpdateInfo.latestVersion,
},
});
};
return {
hasAgentMarketplaceUpdate: marketplaceUpdateInfo.hasPublishUpdate,
hasMarketplaceUpdate: marketplaceUpdateInfo.hasUpdate,
latestMarketplaceVersion: marketplaceUpdateInfo.latestVersion,
isUpdating: updateToLatestMutation.isPending,
modalOpen,
setModalOpen,
handlePublishUpdate,
handleUpdateToLatest,
};
}

View File

@@ -85,7 +85,10 @@ export function useNewAgentLibraryView() {
);
// Show sidebar layout while loading or when there are items or settings is selected
const showSidebarLayout = useEffect(() => {
const showSidebarLayout =
sidebarLoading || hasAnyItems || activeItem === "settings";
useEffect(() => {
if (agent) {
document.title = `${agent.name} - Library - AutoGPT Platform`;
}
@@ -204,8 +207,8 @@ export function useNewAgentLibraryView() {
handleClearSelectedRun,
handleCountsChange,
handleSelectRun,
onRunInitiated,
handleSelectSettings,
onRunInitiated,
onTriggerSetup,
onScheduleCreated,
};

View File

@@ -5,7 +5,13 @@ import { Separator } from "@/components/__legacy__/ui/separator";
import Link from "next/link";
import { User } from "@supabase/supabase-js";
import { cn } from "@/lib/utils";
import { okData } from "@/app/api/helpers";
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import type { ChangelogEntry } from "@/app/api/__generated__/models/changelogEntry";
import { useAgentInfo } from "./useAgentInfo";
import { useGetV2GetSpecificAgent } from "@/app/api/__generated__/endpoints/store/store";
import { Text } from "@/components/atoms/Text/Text";
import * as React from "react";
interface AgentInfoProps {
user: User | null;
@@ -21,6 +27,8 @@ interface AgentInfoProps {
version: string;
storeListingVersionId: string;
isAgentAddedToLibrary: boolean;
creatorSlug?: string;
agentSlug?: string;
}
export const AgentInfo = ({
@@ -37,6 +45,8 @@ export const AgentInfo = ({
version,
storeListingVersionId,
isAgentAddedToLibrary,
creatorSlug,
agentSlug,
}: AgentInfoProps) => {
const {
handleDownload,
@@ -45,6 +55,106 @@ export const AgentInfo = ({
isAddingAgentToLibrary,
} = useAgentInfo({ storeListingVersionId });
// State for expanding version list - start with 3, then show 3 more each time
const [visibleVersionCount, setVisibleVersionCount] = React.useState(3);
// Get store agent data for version history
const { data: storeAgentData } = useGetV2GetSpecificAgent(
creatorSlug || "",
agentSlug || "",
{ include_changelog: true },
{
query: {
enabled: !!(creatorSlug && agentSlug),
},
},
);
// Calculate update information using simple helper functions
const storeData = okData<StoreAgentDetails>(storeAgentData);
// Process version data for display - use store listing versions (not agentGraphVersions)
const allVersions = storeData?.versions
? storeData.versions
.map((versionStr: string) => parseInt(versionStr, 10))
.sort((a: number, b: number) => b - a)
.map((versionNum: number) => ({
version: versionNum,
isCurrentVersion: false, // We'll update this logic if needed
}))
: [];
const agentVersions = allVersions.slice(0, visibleVersionCount);
const hasMoreVersions = allVersions.length > visibleVersionCount;
const formatDate = (version: number) => {
// Generate sample dates based on version
const baseDate = new Date("2025-12-18");
baseDate.setDate(baseDate.getDate() - (19 - version) * 7);
return baseDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const renderVersionItem = (versionInfo: {
version: number;
isCurrentVersion: boolean;
}) => {
// Find real changelog data for this version
const storeData = okData<StoreAgentDetails>(storeAgentData);
const changelogEntry = storeData?.changelog?.find(
(entry: ChangelogEntry) =>
entry.version === versionInfo.version.toString(),
);
return (
<div key={versionInfo.version} className="mb-6 last:mb-0">
{/* Version Header */}
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<Text
variant="body"
className="font-semibold text-neutral-900 dark:text-neutral-100"
>
Version {versionInfo.version}.0
</Text>
{versionInfo.isCurrentVersion && (
<span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-100">
Current
</span>
)}
</div>
<Text
variant="small"
className="text-neutral-500 dark:text-neutral-400"
>
{changelogEntry
? new Date(changelogEntry.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: formatDate(versionInfo.version)}
</Text>
</div>
{/* Real Changelog Content */}
{changelogEntry && (
<div className="space-y-2">
<Text
variant="body"
className="text-neutral-700 dark:text-neutral-300"
>
{changelogEntry.changes_summary}
</Text>
</div>
)}
</div>
);
};
return (
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
{/* Title */}
@@ -158,17 +268,48 @@ export const AgentInfo = ({
</div>
</div>
{/* Version History */}
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
{/* Changelog */}
<div className="flex w-full flex-col gap-1.5 sm:gap-2">
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
Version history
Changelog
</div>
<div className="decoration-skip-ink-none text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
Last updated {lastUpdated}
</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
Version {version}
</div>
{/* Version List */}
{agentVersions.length > 0 ? (
<div className="mt-4">
{agentVersions.map(renderVersionItem)}
{hasMoreVersions && (
<button
onClick={() => setVisibleVersionCount((prev) => prev + 3)}
className="mt-2 flex items-center gap-1 text-sm font-medium text-neutral-900 hover:text-neutral-700 dark:text-neutral-100 dark:hover:text-neutral-300"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
>
<path
d="M4 6l4 4 4-4"
stroke="currentColor"
strokeWidth="1.5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>Read more</span>
</button>
)}
</div>
) : (
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
Version {version}
</div>
)}
</div>
</div>
</div>

View File

@@ -2,6 +2,8 @@
import { Separator } from "@/components/__legacy__/ui/separator";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { okData } from "@/app/api/helpers";
import type { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import { MarketplaceAgentPageParams } from "../../agent/[creator]/[slug]/page";
import { AgentImages } from "../AgentImages/AgentImage";
import { AgentInfo } from "../AgentInfo/AgentInfo";
@@ -10,34 +12,22 @@ import { AgentsSection } from "../AgentsSection/AgentsSection";
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
import { useMainAgentPage } from "./useMainAgentPage";
type MainAgentPageProps = {
interface Props {
params: MarketplaceAgentPageParams;
};
}
export const MainAgentPage = ({ params }: MainAgentPageProps) => {
const {
agent,
otherAgents,
similarAgents,
libraryAgent,
isLoading,
hasError,
user,
} = useMainAgentPage({ params });
export function MainAgentPage({ params }: Props) {
const { agent, user, isLoading, similarAgents, otherAgents, libraryAgent } =
useMainAgentPage(params);
if (isLoading) {
return <AgentPageLoading />;
}
if (hasError) {
return (
<div className="mx-auto w-full max-w-[1360px]">
<main className="px-4">
<div className="flex min-h-[400px] items-center justify-center">
<ErrorCard
isSuccess={false}
responseError={{ message: "Failed to load agent data" }}
context="agent page"
onRetry={() => window.location.reload()}
<div className="flex h-[600px] items-center justify-center">
<AgentPageLoading
message="Discovering agent..."
submessage="Loading agent details"
className="w-full max-w-md"
/>
</div>
@@ -46,7 +36,8 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
);
}
if (!agent) {
const agentData = okData<StoreAgentDetails>(agent);
if (!agentData) {
return (
<div className="mx-auto w-full max-w-[1360px]">
<main className="px-4">
@@ -55,8 +46,6 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
isSuccess={false}
responseError={{ message: "Agent not found" }}
context="agent page"
onRetry={() => window.location.reload()}
className="w-full max-w-md"
/>
</div>
</main>
@@ -67,10 +56,10 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
const breadcrumbs = [
{ name: "Marketplace", link: "/marketplace" },
{
name: agent.creator,
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`,
name: agentData.creator ?? "",
link: `/marketplace/creator/${encodeURIComponent(agentData.creator ?? "")}`,
},
{ name: agent.agent_name, link: "#" },
{ name: agentData.agent_name ?? "", link: "#" },
];
return (
@@ -82,18 +71,29 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
<div className="w-full md:w-auto md:shrink-0">
<AgentInfo
user={user}
agentId={agent.active_version_id ?? ""}
name={agent.agent_name}
creator={agent.creator}
shortDescription={agent.sub_heading}
longDescription={agent.description}
rating={agent.rating}
runs={agent.runs}
categories={agent.categories}
lastUpdated={agent.last_updated.toISOString()}
version={agent.versions[agent.versions.length - 1]}
storeListingVersionId={agent.store_listing_version_id}
agentId={agentData.active_version_id ?? ""}
name={agentData.agent_name ?? ""}
creator={agentData.creator ?? ""}
shortDescription={agentData.sub_heading ?? ""}
longDescription={agentData.description ?? ""}
rating={agentData.rating ?? 0}
runs={agentData.runs ?? 0}
categories={agentData.categories ?? []}
lastUpdated={
agentData.last_updated?.toISOString() ??
new Date().toISOString()
}
version={
agentData.versions
? Math.max(
...agentData.versions.map((v: string) => parseInt(v, 10)),
).toString()
: "1"
}
storeListingVersionId={agentData.store_listing_version_id ?? ""}
isAgentAddedToLibrary={Boolean(libraryAgent)}
creatorSlug={params.creator}
agentSlug={params.slug}
/>
</div>
<AgentImages
@@ -101,23 +101,23 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
const orderedImages: string[] = [];
// 1. YouTube/Overview video (if it exists)
if (agent.agent_video) {
orderedImages.push(agent.agent_video);
if (agentData.agent_video) {
orderedImages.push(agentData.agent_video);
}
// 2. First image (hero)
if (agent.agent_image.length > 0) {
orderedImages.push(agent.agent_image[0]);
if (agentData.agent_image?.length > 0) {
orderedImages.push(agentData.agent_image[0]);
}
// 3. Agent Output Demo (if it exists)
if ((agent as any).agent_output_demo) {
orderedImages.push((agent as any).agent_output_demo);
if (agentData.agent_output_demo) {
orderedImages.push(agentData.agent_output_demo);
}
// 4. Additional images
if (agent.agent_image.length > 1) {
orderedImages.push(...agent.agent_image.slice(1));
if (agentData.agent_image && agentData.agent_image.length > 1) {
orderedImages.push(...agentData.agent_image.slice(1));
}
return orderedImages;
@@ -129,7 +129,7 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
<AgentsSection
margin="32px"
agents={otherAgents.agents}
sectionTitle={`Other agents by ${agent.creator}`}
sectionTitle={`Other agents by ${agentData.creator ?? ""}`}
/>
)}
<Separator className="mb-[25px] mt-[60px]" />
@@ -140,13 +140,8 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
sectionTitle="Similar agents"
/>
)}
<Separator className="mb-[25px] mt-[60px]" />
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"
buttonText="Become a Creator"
/>
<BecomeACreator />
</main>
</div>
);
};
}

View File

@@ -7,6 +7,7 @@ import { useGetV2GetAgentByStoreId } from "@/app/api/__generated__/endpoints/lib
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { okData } from "@/app/api/helpers";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
export const useMainAgentPage = ({
@@ -20,13 +21,7 @@ export const useMainAgentPage = ({
data: agent,
isLoading: isAgentLoading,
isError: isAgentError,
} = useGetV2GetSpecificAgent(creator_lower, params.slug, {
query: {
select: (x) => {
return x.data as StoreAgentDetails;
},
},
});
} = useGetV2GetSpecificAgent(creator_lower, params.slug);
const {
data: otherAgents,
isLoading: isOtherAgentsLoading,
@@ -59,14 +54,18 @@ export const useMainAgentPage = ({
data: libraryAgent,
isLoading: isLibraryAgentLoading,
isError: isLibraryAgentError,
} = useGetV2GetAgentByStoreId(agent?.active_version_id ?? "", {
query: {
select: (x) => {
return x.data as LibraryAgent;
} = useGetV2GetAgentByStoreId(
okData<StoreAgentDetails>(agent)?.active_version_id ?? "",
{
query: {
select: (x) => {
return x.data as LibraryAgent;
},
enabled:
!!user && !!okData<StoreAgentDetails>(agent)?.active_version_id,
},
enabled: !!user && !!agent?.active_version_id,
},
});
);
const isLoading =
isAgentLoading ||

View File

@@ -33,6 +33,7 @@ export interface AgentTableRowProps {
video_url?: string;
categories?: string[];
store_listing_version_id?: string;
changes_summary?: string;
onViewSubmission: (submission: StoreSubmission) => void;
onDeleteSubmission: (submission_id: string) => void;
onEditSubmission: (
@@ -58,6 +59,7 @@ export const AgentTableRow = ({
video_url,
categories,
store_listing_version_id,
changes_summary,
onViewSubmission,
onDeleteSubmission,
onEditSubmission,
@@ -80,6 +82,7 @@ export const AgentTableRow = ({
video_url,
categories,
store_listing_version_id,
changes_summary,
});
// Determine if we should show Edit or View button

View File

@@ -25,6 +25,7 @@ interface useAgentTableRowProps {
video_url?: string;
categories?: string[];
store_listing_version_id?: string;
changes_summary?: string;
}
export const useAgentTableRow = ({
@@ -44,6 +45,7 @@ export const useAgentTableRow = ({
video_url,
categories,
store_listing_version_id,
changes_summary,
}: useAgentTableRowProps) => {
const handleView = () => {
onViewSubmission({
@@ -72,7 +74,7 @@ export const useAgentTableRow = ({
image_urls: imageSrc,
video_url,
categories,
changes_summary: "Update Submission",
changes_summary: changes_summary || "Update Submission",
store_listing_version_id,
agent_id,
});

View File

@@ -98,6 +98,7 @@ export const MainDashboardPage = () => {
slug: submission.slug,
store_listing_version_id:
submission.store_listing_version_id || undefined,
changes_summary: submission.changes_summary || undefined,
}))}
onViewSubmission={onViewSubmission}
onDeleteSubmission={onDeleteSubmission}

View File

@@ -5113,6 +5113,16 @@
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Agent Name" }
},
{
"name": "include_changelog",
"in": "query",
"required": false,
"schema": {
"type": "boolean",
"default": false,
"title": "Include Changelog"
}
}
],
"responses": {
@@ -6510,6 +6520,16 @@
"required": ["file"],
"title": "Body_postV2Upload submission media"
},
"ChangelogEntry": {
"properties": {
"version": { "type": "string", "title": "Version" },
"changes_summary": { "type": "string", "title": "Changes Summary" },
"date": { "type": "string", "format": "date-time", "title": "Date" }
},
"type": "object",
"required": ["version", "changes_summary", "date"],
"title": "ChangelogEntry"
},
"ChatRequest": {
"properties": {
"query": { "type": "string", "title": "Query" },
@@ -7953,6 +7973,11 @@
"title": "Auto Update Version",
"description": "Auto-update the agent version"
},
"graph_version": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Graph Version",
"description": "Specific graph version to update to"
},
"is_favorite": {
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
"title": "Is Favorite",
@@ -9508,6 +9533,12 @@
"type": "array",
"title": "Versions"
},
"agentGraphVersions": {
"items": { "type": "string" },
"type": "array",
"title": "Agentgraphversions"
},
"agentGraphId": { "type": "string", "title": "Agentgraphid" },
"last_updated": {
"type": "string",
"format": "date-time",
@@ -9525,6 +9556,16 @@
"type": "boolean",
"title": "Has Approved Version",
"default": false
},
"changelog": {
"anyOf": [
{
"items": { "$ref": "#/components/schemas/ChangelogEntry" },
"type": "array"
},
{ "type": "null" }
],
"title": "Changelog"
}
},
"type": "object",
@@ -9543,6 +9584,8 @@
"runs",
"rating",
"versions",
"agentGraphVersions",
"agentGraphId",
"last_updated"
],
"title": "StoreAgentDetails"

View File

@@ -0,0 +1,102 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
interface MarketplaceBannersProps {
hasUpdate?: boolean;
latestVersion?: number;
hasUnpublishedChanges?: boolean;
currentVersion?: number;
isUpdating?: boolean;
onUpdate?: () => void;
onPublish?: () => void;
onViewChanges?: () => void;
}
export function MarketplaceBanners({
hasUpdate,
latestVersion,
hasUnpublishedChanges,
isUpdating,
onUpdate,
onPublish,
}: MarketplaceBannersProps) {
const renderUpdateBanner = () => {
if (hasUpdate && latestVersion) {
return (
<div className="mb-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-900">
<div className="flex flex-col gap-3">
<div>
<Text
variant="large-medium"
className="mb-2 text-neutral-900 dark:text-neutral-100"
>
Update available
</Text>
<Text variant="body" className="text-gray-700 dark:text-gray-300">
You should update your agent in order to get the latest / best
results
</Text>
</div>
{onUpdate && (
<div className="flex justify-start">
<Button
size="small"
onClick={onUpdate}
disabled={isUpdating}
className="bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:hover:bg-neutral-800"
>
{isUpdating ? "Updating..." : "Update agent"}
</Button>
</div>
)}
</div>
</div>
);
}
return null;
};
const renderUnpublishedChangesBanner = () => {
if (hasUnpublishedChanges) {
return (
<div className="mb-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-900">
<div className="flex flex-col gap-3">
<div>
<Text
variant="large-medium"
className="mb-2 text-neutral-900 dark:text-neutral-100"
>
Unpublished changes
</Text>
<Text variant="body" className="text-gray-700 dark:text-gray-300">
You&apos;ve made changes to this agent that aren&apos;t
published yet. Would you like to publish the latest version?
</Text>
</div>
{onPublish && (
<div className="flex justify-start">
<Button
size="small"
onClick={onPublish}
className="bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:hover:bg-neutral-800"
>
Publish changes
</Button>
</div>
)}
</div>
</div>
);
}
return null;
};
return (
<>
{renderUpdateBanner()}
{renderUnpublishedChangesBanner()}
</>
);
}

View File

@@ -18,6 +18,8 @@ export function PublishAgentModal({
trigger,
targetState,
onStateChange,
preSelectedAgentId,
preSelectedAgentVersion,
}: Props) {
const {
// State
@@ -34,7 +36,12 @@ export function PublishAgentModal({
handleGoToBuilder,
handleSuccessFromInfo,
handleBack,
} = usePublishAgentModal({ targetState, onStateChange });
} = usePublishAgentModal({
targetState,
onStateChange,
preSelectedAgentId,
preSelectedAgentVersion,
});
const { user, isUserLoading } = useSupabase();
@@ -65,6 +72,7 @@ export function PublishAgentModal({
selectedAgentId={selectedAgentId}
selectedAgentVersion={selectedAgentVersion}
initialData={initialData}
isMarketplaceUpdate={!!currentState.submissionData}
/>
);
case "review":

View File

@@ -19,6 +19,7 @@ export function AgentInfoStep({
selectedAgentId,
selectedAgentVersion,
initialData,
isMarketplaceUpdate,
}: Props) {
const {
form,
@@ -34,6 +35,7 @@ export function AgentInfoStep({
selectedAgentId,
selectedAgentVersion,
initialData,
isMarketplaceUpdate,
});
const [cronScheduleDialogOpen, setCronScheduleDialogOpen] =
@@ -65,6 +67,41 @@ export function AgentInfoStep({
<Form {...form}>
<form onSubmit={handleSubmit} className="flex-grow overflow-y-auto p-6">
{/* Changes summary field - only shown for updates */}
{isMarketplaceUpdate && (
<FormField
control={form.control}
name="changesSummary"
render={({ field }) => (
<div className="mb-6">
<Input
id={field.name}
label="What changed?"
type="textarea"
placeholder="Describe what's new or improved in this version..."
error={form.formState.errors.changesSummary?.message}
required
{...field}
/>
<Text variant="small" className="mt-1 text-gray-600">
This is required to help users understand what&apos;s
different in this update.
</Text>
</div>
)}
/>
)}
{/* Optional section label for updates */}
{isMarketplaceUpdate && (
<div className="mb-4">
<Text variant="body" className="font-medium text-gray-700">
Optional: Update any of the following details (or leave them
as-is)
</Text>
</div>
)}
<FormField
control={form.control}
name="title"

View File

@@ -25,6 +25,17 @@ export function useThumbnailImages({
const thumbnailsContainerRef = useRef<HTMLDivElement | null>(null);
const { toast } = useToast();
// Memoize the stringified version to detect actual changes
const initialImagesKey = JSON.stringify(initialImages);
// Update images when initialImages prop changes (by value, not reference)
useEffect(() => {
if (initialImages.length > 0) {
setImages(initialImages);
setSelectedImage(initialSelectedImage || initialImages[0]);
}
}, [initialImagesKey, initialSelectedImage]); // Use stringified key instead of array reference
// Notify parent when images change
useEffect(() => {
onImagesChange(images);

View File

@@ -1,45 +1,113 @@
import z from "zod";
import { validateYouTubeUrl } from "@/lib/utils";
export const publishAgentSchema = z.object({
title: z
.string()
.min(1, "Title is required")
.max(100, "Title must be less than 100 characters"),
subheader: z
.string()
.min(1, "Subheader is required")
.max(200, "Subheader must be less than 200 characters"),
slug: z
.string()
.min(1, "Slug is required")
.max(50, "Slug must be less than 50 characters")
.regex(
/^[a-z0-9-]+$/,
"Slug can only contain lowercase letters, numbers, and hyphens",
),
youtubeLink: z
.string()
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
category: z.string().min(1, "Category is required"),
description: z
.string()
.min(1, "Description is required")
.max(1000, "Description must be less than 1000 characters"),
recommendedScheduleCron: z.string().optional(),
instructions: z
.string()
.optional()
.refine(
(val) => !val || val.length <= 2000,
"Instructions must be less than 2000 characters",
),
agentOutputDemo: z
.string()
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
});
// Create conditional schema that changes based on whether it's a marketplace update
export const publishAgentSchemaFactory = (
isMarketplaceUpdate: boolean = false,
) => {
const baseSchema = {
changesSummary: isMarketplaceUpdate
? z
.string()
.min(1, "Changes summary is required for updates")
.max(500, "Changes summary must be less than 500 characters")
: z.string().optional(),
title: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || val.length <= 100,
"Title must be less than 100 characters",
)
: z
.string()
.min(1, "Title is required")
.max(100, "Title must be less than 100 characters"),
subheader: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || val.length <= 200,
"Subheader must be less than 200 characters",
)
: z
.string()
.min(1, "Subheader is required")
.max(200, "Subheader must be less than 200 characters"),
slug: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || (val.length <= 50 && /^[a-z0-9-]+$/.test(val)),
"Slug can only contain lowercase letters, numbers, and hyphens",
)
: z
.string()
.min(1, "Slug is required")
.max(50, "Slug must be less than 50 characters")
.regex(
/^[a-z0-9-]+$/,
"Slug can only contain lowercase letters, numbers, and hyphens",
),
youtubeLink: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || validateYouTubeUrl(val),
"Please enter a valid YouTube URL",
)
: z
.string()
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
category: isMarketplaceUpdate
? z.string().optional()
: z.string().min(1, "Category is required"),
description: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || val.length <= 1000,
"Description must be less than 1000 characters",
)
: z
.string()
.min(1, "Description is required")
.max(1000, "Description must be less than 1000 characters"),
recommendedScheduleCron: z.string().optional(),
instructions: z
.string()
.optional()
.refine(
(val) => !val || val.length <= 2000,
"Instructions must be less than 2000 characters",
),
agentOutputDemo: isMarketplaceUpdate
? z
.string()
.optional()
.refine(
(val) => !val || validateYouTubeUrl(val),
"Please enter a valid YouTube URL",
)
: z
.string()
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
};
export type PublishAgentFormData = z.infer<typeof publishAgentSchema>;
return z.object(baseSchema);
};
// Default schema for backwards compatibility
export const publishAgentSchema = publishAgentSchemaFactory(false);
export type PublishAgentFormData = z.infer<
ReturnType<typeof publishAgentSchemaFactory>
>;
export interface PublishAgentInfoInitialData {
agent_id: string;
@@ -54,4 +122,5 @@ export interface PublishAgentInfoInitialData {
recommendedScheduleCron?: string;
instructions?: string;
agentOutputDemo?: string;
changesSummary?: string;
}

View File

@@ -9,7 +9,7 @@ import * as Sentry from "@sentry/nextjs";
import {
PublishAgentFormData,
PublishAgentInfoInitialData,
publishAgentSchema,
publishAgentSchemaFactory,
} from "./helpers";
export interface Props {
@@ -18,6 +18,7 @@ export interface Props {
selectedAgentId: string | null;
selectedAgentVersion: number | null;
initialData?: PublishAgentInfoInitialData;
isMarketplaceUpdate?: boolean;
}
export function useAgentInfoStep({
@@ -26,6 +27,7 @@ export function useAgentInfoStep({
selectedAgentId,
selectedAgentVersion,
initialData,
isMarketplaceUpdate = false,
}: Props) {
const [agentId, setAgentId] = useState<string | null>(null);
const [images, setImages] = useState<string[]>([]);
@@ -36,8 +38,9 @@ export function useAgentInfoStep({
const api = useBackendAPI();
const form = useForm<PublishAgentFormData>({
resolver: zodResolver(publishAgentSchema),
resolver: zodResolver(publishAgentSchemaFactory(isMarketplaceUpdate)),
defaultValues: {
changesSummary: "",
title: "",
subheader: "",
slug: "",
@@ -61,6 +64,7 @@ export function useAgentInfoStep({
// Update form with initial data
form.reset({
changesSummary: initialData.changesSummary || "",
title: initialData.title,
subheader: initialData.subheader,
slug: initialData.slug.toLocaleLowerCase().trim(),
@@ -104,9 +108,10 @@ export function useAgentInfoStep({
agent_output_demo_url: data.agentOutputDemo || "",
agent_id: selectedAgentId || "",
agent_version: selectedAgentVersion || 0,
slug: data.slug.replace(/\s+/g, "-"),
slug: (data.slug || "").replace(/\s+/g, "-"),
categories: filteredCategories,
recommended_schedule_cron: data.recommendedScheduleCron || null,
changes_summary: data.changesSummary || null,
} as any);
await queryClient.invalidateQueries({

View File

@@ -52,7 +52,7 @@ export function AgentReviewStep({
</Text>
<Text
variant="large"
className="line-clamp-1 text-ellipsis text-center !text-neutral-500"
className="line-clamp-1 text-ellipsis text-center text-neutral-500"
>
{subheader}
</Text>
@@ -80,7 +80,7 @@ export function AgentReviewStep({
{description ? (
<Text
variant="large"
className="line-clamp-1 text-ellipsis pt-2 text-center !text-neutral-500"
className="line-clamp-1 text-ellipsis pt-2 text-center text-neutral-500"
>
{description}
</Text>

View File

@@ -8,4 +8,8 @@ export const emptyModalState = {
category: "",
description: "",
recommendedScheduleCron: "",
instructions: "",
agentOutputDemo: "",
changesSummary: "",
additionalImages: [],
};

View File

@@ -3,6 +3,12 @@ import { useCallback, useEffect, useState } from "react";
import { PublishAgentInfoInitialData } from "./components/AgentInfoStep/helpers";
import { useRouter } from "next/navigation";
import { emptyModalState } from "./helpers";
import {
useGetV2GetMyAgents,
useGetV2ListMySubmissions,
} from "@/app/api/__generated__/endpoints/store/store";
import { okData } from "@/app/api/helpers";
import type { MyAgent } from "@/app/api/__generated__/models/myAgent";
const defaultTargetState: PublishState = {
isOpen: false,
@@ -22,9 +28,16 @@ export interface Props {
trigger?: React.ReactNode;
targetState?: PublishState;
onStateChange?: (state: PublishState) => void;
preSelectedAgentId?: string;
preSelectedAgentVersion?: number;
}
export function usePublishAgentModal({ targetState, onStateChange }: Props) {
export function usePublishAgentModal({
targetState,
onStateChange,
preSelectedAgentId,
preSelectedAgentVersion,
}: Props) {
const [currentState, setCurrentState] = useState<PublishState>(
targetState || defaultTargetState,
);
@@ -42,14 +55,20 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
const [_, setSelectedAgent] = useState<string | null>(null);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(
preSelectedAgentId || null,
);
const [selectedAgentVersion, setSelectedAgentVersion] = useState<
number | null
>(null);
>(preSelectedAgentVersion || null);
const router = useRouter();
// Fetch agent data for pre-populating form when agent is pre-selected
const { data: myAgents } = useGetV2GetMyAgents();
const { data: mySubmissions } = useGetV2ListMySubmissions();
// Sync currentState with targetState when it changes from outside
useEffect(() => {
if (targetState) {
@@ -60,13 +79,90 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
// Reset internal state when modal opens
useEffect(() => {
if (!targetState) return;
if (targetState.isOpen && targetState.step === "select") {
if (targetState.isOpen) {
setSelectedAgent(null);
setSelectedAgentId(null);
setSelectedAgentVersion(null);
setSelectedAgentId(preSelectedAgentId || null);
setSelectedAgentVersion(preSelectedAgentVersion || null);
setInitialData(emptyModalState);
}
}, [targetState]);
}, [targetState, preSelectedAgentId, preSelectedAgentVersion]);
// Pre-populate form data when modal opens with info step and pre-selected agent
useEffect(() => {
if (
!targetState?.isOpen ||
targetState.step !== "info" ||
!preSelectedAgentId ||
!preSelectedAgentVersion
)
return;
const agentsData = okData(myAgents) as any;
const submissionsData = okData(mySubmissions) as any;
if (!agentsData || !submissionsData) return;
// Find the agent data
const agent = agentsData.agents?.find(
(a: MyAgent) => a.agent_id === preSelectedAgentId,
);
if (!agent) return;
// Find published submission data for this agent (for updates)
const publishedSubmissionData = submissionsData.submissions
?.filter(
(s: StoreSubmission) =>
s.status === "APPROVED" && s.agent_id === preSelectedAgentId,
)
.sort(
(a: StoreSubmission, b: StoreSubmission) =>
b.agent_version - a.agent_version,
)[0];
// Populate initial data (same logic as handleNextFromSelect)
const initialFormData: PublishAgentInfoInitialData = publishedSubmissionData
? {
agent_id: preSelectedAgentId,
title: publishedSubmissionData.name,
subheader: publishedSubmissionData.sub_heading || "",
description: publishedSubmissionData.description,
instructions: publishedSubmissionData.instructions || "",
youtubeLink: publishedSubmissionData.video_url || "",
agentOutputDemo: publishedSubmissionData.agent_output_demo_url || "",
additionalImages: [
...new Set(publishedSubmissionData.image_urls || []),
].filter(Boolean) as string[],
category: publishedSubmissionData.categories?.[0] || "",
thumbnailSrc: agent.agent_image || "https://picsum.photos/300/200",
slug: publishedSubmissionData.slug,
recommendedScheduleCron: agent.recommended_schedule_cron || "",
changesSummary: publishedSubmissionData.changes_summary || "",
}
: {
...emptyModalState,
agent_id: preSelectedAgentId,
title: agent.agent_name,
description: agent.description || "",
thumbnailSrc: agent.agent_image || "https://picsum.photos/300/200",
slug: agent.agent_name.replace(/ /g, "-"),
recommendedScheduleCron: agent.recommended_schedule_cron || "",
};
setInitialData(initialFormData);
// Update the state with the submission data if this is an update
if (publishedSubmissionData) {
setCurrentState((prevState) => ({
...prevState,
submissionData: publishedSubmissionData,
}));
}
}, [
targetState,
preSelectedAgentId,
preSelectedAgentVersion,
myAgents,
mySubmissions,
]);
function handleClose() {
// Reset all internal state
@@ -97,20 +193,43 @@ export function usePublishAgentModal({ targetState, onStateChange }: Props) {
imageSrc: string;
recommendedScheduleCron: string | null;
},
publishedSubmissionData?: StoreSubmission | null,
) {
setInitialData({
...emptyModalState,
agent_id: agentId,
title: agentData.name,
description: agentData.description,
thumbnailSrc: agentData.imageSrc,
slug: agentData.name.replace(/ /g, "-"),
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
});
// Pre-populate with published data if this is an update, otherwise use agent data
const initialFormData: PublishAgentInfoInitialData = publishedSubmissionData
? {
agent_id: agentId,
title: publishedSubmissionData.name,
subheader: publishedSubmissionData.sub_heading || "",
description: publishedSubmissionData.description,
instructions: publishedSubmissionData.instructions || "",
youtubeLink: publishedSubmissionData.video_url || "",
agentOutputDemo: publishedSubmissionData.agent_output_demo_url || "",
additionalImages: [
...new Set(publishedSubmissionData.image_urls || []),
].filter(Boolean) as string[],
category: publishedSubmissionData.categories?.[0] || "", // Take first category
thumbnailSrc: agentData.imageSrc, // Use current agent image
slug: publishedSubmissionData.slug,
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
changesSummary: publishedSubmissionData.changes_summary || "", // Pre-populate with existing changes summary
}
: {
...emptyModalState,
agent_id: agentId,
title: agentData.name,
description: agentData.description,
thumbnailSrc: agentData.imageSrc,
slug: agentData.name.replace(/ /g, "-"),
recommendedScheduleCron: agentData.recommendedScheduleCron || "",
};
setInitialData(initialFormData);
updateState({
...currentState,
step: "info",
submissionData: publishedSubmissionData || null,
});
setSelectedAgentId(agentId);

View File

@@ -0,0 +1,58 @@
/**
* Marketplace-specific helper functions that can be reused across different marketplace screens
*/
/**
* Calculate the latest marketplace version from agent graph versions
*/
export function getLatestMarketplaceVersion(
agentGraphVersions?: string[],
): number | undefined {
if (!agentGraphVersions?.length) return undefined;
return Math.max(...agentGraphVersions.map((v: string) => parseInt(v, 10)));
}
/**
* Check if the current user is the creator of the agent
*/
export function isUserCreator(
creator: string,
currentUser: { email?: string } | null,
): boolean {
if (!currentUser?.email) return false;
const userHandle = currentUser.email.split("@")[0]?.toLowerCase() || "";
return creator.toLowerCase().includes(userHandle);
}
/**
* Calculate update status for an agent
*/
export function calculateUpdateStatus({
latestMarketplaceVersion,
currentVersion,
isUserCreator,
isAgentAddedToLibrary,
}: {
latestMarketplaceVersion?: number;
currentVersion: number;
isUserCreator: boolean;
isAgentAddedToLibrary: boolean;
}) {
if (!latestMarketplaceVersion) {
return { hasUpdate: false, hasUnpublishedChanges: false };
}
const hasUnpublishedChanges =
isUserCreator &&
isAgentAddedToLibrary &&
currentVersion > latestMarketplaceVersion;
const hasUpdate =
isAgentAddedToLibrary &&
!isUserCreator &&
latestMarketplaceVersion > currentVersion;
return { hasUpdate, hasUnpublishedChanges };
}