Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implementing UI/UX features in sidecar including modals (internal/modal library), keyboard shortcuts, mouse support, scrolling, pill/tab rendering, and pane resizing.
SkillScore91/100
Test results 91
3/3 safety checks passed
36%91%of test cases passed +55 pts
Passed 20 of 22 test cases with the skill, and 8 of 22 without it.
12 fixed8 passed either way2 still failing0 broke
One test taskIn a plugin modal where content depends on asynchronously fetched git branch data, what should happen in the plugin Update method when branch data arrives to ensure the modal displays refreshed content?
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add marcus-ui-features
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install marcus-ui-featuresThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/marcus-ui-features/SKILL.md https://app.decimal.ai/s/marcus-ui-features/SKILL.md.claude/skills/marcus-ui-features/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add marcus-ui-featuresimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add marcus-ui-featuresfrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add marcus-ui-featuresimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add marcus-ui-featuresimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add marcus-ui-featuresfrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add marcus-ui-featuresfrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add marcus-ui-featuresimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add marcus-ui-featuresfrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add marcus-ui-featuresfrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add marcus-ui-featuresimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add marcus-ui-featuresimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 1 other file is not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add marcus-ui-featuresfrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("marcus-ui-features")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/marcus-ui-features/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/marcus-ui-features/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add marcus-ui-featuresNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/marcus-ui-features/SKILL.md https://app.decimal.ai/s/marcus-ui-features/SKILL.md.agents/skills/marcus-ui-features/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 1 other file is not included.
Raw files: /s/marcus-ui-features/SKILL.md · /s/marcus-ui-features.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/marcus-ui-features/SKILL.mdcurl -fsSL https://app.decimal.ai/s/marcus-ui-features.jsonThis gets SKILL.md only. The skill's 1 other file is not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
Single entry point for sidecar UI work. All new modals must use internal/modal. For complete keyboard shortcut listings, see references/keyboard-shortcuts-reference.md.
internal/modal, render with ui.OverlayModal, avoid manual hit region mathstyles.RenderPillWithStyle; auto-fallback when nerdFontsEnabled is falsecontentHeight := height - headerLines - footerLinesCommands()All new modals must use internal/modal. See docs/guides/deprecated/declarative-modal-guide.md for the full API.
m := modal.New("Delete Worktree?",
modal.WithWidth(58),
modal.WithVariant(modal.VariantDanger),
modal.WithPrimaryAction("delete"),
).
AddSection(modal.Text("Name: " + wt.Name)).
AddSection(modal.Spacer()).
AddSection(modal.Buttons(
modal.Btn(" Delete ", "delete", modal.BtnDanger()),
modal.Btn(" Cancel ", "cancel"),
))
func (p *Plugin) renderDeleteView(width, height int) string {
background := p.renderListView(width, height)
rendered := p.deleteModal.Render(width, height, p.mouseHandler)
return ui.OverlayModal(background, rendered, width, height)
}
case tea.KeyMsg:
action, cmd := p.deleteModal.HandleKey(msg)
if action != "" {
return p.handleModalAction(action)
}
return p, cmd
case tea.MouseMsg:
action := p.deleteModal.HandleMouse(msg, p.mouseHandler)
if action != "" {
return p.handleModalAction(action)
}
return p, nil
Always call ensureModal() in BOTH View and Update handlers. Create an ensure function that:
func (p *Plugin) ensureMyModal() {
if p.targetItem == nil {
return
}
modalW := 50
if modalW > p.width-4 { modalW = p.width - 4 }
if modalW < 20 { modalW = 20 }
if p.myModal != nil && p.myModalWidthCache == modalW {
return
}
p.myModalWidthCache = modalW
p.myModal = modal.New("Title", modal.WithWidth(modalW), ...).
AddSection(...)
}
The key handler MUST call ensure before checking nil:
func (p *Plugin) handleMyModalKeys(msg tea.KeyMsg) tea.Cmd {
p.ensureMyModal() // CRITICAL: Initialize before nil check
if p.myModal == nil { return nil }
action, cmd := p.myModal.HandleKey(msg)
return cmd
}
When modal content depends on async data, invalidate the cache when data arrives:
case MyDataLoadedMsg:
p.myData = msg.Data
p.clearMyModal() // Force rebuild with new content
return p, nil
Modals need their own focus context and commands for footer hints:
FocusContext()Commands()internal/keymap/bindings.gomodal.HandleKey (Tab/Enter/Esc are handled internally)func (p *Plugin) FocusContext() string {
switch p.viewMode {
case ViewModeError: return "git-error"
case ViewModePushMenu: return "git-push-menu"
default: return "git-status"
}
}
HandleKey/HandleMouse handle Tab, Shift+Tab, Enter, Esc internallyWithCloseOnBackdropClick(false) to disablemodal.Custom and return explicit focusable offsetsSetFocus(id) auto-scrolls viewport to focused elementui.OverlayModal(background, modal, width, height) for dimmed overlays; do not pre-center with lipgloss.PlaceLipgloss Background() does not cascade into child content. ANSI resets clear the parent background. Solution: replace ANSI resets within viewport lines with reset + background re-apply, then pad short lines. See fillBackground in internal/modal/layout.go.
Controlled by nerdFontsEnabled in ~/.config/sidecar/config.json (ui.nerdFontsEnabled).
// With explicit colors
label := styles.RenderPill("Output", styles.TextPrimary, styles.Primary, "")
// With a lipgloss.Style (preferred for tabs/chips)
active := styles.RenderPillWithStyle("Output", styles.BarChipActive, "")
inactive := styles.RenderPillWithStyle("Diff", styles.BarChip, "")
Available styles: styles.BarChip (inactive), styles.BarChipActive (active), or custom lipgloss.Style.
Test with both nerdFontsEnabled: true and false to verify fallback.
For complete per-plugin shortcut listings, see references/keyboard-shortcuts-reference.md.
Commands() (e.g., "stage-file")internal/keymap/bindings.go (e.g., "stage-file")"git-status")// 1) Commands()
{ID: "stage-file", Name: "Stage", Context: "git-status", Priority: 1}
// 2) FocusContext()
func (p *Plugin) FocusContext() string { return "git-status" }
// 3) bindings.go
{Key: "s", Command: "stage-file", Context: "git-status"}
Return different context strings from FocusContext() for different modes. Each context gets its own footer hints and key bindings.
In root contexts, q shows quit confirmation. In non-root, q navigates back. Root contexts: global, conversations, conversations-sidebar, git-status, git-status-commits, git-status-diff, file-browser-tree, workspace-list, td-monitor.
Update isRootContext() in internal/app/update.go when adding new contexts.
When a view has text input, implement plugin.TextInputConsumer and return true while active. This prevents app-level shortcuts from intercepting typed characters.
func (p *Plugin) ConsumesTextInput() bool {
return p.showMyModal
}
footerHints()
+-- pluginFooterHints() -> Commands() filtered by FocusContext(), sorted by Priority
+-- globalFooterHints() -> App-level hints
renderHintLineTruncated(hints, availableWidth)
-> Renders left-to-right until width exceeded
Commands() with ID, Name, Context, PriorityFocusContext() returns matching contextinternal/keymap/bindings.goUpdate() if app does not interceptq behavior with isRootContext()| File | Purpose |
|---|---|
internal/plugin/plugin.go | Command struct, Commands(), FocusContext(), TextInputConsumer |
internal/keymap/bindings.go | Default key-to-command mappings |
internal/keymap/registry.go | Runtime binding lookup |
internal/app/update.go | Key routing, isRootContext() |
internal/app/view.go | Footer rendering |
rendered, geom := ui.RenderScrollbarWithState(ui.ScrollbarParams{
TotalItems: len(items),
ScrollOffset: p.scrollOffset,
VisibleItems: visibleCount,
TrackHeight: height,
}, style) // ui.ScrollbarStyle{Thumb: state, Track: state}; zero value renders byte-identically to RenderScrollbar
Every scrollbar backed by real scroll state is interactive under the mouse (macOS-style),
including the terminal panes' bars over captured scrollback. Terminal surfaces map
presses and drags through tty.WindowScrollbarFor onto the shared window model: the
gesture freezes the window at an absolute start, release thaws (offset zero follows
again), and history loads defer to release so a mid-gesture renumber cannot shift the
mapping. Adoption ledger:
docs/plans/active/mouse-draggable-scrollbars.md ("Adoption outcome").
Pattern: reduce content width by 1, render content, render scrollbar, join horizontally with lipgloss.JoinHorizontal(lipgloss.Top, content, scrollbar).
For multi-line items, set TrackHeight to actual terminal rows: visibleCount * linesPerItem.
Mouse contract per surface:
ui.RegionScrollbarTrack, then ui.RegionScrollbarThumb, after content
regions in the same render pass that builds the HitMap (reverse scan gives the bar its
column). Register nothing when geom.HasThumb is false — that column is a spacer.StartDrag so releasing anywhere settles cleanly. Drag maps through
ui.OffsetAtRow against the press-time params snapshot; past-end clamps never end the
gesture.ActionDoubleClick: answer it like ActionClick
(re-grab), never swallow it.ui.HandleStateFrom(hovering, dragging) — intensity modulation on
the existing theme keys, no new keys.Trackpad and Magic Mouse flicks emit hundreds of inertial wheel events. Bubble Tea
repaints all of Sidecar after every accepted one, so clamping an offset during
Update is too late — the freeze happens before the clamp helps. tea.WithFilter(app.FilterInput)
asks one read-only question before Update and View:
Would this exact wheel event change the surface currently under the pointer?
Rule: every new scrollable surface or modal must provide exact pre-update
bounds, or explicitly declare why its answer is unknown. "Unknown" is a valid,
safe answer — guessing is not. Return true only when the event is a certain
no-op.
How to comply:
plugin.WheelBoundaryConsumer on the plugin
(WheelAtBoundary(tea.MouseWheelMsg) bool) and add
var _ plugin.WheelBoundaryConsumer = (*Plugin)(nil).handleMouseScroll's routing exactly — same hit map, same modal
precedence — but load nothing, move nothing, render nothing.internal/scroll.Bounds), never a second copy of the arithmetic.modal.WheelAtBoundary(msg, handler); the host only owns precedence between
a modal, a nested overlay, and a custom scrolling child.
Call Invalidate() when content or geometry changes so a stale layout
answers unknown instead of wrong.false (unknown) for: embedded models you do not own, tmux panes with
mouse reporting, scrollback with unloaded history, lazy lists that can load
more, and anything before its first trustworthy render.assembly.WheelBoundaryRegistry
(covered / externally-owned / deprecated-exclusion). A new plugin
without a row fails the assembly tests; a new ModalKind without a row fails
TestEveryModalKindHasALedgerRow in internal/app.internal/scroll/scrolltest:
scrolltest.Run(t, scrolltest.Tail{...}) feeds hundreds of same-direction
events and one reverse event, with no sleeps.Background: docs/plans/active/scroll-inertia-complete-coverage.md.
Boundary drop alone is half the protection. A flick that never reaches a boundary
— mid-range inertia over a long card — still delivers one event per notch, and
each accepted one is an Update + View repaint. tty.WheelBurst /
tty.WheelBursts (internal/tty/wheel.go) coalesce a flick into whole flushes:
"how much of this burst has the surface earned" lives in that one place, so a
flick travels the same distance on every surface.
Rule: every new scrollable content area applies wheel deltas through the shared burst guard and drops at boundaries — both, not either.
How to comply:
tty.WheelBurst per scroll surface. A host with one surface embeds
the value in whatever struct owns it (internal/overview/preview.go,
workspace issuePane); a host with several uses tty.WheelBursts keyed per
leaf/region (Files tree + preview, app content deck).Add(delta, now); when ok is false the event was held, change
nothing, and return early. The held delta rides the next flush — never apply
delta directly.wheelNow func() time.Time field (Files), the
plugin clock (p.now()), or a Model hook (notificationCentreNow). Tests
drive a whole flick without sleeping — see
internal/plugins/filebrowser/scroll_burst_test.go and the issue-viewer
equivalents beside each host.WheelBursts.For resets automatically.Reference adoptions: embedded terminals (internal/tty/tty.go), Files tree and
preview (internal/plugins/filebrowser), workspace terminal + issue panes,
overview terminal + issue preview, app content deck + notification centre +
issue preview modal.
type Plugin struct {
mouseHandler *mouse.Handler
}
func New() *Plugin {
return &Plugin{mouseHandler: mouse.NewHandler()}
}
func (p *Plugin) View(width, height int) string {
p.mouseHandler.Clear()
p.mouseHandler.HitMap.AddRect("pane", 0, 0, width, height, nil)
p.mouseHandler.HitMap.AddRect("item", 2, 5, width-4, 1, 0)
return content
}
Regions tested in reverse order. Add general regions first, specific regions last.
A pane header's right edge carries two controls: the layout button ⊞ (U+229E) and the close ×. Both are drawn through ui.ResolveButtonStyle with the same one-cell padding, so a pane header never invents a third button look, and both are gated on the pane_move feature for the layout button's half.
Reserve, do not measure. ui.ReserveHeaderControls(width, controls ...HeaderControl) returns the tab strip's usable width and each control's column in one call; panereposition.ReserveMovableHeader(width, movable, hasClose) is the pane-side wrapper. Each host binds it once per frame in its own reserveHeader/composeHeader pair (workspace.Plugin, overview.Model, app.appContentDeck) so its header renderer, its tab strips and its region sink all measure from one answer — a strip laid out for a different reserve than the header composes is how a tab click lands on the wrong tab.
movable comes from the tree, via panereposition.Movable: it is false with no tree and false for a tree of one leaf. PlanMove refuses every destination on a single leaf, so a control there is a target the user aims at for nothing, and a header with no leaf at all used to compare hover against leaf 0 — which every un-hovered header matches — and paint a permanently hovered button. ui.ReserveHeaderClose remains a one-line compatibility wrapper for headers that carry only the ×. The arithmetic assumes a one-column glyph and the test suite pins ansi.StringWidth of the rendered label; a wider glyph silently shifts every hit region on the row.
Drop order as the row narrows: the layout button goes first, the close × last. A control is dropped all-or-nothing. A half-drawn control is a target whose meaning cannot be recovered, and the close button is the one a user cannot work around.
The Layout region rung is after Title and before Close. paneframe.RegionSink registers Layout(node *panelayout.Node, hit Box) at that rung for the same reason RegisterRegions documents for the close button, one step earlier: regions are tested in reverse order, so the later-registered Close wins the cell it actually occupies while Layout still outranks the title strip underneath it. All three pane hosts implement it beside their close-region binding, with the same hover tracking.
The button opens panereposition.Controller — the same modal M opens. See .claude/skills/keyboard-shortcuts/SKILL.md for the key and its target resolution, and .claude/skills/drag-pane/SKILL.md for where the region sits in the windowing model.
App offsets Y by headerHeight (the single painted header row) before forwarding to plugins. Plugins operate in local coords where Y=0 is plugin content top.
| Symptom | Fix |
|---|---|
| Clicks don't register | Check region order (pane first) |
| Y offsets wrong | Account for borders, padding, headers |
| Scroll over items broken | Include item regions in scroll routing |
| Double-click fails | Ensure consistent region ID/bounds |
| Drag broken | Call StartDrag on click, check DragRegion during drag |
Automatically, when a task matches. Use when implementing UI features, handling user input, adding keyboard shortcuts, building modals, or working on UX improvements.
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.