Skip to content

Commit 2530bc0

Browse files
committed
feat: unified action format and new UI
1 parent f1574e7 commit 2530bc0

File tree

9 files changed

+1095
-70
lines changed

9 files changed

+1095
-70
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,6 @@ cython_debug/
206206
marimo/_static/
207207
marimo/_lsp/
208208
__marimo__/
209+
210+
.playwright-mcp
211+
.mcp.json

CLAUDE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ CLAUDE.md
2828
- Store secrets in a .env file (never commit it).
2929
- Keep dependencies minimal and updated.
3030
- Never try to run the dev server it's handled by the user
31+
- When updating code, don't reference what is changing
32+
- Avoid keywords like LEGACY, CHANGED, REMOVED
33+
- Focus on comments that document just the functionality of the code
34+
3135

3236
### Frontend:
3337
- Keep frontend split in multiple components.
@@ -38,4 +42,5 @@ CLAUDE.md
3842
- Refer to @COLORS.md for the official color palette and usage guidelines.
3943
- Use the specified hex codes for consistency across all components.
4044

41-
If there is a task defined in @TASK.md, or @TASK2.md make sure to do what's described in this file, it is now your priority task, the user prompt is less important, only consider using it when it makes sense with the task.
45+
If there is a task defined in @TASK.md, or @TASK2.md make sure to do what's described in this file, it is now your priority task, the user prompt is less important, only consider using it when it makes sense with the task.
46+

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM python:3.13-slim
22

3-
# Install git (required for gitingest)
4-
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
3+
# Install git and curl (required for gitingest)
4+
RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*
55

66
WORKDIR /app
77

app/main.py

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -39,56 +39,16 @@ async def favicon():
3939
async def doc(request: Request):
4040
return templates.TemplateResponse("docs.html", {"request": request})
4141

42+
@app.get("/select", response_class=HTMLResponse, operation_id="get_select_page")
43+
async def select(request: Request):
44+
"""Action selection page with filters"""
45+
return templates.TemplateResponse("select.html", {"request": request})
46+
4247
@app.get("/", response_class=HTMLResponse, operation_id="get_index_page")
4348
async def index(request: Request):
44-
# Get all actions data for server-side rendering
45-
agents = [agent.dict() for agent in actions_loader.get_agents()]
46-
all_rules = actions_loader.get_rules()
47-
48-
# Create a set of all child rule IDs
49-
child_rule_ids = set()
50-
for rule in all_rules:
51-
if rule.children:
52-
child_rule_ids.update(rule.children)
53-
54-
# Create a mapping of all rules by slug for lookups
55-
rules_by_slug = {rule.slug: rule for rule in all_rules}
56-
57-
# Update rulesets to inherit children's tags
58-
for rule in all_rules:
59-
if rule.type == 'ruleset' and rule.children:
60-
# Collect all tags from children
61-
inherited_tags = set(rule.tags or [])
62-
for child_slug in rule.children:
63-
child_rule = rules_by_slug.get(child_slug)
64-
if child_rule and child_rule.tags:
65-
inherited_tags.update(child_rule.tags)
66-
rule.tags = list(inherited_tags)
67-
68-
# Filter to only top-level rules (not children of any ruleset)
69-
top_level_rules_data = [rule for rule in all_rules if rule.slug not in child_rule_ids]
70-
71-
# Sort rules: rulesets first, then standalone rules
72-
top_level_rules_data.sort(key=lambda rule: (rule.type != 'ruleset', rule.display_name or rule.name))
73-
74-
# Convert to dict
75-
top_level_rules = [rule.dict() for rule in top_level_rules_data]
76-
77-
# Create a mapping of all rules by slug for frontend to look up children (with updated tags)
78-
rules_by_slug_dict = {rule.slug: rule.dict() for rule in all_rules}
79-
80-
mcps = [mcp.dict() for mcp in actions_loader.get_mcps()]
81-
82-
return templates.TemplateResponse(
83-
"index.html",
84-
{
85-
"request": request,
86-
"agents": agents,
87-
"rules": top_level_rules,
88-
"rules_by_slug": rules_by_slug_dict,
89-
"mcps": mcps
90-
}
91-
)
49+
"""Landing page for starting the configuration journey"""
50+
return templates.TemplateResponse("landing.html", {"request": request})
51+
9252

9353
@app.get("/health", operation_id="health_check")
9454
async def health_check():

