Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reference for keyboard shortcut implementation, keybinding registration, shortcut parity with vim and other TUI tools, and the complete shortcut assignment table across all sidecar plugins.
SkillScore80/100
Test results 80
3/3 safety checks passed
20%80%of test cases passed +60 pts
Passed 20 of 25 test cases with the skill, and 5 of 25 without it.
15 fixed5 passed either way5 still failing0 broke
One test taskWhen operating inside Sidecar's interactive terminal mode (workspace-interactive), which key combination exits interactive mode and returns focus to the workspace list?
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add marcus-keyboard-shortcuts
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-keyboard-shortcuts")
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-keyboard-shortcutsThe 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-keyboard-shortcuts/SKILL.md https://app.decimal.ai/s/marcus-keyboard-shortcuts/SKILL.md.claude/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsimport 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsfrom 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsimport 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsimport 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsfrom 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsfrom 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsimport 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-keyboard-shortcuts")
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-keyboard-shortcuts/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-keyboard-shortcutsfrom 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsfrom 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsimport 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsimport 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsfrom 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-keyboard-shortcuts")
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-keyboard-shortcuts/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcutsNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/marcus-keyboard-shortcuts/SKILL.md https://app.decimal.ai/s/marcus-keyboard-shortcuts/SKILL.md.agents/skills/marcus-keyboard-shortcuts/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-keyboard-shortcuts/SKILL.md · /s/marcus-keyboard-shortcuts.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-keyboard-shortcuts/SKILL.mdcurl -fsSL https://app.decimal.ai/s/marcus-keyboard-shortcuts.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.
Complete shortcut listings and context reference for all sidecar plugins. For implementation patterns, see docs/guides/deprecated/ui-feature-guide.md. For a detailed assessment of inconsistencies, vim alignment, mnemonic quality, and improvement proposals, see references/assessment.md in this skill directory.
internal/keymap/bindings.go is the single source of truth for key bindings.?): Auto-discovers bindings for discoverability.~/.config/sidecar/config.json.g g are supported with 500ms timeout.internal/keymap/bindings.go under the appropriate context.Update() method (usually in a handlers.go file).Commands() method for footer hint and command palette.TD shortcuts are dynamically exported from TD itself via ExportBindings() and ExportCommands() in pkg/monitor/keymap/. TD is the single source of truth. To add TD shortcuts:
pkg/monitor/keymap/bindings.gopkg/monitor/keymap/registry.gopkg/monitor/keymap/export.gopkg/monitor/model.go| Key | Command | Description |
|---|---|---|
j / down | cursor-down | Move cursor down |
k / up | cursor-up | Move cursor up |
G | cursor-bottom | Jump to bottom |
g g | cursor-top | Jump to top |
ctrl+d | page-down | Page down |
ctrl+u | page-up | Page up |
enter | select | Select item |
esc | back | Go back / close |
` | next-plugin | Next header entry |
~ | prev-plugin | Previous header entry |
] | next-plugin | Next header entry |
[ | prev-plugin | Previous header entry |
1-7 | focus-plugin-N | Focus the Nth project tab (positional; stops at 7) |
8 | focus-sessions | Sessions (global) |
9 | focus-activity | Activity (global) |
0 | focus-tasks | Tasks (global; no-op when the Tasks host is disabled) |
? | toggle-palette | Command palette |
! | toggle-diagnostics | Diagnostics overlay |
@ | switch-project | Project switcher |
W | switch-worktree | Worktree switcher |
# | switch-theme | Theme switcher |
, | open-configuration | Open Configuration (contexts that bind , win) |
i | open-issue | Open issue |
r | refresh | Refresh |
q | quit | Quit (root contexts only) |
ctrl+c | quit | Force quit |
Sidecar's header is a single row of entries: the global ones in the left cluster (Sessions, Activity, and Tasks when its feature is on) followed by the project's plugin tabs on the right.
[ / ] (and their ~ / ` aliases) wrap through all of it, in that
order, and the ring is identical from either scope — the project tabs are
painted only in project scope, but they stay in the ring from the global space
so the cycle is never a trap and ] then [ is always the identity. Tasks is
absent from the ring whenever its feature is off.
The number row addresses the same row, but by two different rules:
1-7 are positional project tabs. They stop at 7. An eighth plugin tab
is reached with [ / ] or from the command palette.8 / 9 / 0 are named global entries — Sessions, Activity, Tasks — and
mean the same thing in every scope. A key whose entry is disabled (0 with
the Tasks host off) does nothing at all, silently; it never falls through to a
plugin tab.All ten digits and the four cycling keys are in keymap.GlobalKeys, so no
plugin may claim them, and all of them yield to a focused text input.
config / config-edit / config-confirm contexts)Opened with , or by clicking the header gear, always on Sidecar Setup. Like the Overview,
Configuration covers the plugin pane and owns keyboard focus: unhandled keys are swallowed
rather than leaking to the hidden plugin. ? still opens the command palette.
| Key | Command | Context | Action |
|---|---|---|---|
j / k / up / down | cursor-down / cursor-up | config | Move through sidebar destinations |
enter | select | config | Open the selected destination |
/ | search | config | Focus Search (enters config-edit) |
tab | focus-search | config | Move focus between sidebar and Search |
esc | close-configuration | config | Return from a child route, else close and restore the prior surface |
down | first-result | config-edit | Move from Search to the first visible result |
up | focus-search | config-edit | Return to Search from the first result |
esc | clear-search | config-edit | Clear the query and restore the full sidebar |
enter / y | confirm | config-confirm | Confirm a consequential change |
esc / n | cancel | config-confirm | Cancel it |
overview context)Opened with K or by clicking the Sidecar logo. The Overview covers the plugin pane and
owns keyboard focus: a plugin left in interactive/text-input mode underneath it (embedded
shell, inline editor) does not receive keys while it is open, and unhandled keys are
swallowed rather than leaking to the hidden plugin.
| Key | Action |
|---|---|
h / l / left / right | Move between lanes |
j / k / up / down | Move within a lane |
enter | Open the selected workspace (switches project) |
r | Refresh the board |
esc / K | Close the Overview |
q | Quit Sidecar (confirmation modal) |
Global shortcuts stay live while it is open: `/~, [/], 1-9, @, #, W, ?, !,
^, i, ctrl+c, q. Plugin-switching keys (`, ~, 1-9) close the Overview first.
esc on the Agents board or Workspaces list leaves the global space. q opens Sidecar's quit modal.
Contexts: global-workspaces (list, root), global-workspaces-filter, global-workspaces-rename, global-workspaces-create, global-workspaces-delete, global-workspaces-terminal (typing), global-workspaces-doc, global-workspaces-doc-search, global-workspaces-doc-find, global-workspaces-issue, global-workspaces-diff.
There is no watched-preview focus: hiding the sidebar is layout only. l / → do not move focus to the preview. Clicking a file or td id focuses a content leaf with its own context; footer, help, and the palette follow WorkspaceFocusContext().
| Key | Action |
|---|---|
j / k / arrows / g / G | Move selection; preview follows; not typing |
enter / E | Start typing in the selected live pane. A dead row stays put |
| click in pane | Start typing. Clicking Diff/Task action chips opens a leaf and does not type |
| click a file tab | Select that file in the document preview. { / } also cycle when the document is focused |
| click an issue tab | Select that issue in the issue preview. { / } also cycle when the issue is focused |
| click a list row | Select it; preview follows; not typing |
| click another row while typing | Switch session and stay typing |
| double-click a row | Open that identity in its owning project |
| wheel on terminal | Scroll only; do not activate |
ctrl+\ / esc esc | Stop typing and land on the list |
ctrl+shift+f while typing | Search the complete terminal history |
i | Find TD task (open-issue). Not interactive |
n | Open Create Workspace (Worktree) |
ctrl+n | Open Create Workspace with Shell selected (modal) |
D | Delete the selected shell (shown only for shell rows) |
m | Open the owning project's established merge strategy workflow for a safe worktree |
/ | Filter |
v / s | Open View: sort the list. v matches the project sidebar; s is the original alias |
\ | Toggle sidebar |
esc | Leave the global space (or clear the filter first) |
q | Quit Sidecar (confirmation modal) |
K | Toggle the global space |
M | Open the reposition modal on the focused pane, or on the selected row's Primary terminal from the list (pane_move) |
B | Open Broadcast to agents (broadcast-agents, agent_control) |
ctrl+] attach stays project-only and is off unless tmux_full_attach is enabled. While typing, i and q go to the pane.
global-workspaces-doc)Same keys as the project document pane: q/esc close, x close tab,
{/} cycle tabs, m toggle render, Y yank path — and the same three
searches, all rooted at the pane's own directory: / in-file search,
ctrl+p file finder, f project search. The finder and project search are
internal/panesearch; the in-file bar is internal/docview. Contexts while
one is up: global-workspaces-doc-search and global-workspaces-doc-find.
global-workspaces-issue)An unmodified click on a td-… link opens it beside the terminal. A
second click appends a tab; an already-open ID is focused. Click a
drawn tab to select it. The header is only the tab strip: ID plus
headline, truncated at the end so the ID stays visible. Footer hints
are Tab× Tab← Tab→. There is no close chip.
enter on a parent or subtask uses the same open-or-focus path (no
duplicate, no silent replace). Tabs stay in memory for the selected
row and are not written to disk.
| Key | Command | Description |
|---|---|---|
enter | open-item | Open or focus the selected parent or subtask as a tab |
O | open-in-td | Open the selected issue in td (same jump as the preview modal's o) |
x | close-tab | Close the active tab. Last tab closes the pane and forgets the set |
{ / } | prev-tab / next-tab | Previous / next issue tab |
y | yank-issue | Copy issue as markdown |
Y | yank-issue-key | Copy issue ID |
q / esc | close | Close the pane and forget this row's in-memory tabs |
workspace-issue)Same open/append/click/cycle/close journey and the same { / } / x
/ yank / enter keys. tab / shift+tab cycle panes; \ toggles the
sidebar. q / esc hide the pane and retain tabs for that surface;
last x forgets. Switching shells or relaunching restores tabs, the
active tab, and each tab's scroll.
| Key | Command | Description |
|---|---|---|
enter | open-item | Open or focus the selected parent or subtask as a tab |
O | open-in-td | Open the selected issue in td (same jump as the preview modal's o) |
x | close-tab | Close the active tab. Last tab forgets the pane |
{ / } | prev-tab / next-tab | Previous / next issue tab |
y | yank-issue | Copy issue as markdown |
Y | yank-issue-key | Copy issue ID |
tab / shift+tab | next-pane / prev-pane | Move focus between sidebar, terminal, document, and issue |
\ | toggle-sidebar | Toggle sidebar visibility |
q / esc | close | Hide the pane. Tabs stay remembered for this surface |
| Key | Action |
|---|---|
Tab / Shift+Tab | Switch focus between panes |
\ | Toggle sidebar visibility |
h / left | Focus left pane |
l / right | Focus right pane |
+ | Grow sidebar width |
- | Shrink sidebar width |
| Context | View |
|---|---|
git-status | File list (root) |
git-status-commits | Recent commits sidebar (root) |
git-status-diff | Inline diff pane (root) |
git-commit-preview | Commit detail in right pane |
git-diff | Full-screen diff |
git-commit | Commit editor |
git-push-menu | Push strategy selection |
git-pull-menu | Pull strategy selection |
git-pull-conflict | Conflict resolution |
git-history | Commit history |
git-commit-detail | Single commit view |
| Key | Command | Description |
|---|---|---|
s | stage-file | Stage selected file |
u | unstage-file | Unstage selected file |
S | stage-all | Stage all modified |
U | unstage-all | Unstage all |
c | commit | Open commit editor |
A | amend | Amend last commit |
d / enter | show-diff | View file changes |
D | discard-changes | Discard unstaged changes |
h | show-history | Open commit history |
P | push | Open push menu |
L | pull | Open pull menu |
f | fetch | Fetch from remote |
b | branch | Branch operations |
z | stash | Stash changes |
Z | stash-pop | Pop stash |
ctrl+z | stash-apply | Stash apply |
o | open-in-github | Open in GitHub |
O | open-in-file-browser | Open in file browser |
y | yank-file | Copy file info |
Y | yank-path | Copy file path |
git-status-diff)The right-hand pane of the Git tab, focused with enter / l from the file
list.
| Key | Command | Description |
|---|---|---|
j / k | scroll-down / scroll-up | Scroll the diff |
ctrl+d / ctrl+u | page-down / page-up | Scroll half a page |
g / G | — | Jump to start (also resets the horizontal axis) / end |
h / l | — | Scroll horizontally; h at column 0 returns to the sidebar |
| | reset-hscroll | Snap the horizontal scroll back to column 0 |
enter | full-diff | Open the full-screen diff |
s / u | stage-file / unstage-file | Stage / unstage the file |
v | toggle-diff-view | Cycle unified → split → full-file |
w | toggle-wrap | Toggle line wrap |
\ | toggle-sidebar | Toggle the sidebar |
+ / - | resize-pane-grow / resize-pane-shrink | Resize the split |
| is vim's goto-column key. It reads as the odd choice next to vim's 0, and
0 is what this pane used to bind — but the whole number row belongs to the
header (see "The header row is one ring"), so 0 never reaches the plugin. It
was a live handler that had quietly stopped being reachable; | is the
replacement, and unlike 0 it is registered, so it appears in the footer and
in ?.
git-diff)| Key | Command | Description |
|---|---|---|
, / . | prev-file / next-file | Previous / next changed file |
s / u | stage-file / unstage-file | Stage / unstage the file on screen |
v | toggle-diff-view | Cycle the diff view mode |
w | toggle-wrap | Toggle line wrap |
y | yank-diff | Copy the diff |
c | commit | Open the commit editor |
q / esc | close-diff | Leave the diff |
This view has no tabs, so { / } are deliberately unbound here rather than
made to mean "next file" — that would be the one place in Sidecar where a brace
did something other than cycle tabs, and a silent wrong action is worse than a
no-op. File stepping is , / ., the same as in the Workspaces Diff pane.
| Key | Command | Description |
|---|---|---|
enter / d | view-commit | Open commit details |
h | show-history | Open history view |
y | yank-commit | Copy commit as markdown |
Y | yank-id | Copy commit hash |
/ | search-history | Search commit messages |
f | filter-author | Filter by author |
p | filter-path | Filter by path |
F | clear-filter | Clear filters |
n | next-match | Next search match |
N | prev-match | Previous match |
o | open-in-github | Open commit in GitHub |
v | toggle-graph | Toggle commit graph |
| Key | Command |
|---|---|
p | pull-merge |
r | pull-rebase |
f | pull-ff-only |
a | pull-autostash |
| Context | View |
|---|---|
file-browser-tree | Tree view (root) |
file-browser-preview | Preview pane |
file-browser-search | Filename search |
file-browser-content-search | Content search |
file-browser-quick-open | Fuzzy file finder |
file-browser-project-search | Ripgrep search modal |
file-browser-file-op | File operation input |
file-browser-inline-edit | Inline vim editor (all keys forwarded, global shortcuts bypassed) |
| Key | Command | Description |
|---|---|---|
/ | search | Filter files by name |
ctrl+p | quick-open | Find — a file by name (same key, same name as a workspace file pane) |
f | project-search | Search — the project's contents (ripgrep) |
/ (preview) | search-content | InFile — this file's contents |
a | create-file | Create new file |
A | create-dir | Create new directory |
d | delete | Delete (with confirmation) |
t | new-tab | Open in new tab |
{ | prev-tab | Previous tab |
} | next-tab | Next tab |
x | close-tab | Close active tab |
y | yank | Copy to clipboard |
p | paste | Paste from clipboard |
s | sort | Cycle sort mode |
m | move | Move file/directory |
R | rename | Rename |
ctrl+r | reveal | Reveal in file manager |
A file opened in an app content pane beside Files uses the shared workspace-doc document context. It keeps the same file-facing shortcuts as the primary Files preview wherever the shared viewer owns the capability: / InFile, ctrl+p Find, f Search, e inline Edit, E external Editor, r Reload, m Render, w Wrap, I Info, ctrl+r Reveal, y Contents, Y Path, configured selection copy/select-all, { / } tabs, and + / - resize. Its finder and project search are the same internal/panesearch surfaces used by project and global Workspace document panes, rooted at that content deck's project and loading results back into the focused pane. Files-only tree operations such as rename and its full-screen blame mode remain owned by the primary Files surface; they are not document-viewer shortcuts and are not forwarded to a potentially different file behind the focused pane.
| Context | View |
|---|---|
conversations | Session list single-pane (root) |
conversations-sidebar | Session list two-pane (root) |
conversations-main | Messages pane |
conversations-search | Search mode |
conversations-filter | Adapter filter |
conversation-detail | Turn list |
message-detail | Single turn content |
analytics | Usage stats |
| Context | View |
|---|---|
workspace-list | Workspace list (root) |
workspace-preview | Preview pane |
workspace-doc | File tabs beside the terminal (hide with q) |
workspace-doc-search | A pane's file finder / project search (owns the keyboard) |
workspace-doc-find | A pane's in-file search bar (owns the keyboard) |
workspace-doc-edit | A pane's inline editor (owns every key, ctrl+c included) |
workspace-issue | Issue tabs beside the terminal (hide with q; last x forgets) |
workspace-diff | Diff tabs beside the terminal (hide with q; last x forgets) |
workspace-create | Create Workspace form |
workspace-task-link | Task selection modal |
workspace-merge | Merge workflow modal |
workspace-interactive | Embedded terminal |
| Key | Command | Description |
|---|---|---|
n | new-workspace | Open Create Workspace (Worktree) |
ctrl+n | new-shell | Create a new shell immediately (shadows the global ctrl+n cursor-down in this context) |
v | open-view | Open View: sort the list (Manual, Activity, Recent, Name) |
V | toggle-view | Toggle list/kanban |
D | delete-workspace | Delete workspace / delete shell (confirm) |
d | show-diff | Open working-tree Diff leaf |
p | push | Push branch |
m | merge-workflow | Start merge workflow |
T | link-task | Link/unlink task |
s | start-agent | Start agent |
enter / E | interactive | Enter interactive mode |
i | open-issue | Find TD task (global; not interactive) |
t | attach | Full tmux attach (tmux_full_attach, default off) |
S | stop-agent | Stop agent |
F | find-file | Open a file pane on the fuzzy file finder |
P | fetch-pr | Fetch a remote PR as a workspace |
M | move-pane | Open the reposition modal on the selected row's Primary terminal (pane_move) |
B | broadcast-agents | Open Broadcast to agents (agent_control) |
| Key | Command | Action |
|---|---|---|
o | open-pane | Open the pane switcher (kind list focused) without leaving the preview |
d | show-diff | Open working-tree Diff leaf |
ctrl+t | toggle-terminal | Toggle a terminal split beside the preview |
M | move-pane | Open the reposition modal on the focused pane (pane_move) |
n opens the pane switcher whenever a content pane has focus — Document,
Issue, Note, Diff or Resource — on both the project workspace
(workspace-doc|issue|note|diff|resource) and the global Workspaces browser
(global-workspaces-doc|issue|note|diff|resource). Every content pane absorbs
the keys it does not own, so without this the switcher was reachable only from
the sidebar or the terminal: putting a second pane beside the one you were
reading meant leaving it first.
n is the same key the sidebar and the terminal preview already answer with
"make me a new thing", so the answer does not change with focus. Two
consequences follow, and both are deliberate:
n / N next-change pair moved to > / < — the
shifted forms of its , / . file steps, so the pair reads as one
hierarchy: step a file, shift to step a change inside it. One key means one
thing in every pane; a key that meant next-change here and "new pane"
everywhere else is exactly the drift this codebase refuses.n for its next-match while it is up, as does the doc editor
and the finder overlay; the switcher is asked only after they decline.The terminal preview keeps o rather than n, because n there belongs to
the list's create. internal/keymap's parity tests hold both surfaces to the
same key in the same contexts.
ctrl+nThe same entry exists in the ordinary plugins, under ctrl+n rather than n.
Every plugin that has a create already spends n on it — new-note,
next-match — so displacing those would be the drift the Workspaces half
deliberately avoided.
It is bound in one place for all five plugins: internal/app/pane_switcher.go,
not once per plugin. The deck the switcher opens into is the app's
(internal/app/content_deck.go) and so is the key routing, so a plugin opts in
simply by being deck-eligible — implementing plugin.ContentLinkProvider and
plugin.PaneFocusProvider, with features.PluginContentPanes on. Today that is
file-browser, git-status, notes, tasks and td-monitor; workspace is
excluded because it owns its own pane tree.
Three rules decide where the key appears, and none of them is a per-plugin list:
paneSwitcherKeyFor) rather than
comparing against a constant. A context that never names open-pane never
reaches the switcher, and a user who rebinds open-pane moves the key and
its footer hint together.ctrl+n is cursor-down in the global
context and in every filter, finder, search and editor context. Claiming it
where it already walks a list would take the cursor out from under someone who
is typing, so those contexts keep it. See the assignment table below.n, not ctrl+n. A leaf inside a plugin's
deck reports the same workspace-doc|issue|note|diff|resource context the two
Workspaces surfaces report for the same pane, so it answers the same key
there. One model, three projections, one key each.The switcher offers pane kinds only outside Workspaces — File, Git diff,
td issue, Note, one row per configured resource provider — and is titled
"Open Pane" rather than "Create Workspace". Shell and Worktree create workspace
rows, which a Notes plugin has nowhere to put; Terminal split is absent because
a plugin deck is a passive contentpanes deck with no live-leaf host. That is
workspacecreate.OpenOpts.PaneKindsOnly plus AllowTerminalSplit: false — the
same data-driven catalog, one more flag, no second modal.
See docs/plans/active/pane-switcher-everywhere.md.
ctrl+n goes, in fullEvery context that binds ctrl+n in keymap.DefaultBindings(), and to what.
internal/keymap/pane_switcher_parity_test.go holds this table to the tree.
| Context | Command | Why |
|---|---|---|
global | cursor-down | The emacs/readline default. Everything below either shadows it or inherits it. |
notes-list | open-pane | Notes' browse context. |
file-browser-tree, file-browser-preview | open-pane | The File Browser's two browse contexts. |
git-status, git-status-commits, git-status-diff, git-diff, git-commit-preview | open-pane | Git names both the focused pane and the cursor's row in its context, so reading a file row, reading a commit and reading a diff are three contexts. |
tasks-list, tasks-detail, tasks-response, tasks-response-detail | open-pane | Exactly Tasks' four root contexts; everything else Tasks reports is an overlay it owns the keyboard in. |
td-monitor, td-board, td-kanban | open-pane | td's three browse views: the main list, board mode, the kanban view. |
global-workspaces, workspace-list | new-shell | The Workspaces lists' second create. This is the precedent ctrl+n follows here — one modifier out from n, same intent. |
global-workspaces-filter, project-switcher | cursor-down | Filters. The key walks the list while you type. |
file-browser-quick-open, file-browser-project-search | cursor-down | Finders. |
notes-search, notes-editor | cursor-down | Search and the built-in editor. |
Contexts deliberately without a row, and the reason each is different:
notes-preview — the note preview answers ctrl+n/ctrl+p as its own cursor
motion in plugin code (notes.handleEditorPreviewKey), with nothing in
bindings.go to show for it. The rule stands aside; Notes stays reachable
from notes-list.td-modal and its sub-focus states (td-epic-tasks, td-parent-epic,
td-blocked-by-focused, td-blocks-focused) — tdmonitor.BlocksGlobalKeys
hands every key in that context to the embedded td model at precedence level
2, two rungs above the switcher's, so a binding there would never fire. This
is the one genuinely wanted pane the entry does not reach.td-search, td-form, td-board-editor, td-confirm, td-close-confirm,
and every non-root tasks-* context — text input or an overlay the plugin
forwards wholesale.git-history — has bindings in bindings.go but gitstatus.FocusContext()
never reports it. A binding on a context nobody stands in is a key that does
nothing.ctrl+n is a real control character:
tty.MapKeyToTmux encodes it as C-n and sends it to the pane. The tty
layer's own chords are ctrl+\ (exit), ctrl+] (attach), alt+c (copy),
alt+v (paste), ctrl+a (select all) and the platform copy chord; its
scrollback set is the arrows plus the j/k/g/G/ctrl+d/ctrl+u pager aliases.
The two host OnKey hooks claim only terminal search and ctrl+t. Above all
of that, workspace-interactive, file-browser-inline-edit,
notes-inline-edit and workspace-doc-edit forward every key two rungs
before the switcher's is reached.g / G jump to the top / bottom of the preview's scrollback. 0 is
deliberately not bound here: it is the header's global Tasks shortcut, and a
context-local binding would make the same key mean two different things one tab
apart. (It previously carried a reset-scroll command that had no handler
anywhere in the tree.)
M Opens the Reposition Modal From Every PaneM (move-pane, feature pane_move, default on) opens the shared pane reposition modal — the same modal the pane header's ⊞ button opens, and the same one for all three pane hosts. It is an entry point, not a second interaction: M never mutates the live tree by itself. Inside the modal, h/j/k/l and the arrows edit a draft, z toggles zoom, enter commits the whole sequence atomically, and esc discards it.
Which pane it targets depends on which window owns the keyboard:
M targets that leaf.workspace-list, and global-workspaces, which the Sessions surface also reports for a focused Primary terminal) M targets the selected row's Primary terminal. Host focus decides which of the two the shared global-workspaces context means.Bound contexts (all feature-gated on pane_move): workspace-list, workspace-preview, workspace-doc, workspace-issue, workspace-note, workspace-diff, workspace-resource, global-workspaces, global-workspaces-doc, global-workspaces-issue, global-workspaces-note, global-workspaces-diff, global-workspaces-resource. The app content decks report the same workspace-* context names for their own leaves and inherit these bindings.
Not bound in text-input, interactive-terminal, modal, or unrelated plugin browse contexts — workspace-filter, workspace-interactive, workspace-doc-edit|search|find, global-workspaces-filter, global-workspaces-terminal, file-browser-tree, git-status, notes-list, and the rest. Those contexts keep every printable key for themselves, and internal/keymap/pane_move_parity_test.go holds them to it.
Why M and not m. m is free in ten of the twelve pane contexts, and taken in exactly the two that would break parity: global-workspaces-doc spends it on render, and global-workspaces (what a focused Primary terminal reports on the Sessions surface) spends it on merge-workflow. One key must mean one thing in every pane, so a key that works in ten and not the other two is not a candidate. M is bound nowhere else except git-status (stash-pop), a plugin browse context that is never a pane leaf.
A pane with nowhere to go has no entry. A tree with a single leaf offers neither the key's modal nor the header's ⊞: PlanMove refuses every destination on it, so both would open onto a layout that cannot change. The button appears when a second pane does.
The agent's half of the same capability is sidecar layout move over the same planner, with --to left|right|up|down compiling through the identical direction rule. See .claude/skills/ui-features/SKILL.md and docs/reference/cli.md.
| Key | Command |
|---|---|
ctrl+\ | exit |
ctrl+] | attach (tmux_full_attach, default off) |
ctrl+t | toggle a terminal split beside the preview (workspace_terminal_panel, default on) |
ctrl+shift+f | search complete terminal history |
alt+c | copy |
super+c | copy (Cmd+C, when the emulator passes it through) |
alt+v | paste |
An unmodified click on a resolvable file path in workspace or shell terminal
output opens it beside that terminal. A second click appends a tab; a path
that is already open is focused (and path:line jumps). Click a drawn tab
to select it. The header is only the tab strip: each label is the relative
path, left-truncated so the filename end always wins. Document panes are
enabled by default; --disable-feature=workspace_doc_panes opts out for a
launch and also disables Diff (no pane tree). shift-drag and alt-drag remain terminal selection gestures.
The global Workspaces view uses the same tab strip and the same click / {
/ } / x keys. Those tabs stay in memory for the selected row.
q / esc hide the pane and remember the tab set for this shell or
workspace. x on the last tab forgets the set. Switching surfaces or
relaunching onto the same surface restores open files, the active tab,
render mode, wrap, scroll, and split ratio. , / . cycle Diff target
tabs only while a Diff leaf is focused; they do not cycle document tabs.
| Key | Command | Description |
|---|---|---|
j / down | scroll-down | Scroll down |
k / up | scroll-up | Scroll up |
ctrl+d / ctrl+u | page-down / page-up | Scroll half a page |
g / G | cursor-top / cursor-bottom | Jump to start / end |
/ | search-content | Search within this file (in-pane bar; same feature as the Files plugin's /) |
e | edit | Edit this file inline (tmux PTY editor in the pane body; features.tmux_inline_edit) |
ctrl+p | find-file | Find a file by name in this pane (modal scoped to the pane) |
f | search-project | Search the project in this pane (modal scoped to the pane) |
x | close-tab | Close the active tab. Last tab closes the pane and forgets the set |
{ / } | prev-tab / next-tab | Previous / next file tab |
m | render | Toggle rendered/raw markdown (markdown only; no-op otherwise) |
w | toggle-wrap | Toggle line wrap |
I | info | File info modal |
ctrl+r | reveal | Reveal in the OS file manager |
Y | yank-path | Copy the relative path |
+ / - | resize-pane-grow / resize-pane-shrink | Resize the workspace split |
tab / shift+tab | next-pane / prev-pane | Move focus between sidebar, terminal, and document |
q / esc | close | Hide the pane. Tabs stay remembered for this surface |
While a pane search is open (workspace-doc-search) it owns every key in
the pane: esc closes it, enter loads the hit in the active tab, and
shift+enter opens it in a new tab.
Inline edit (e) opens the same tmux-PTY editor the Files plugin uses, sized
to the pane body, on both pane surfaces (workspace-doc-edit and the global
browser's document pane). While a session is live every key is the editor's —
ctrl+\ or esc esc exit it — and clicking outside the pane raises the
save / discard / cancel confirmation instead of leaving the buffer behind.
In-file search (/) is a third surface, drawn by internal/docview as one
row inside the pane, and it owns every key while it is up
(workspace-doc-find, global-workspaces-doc-find): enter commits,
n / N step matches, esc closes. It dismisses when the pane loses focus.
While it is up it also keeps n away from the pane switcher, which is asked
only after the pane's own input surfaces decline.
d / show-diff on the Workspaces list or preview opens a working-tree Diff
leaf beside the terminal. The leaf is not a root context: q / esc hide it.
{ / } cycle Diff target tabs while the leaf is focused; , / . step
next/prev file inside the view, and > / < step next/prev change inside a
file (this pair was n / N until the pane switcher took n in every content
pane).
One rule everywhere: { / } is always "cycle the tabs of the thing I am
looking at." Document, issue, File Browser and Diff leaves all obey it. The
Diff pane is the only surface with a second navigation axis — the files inside
the active target — and that axis gets its own pair, , / ..
The earlier arrangement was the reverse ({ / } = file, , / . = tab), on
the reasoning that in-view file jumping is the more frequent act in a diff and
so deserved the better-known keys. That reasoning was sound in isolation and
wrong in aggregate: it made the diff the one place in Sidecar where } did not
mean "next tab", so the cost was paid on every context switch into and out of
the diff, by everyone, forever — while the benefit accrued only inside the diff.
Consistency of a key's meaning across surfaces beats optimality of its
assignment on one surface. If you are tempted to re-optimise a key for a
single pane again, that is the trade to weigh.
| Key | Command | Description |
|---|---|---|
d | show-diff | Open working-tree Diff leaf (list and preview) |
q / esc | close | Hide the pane. Tabs stay remembered for this surface |
x | close-tab | Close the active tab. Last tab forgets the pane |
{ / } | prev-tab / next-tab | Previous / next Diff target tab |
, / . | prev-file / next-file | Previous / next file in this target |
Y | yank-id | Copy the target identity (wt / c:… / r:…) |
tab / shift+tab | next-pane / prev-pane | Move focus between sidebar, terminal, and content |
\ | toggle-sidebar | Toggle sidebar visibility |
Moving around inside the viewer. These are the shared viewer's own keys
(internal/workspacediff/keys.go); they are registered in bindings.go for both
workspace-diff and global-workspaces-diff so the footer, help and palette show
them, and the viewer answers them before keymap dispatch.
| Key | Command | Description |
|---|---|---|
l / right / enter | diff-open | Open the selected file's diff, or a commit's file list |
j / down | diff-down / diff-scroll-down | Next item in the list; scroll the diff body |
k / up | diff-up / diff-scroll-up | Previous item; scroll the diff body up |
h / left | diff-back | Back to the file list (scrolls sideways first when panned) |
g / G | diff-top / diff-bottom | Jump to the top / bottom |
ctrl+d / pgdown | diff-page-down | Page down |
ctrl+u / pgup | diff-page-up | Page up |
v | toggle-diff-view | Cycle unified → side-by-side → full-file |
z | toggle-diff-scope | Cycle working tree → commits → aggregate |
> / < | diff-next-change | Next / previous change (full-file mode) |
f | file-picker | Open the file picker (project pane only) |
An unmodified click on a td-… link in workspace or shell terminal
output opens it beside that terminal. A second click appends a tab; an
ID that is already open is focused. Click a drawn tab to select it.
The header is only the tab strip: each label is the issue ID plus
headline, truncated at the end so the ID stays visible. There is no
close chip. Footer hints are Tab× Tab← Tab→.
The global Workspaces view uses the same tab strip and the same click /
{ / } / x keys. Those tabs stay in memory for the selected row
and are not written to disk. q / esc and last-x forget that row's
set.
In project Workspaces, q / esc hide the pane and remember the tab
set for this shell or workspace. x on the last tab forgets the set.
Switching surfaces or relaunching onto the same surface restores open
issues, the active tab, and each tab's scroll. Parent and subtask
enter uses the same open-or-focus path.
File tabs keep left-truncated paths so the filename survives. Issue tabs keep the ID visible and truncate the headline.
| Key | Command | Description |
|---|---|---|
j / k | scroll-down / scroll-up | Scroll down / up |
down / up | Walk parent, siblings, and subtasks | |
ctrl+d / ctrl+u | page-down / page-up | Scroll half a page |
g / G | cursor-top / cursor-bottom | Jump to start / end |
x | close-tab | Close the active tab. Last tab closes the pane and forgets the set |
{ / } | prev-tab / next-tab | Previous / next issue tab |
enter | open-item | Open or focus the selected parent or subtask as a tab |
y | yank-issue | Copy the issue as markdown |
Y | yank-issue-key | Copy the issue ID |
tab / shift+tab | next-pane / prev-pane | Move focus between sidebar, terminal, document, and issue |
q / esc | close | Hide the pane. Tabs stay remembered for this surface |
Contexts: notes-list (root), notes-preview, notes-editor, notes-search, notes-info, notes-task-modal, notes-delete-modal, notes-inline-edit.
The built-in editor is modeless: printable keys type. Selection and undo use modifier chords so v, u, y, d, x, and p stay letters.
notes-editor)| Key | Command | Description |
|---|---|---|
shift+arrows / shift+home / shift+end | select-* | Extend a source-coordinate selection |
alt+s | select-toggle | Set or clear the selection anchor; ordinary movement then extends |
alt+a | select-all | Select the whole note |
super+a | select-all | Select the whole note when the terminal delivers Cmd; alt+a is the portable advertised fallback |
super+up / super+down | note-start / note-end | Move to the start / end of the note when delivered |
shift+super+up / shift+super+down | select-note-start / select-note-end | Extend selection to the start / end of the note when delivered |
alt+c | copy-note | Copy the selection, or the whole note if none |
alt+x | cut | Cut the selection |
| backspace / delete / type / paste | — | Replace the selection (one undo unit) |
ctrl+z | undo-edit | Undo the last edit unit |
ctrl+y / ctrl+shift+z | redo-edit | Redo |
ctrl+s | save | Save now; after leaving edit, retry a failed save from list, preview, or search |
esc / tab | back / switch-pane | Leave edit and persist |
The tmux/$EDITOR pane (e) keeps vim's own undo and selection.
In the list and read-only preview, ctrl+y copies the selected note ID. In the
built-in editor it remains redo, and in the tmux/$EDITOR pane it is forwarded
unchanged. Enter and note-body clicks follow plugins.notes.defaultEditor;
i, e, and E remain explicit built-in, in-pane, and external editor paths.
Contexts: td-monitor (root), td-board, td-kanban, td-modal, td-stats, td-search, td-confirm, td-epic-tasks, td-parent-epic, td-handoffs.
Shortcuts are defined in TD's pkg/monitor/keymap/ and auto-exported. The
context names come from there too — td names its own context and
monitor/keymap.ContextToSidecar spells it for sidecar — so any sidecar-side
binding on a td-* context is a copy of someone else's constant.
tdmonitor.TestBrowseContextsCarryThePaneSwitcherEntry derives them from the
upstream constants so a rename in td fails a test here rather than silently
unbinding a key.
ctrl+n opens the pane switcher in td-monitor, td-board and td-kanban.
See "The Pane Switcher Is Reachable From Every Pane" for why td-modal is not
among them.
Contexts: tasks-list, tasks-detail, tasks-response, tasks-response-detail
(the four root contexts), plus every overlay Tasks reports —
tasks-filter, tasks-form, tasks-modal, tasks-picker, tasks-prompt,
tasks-task-edit, tasks-agent-activity and the rest.
Shortcuts are defined in Tasks' pkg/tui and auto-exported, and the context
names come from there verbatim (tasksui.FocusContext). Root-ness is sidecar's
own judgement (internal/plugins/tasks/routing.go) and it fails conservative:
anything unknown is treated as an overlay, so sidecar's global keys do not fire
underneath one.
ctrl+n opens the pane switcher in the four root contexts and nowhere else —
in an overlay, BlocksGlobalKeys has already handed the key to the tab.
| Key | Command |
|---|---|
@ | toggle |
down / ctrl+n | cursor-down |
up / ctrl+p | cursor-up |
Enter | select |
Esc | close |
Press ? to open. Press tab to toggle between current-context and all-contexts view.
| Key | Action |
|---|---|
j / k / up / down | Navigate |
ctrl+d / ctrl+u | Page down/up |
enter | Execute |
esc | Close |
tab | Toggle context filter |
Key conflicts exist across plugins (e.g., d = delete in file-browser, diff in git, delete-session in conversations). See references/assessment.md for the full inconsistency analysis, vim alignment audit, mnemonic analysis, and proposed improvement plan.
s/S: stage / stage-all (git)u/U: unstage / unstage-all (git)d/D: diff / discard (git), delete/- (file-browser)y/Y: yank item / yank pathn/N: next-match / prev-match (search contexts)Automatically, when a task matches. Use when adding or modifying keyboard shortcuts, checking shortcut assignments, resolving key conflicts, or assessing alignment with vim conventions.
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.