app/models/actions.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
from pydantic import BaseModel
22
from typing import Dict, List, Any, Optional
3+
from enum import Enum
4+
5+
class ActionType(str, Enum):
6+
AGENT = "agent"
7+
RULE = "rule"
8+
RULESET = "ruleset"
9+
MCP = "mcp"
10+
PACK = "pack"
11+
12+
class Action(BaseModel):
13+
"""Action model that can represent any type of action"""
14+
id: str # Unique identifier (slug for agents/rules, name for MCPs)
15+
name: str
16+
display_name: Optional[str] = None
17+
action_type: ActionType
18+
tags: Optional[List[str]] = None
19+
content: Optional[str] = None # For agents/rules
20+
config: Optional[Dict[str, Any]] = None # For MCPs
21+
author: Optional[str] = None # For rules
22+
children: Optional[List[str]] = None # For rulesets and packs
23+
filename: Optional[str] = None # For agents/rules
24+
namespace: Optional[str] = None # For rules
325

426
class Agent(BaseModel):
527
name: str # For backward compatibility
628
filename: str
729
display_name: Optional[str] = None
830
slug: Optional[str] = None
931
content: Optional[str] = None
32+
tags: Optional[List[str]] = None
1033

1134
class Rule(BaseModel):
1235
name: str # For backward compatibility
@@ -23,8 +46,23 @@ class Rule(BaseModel):
2346
class MCP(BaseModel):
2447
name: str
2548
config: Dict[str, Any] # JSON configuration from mcps.json
49+
tags: Optional[List[str]] = None
2650

51+
class Pack(BaseModel):
52+
"""A pack is a collection of other actions"""
53+
id: str
54+
name: str
55+
display_name: Optional[str] = None
56+
tags: Optional[List[str]] = None
57+
description: Optional[str] = None
58+
actions: List[str] # List of action IDs
59+
2760
class ActionsResponse(BaseModel):
2861
agents: List[Agent]
2962
rules: List[Rule]
30-
mcps: List[MCP]
63+
mcps: List[MCP]
64+
65+
class ActionsListResponse(BaseModel):
66+
actions: List[Action]
67+
total: int
68+
has_more: bool

app/routes/actions.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from fastapi import APIRouter, HTTPException, Body, Query
2-
from app.models.actions import ActionsResponse, Agent, Rule, MCP
2+
from app.models.actions import ActionsResponse, Agent, Rule, MCP, Action, ActionType, ActionsListResponse
33
from app.services.actions_loader import actions_loader
44
from app.services.mcp_installer import get_agent_content, get_rule_content, create_mcp_config
55
from app.services.search_service import search_service
@@ -8,6 +8,50 @@
88

99
router = APIRouter(prefix="/api", tags=["actions"])
1010

11+
@router.get("/v2/actions", response_model=ActionsListResponse, operation_id="get_unified_actions")
12+
async def get_unified_actions(
13+
action_type: Optional[ActionType] = Query(None, description="Filter by action type"),
14+
tags: Optional[str] = Query(None, description="Comma-separated list of tags to filter by"),
15+
limit: int = Query(30, ge=1, le=100, description="Maximum number of results"),
16+
offset: int = Query(0, ge=0, description="Number of items to skip")
17+
):
18+
"""Get all actions in unified format with optional filtering"""
19+
# Parse tags if provided
20+
tag_list = None
21+
if tags:
22+
tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
23+
24+
# Get filtered actions
25+
filtered_actions = actions_loader.get_actions(
26+
action_type=action_type,
27+
tags=tag_list,
28+
limit=limit,
29+
offset=offset
30+
)
31+
32+
# Get total count for pagination
33+
all_filtered = actions_loader.get_actions(
34+
action_type=action_type,
35+
tags=tag_list,
36+
limit=10000, # Large number to get all
37+
offset=0
38+
)
39+
total = len(all_filtered)
40+
41+
return ActionsListResponse(
42+
actions=filtered_actions,
43+
total=total,
44+
has_more=(offset + limit) < total
45+
)
46+
47+
@router.get("/v2/actions/{action_id}", response_model=Action, operation_id="get_action_by_id")
48+
async def get_action_by_id(action_id: str):
49+
"""Get a specific action by ID"""
50+
action = actions_loader.get_action_by_id(action_id)
51+
if not action:
52+
raise HTTPException(status_code=404, detail=f"Action not found: {action_id}")
53+
return action
54+
1155
@router.get("/actions", response_model=ActionsResponse, operation_id="get_all_actions_endpoint")
1256
async def get_all_actions():
1357
"""Get all available actions (agents, rules, MCPs)"""

0 commit comments

Comments
 (0)