diff --git a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py
index ba1f729e..d8d41349 100644
--- a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py
+++ b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py
@@ -97,12 +97,11 @@ class SkillToolLoader(loader.ToolLoader):
if skill_data is None:
visible_skills = getattr(skill_mgr, 'skills', {})
available_names = ', '.join(sorted(visible_skills.keys())) or 'none'
- raise ValueError(
- f'Skill "{skill_name}" not found. Available skills: {available_names}'
- )
+ raise ValueError(f'Skill "{skill_name}" not found. Available skills: {available_names}')
# Register activated skill for sandbox mount path resolution
from . import skill as skill_loader
+
skill_loader.register_activated_skill(query, skill_data)
# Return SKILL.md content as Tool Result (injects into context)
@@ -112,17 +111,17 @@ class SkillToolLoader(loader.ToolLoader):
# Build Tool Result content
result_content = f'The "{skill_name}" skill is activated\n'
- result_content += f'\n'
+ result_content += '\n'
result_content += f'{skill_name}\n'
result_content += f'{mount_path}\n'
result_content += f'{package_root}\n'
result_content += f'\n## Instructions\n{instructions}\n'
- result_content += f'\n## Runtime Context\n'
+ result_content += '\n## Runtime Context\n'
result_content += f'The skill package is mounted at {mount_path}. Use the standard tools to interact with it:\n'
result_content += f'- Use `read` to inspect files under {mount_path}\n'
result_content += f'- Use `exec` with workdir set to {mount_path} to run commands in that package\n'
- result_content += f'- Use `write` and `edit` on that path when the instructions require updating files\n'
- result_content += f'\n'
+ result_content += '- Use `write` and `edit` on that path when the instructions require updating files\n'
+ result_content += '\n'
return {
'activated': True,
@@ -190,7 +189,7 @@ class SkillToolLoader(loader.ToolLoader):
if not normalized_path.startswith('/workspace'):
raise ValueError('path must be under /workspace')
- relative = normalized_path[len('/workspace'):].lstrip('/')
+ relative = normalized_path[len('/workspace') :].lstrip('/')
host_root = os.path.realpath(workspace_root)
host_path = os.path.realpath(os.path.join(host_root, relative))
@@ -213,7 +212,7 @@ class SkillToolLoader(loader.ToolLoader):
'properties': {
'skill_name': {
'type': 'string',
- 'description': 'The skill name to activate (no arguments). E.g., "pdf" or "create-skill"',
+ 'description': 'The skill name to activate (no arguments). E.g., "pdf" or "data-analysis"',
},
},
'required': ['skill_name'],
@@ -227,7 +226,7 @@ class SkillToolLoader(loader.ToolLoader):
name=REGISTER_SKILL_TOOL_NAME,
human_desc='Register a skill from sandbox',
description=(
- 'Register a skill package from a directory under /workspace into LangBot\'s skill store. '
+ "Register a skill package from a directory under /workspace into LangBot's skill store. "
'Use this after creating or preparing a skill in the sandbox with exec/read/write/edit. '
'The directory must contain a SKILL.md file. '
'After registration, the skill can be activated with the activate tool.'
@@ -276,15 +275,15 @@ class SkillToolLoader(loader.ToolLoader):
available_skills_lines = ['']
for skill_name, skill_data in sorted(skills.items()):
description = skill_data.get('description', '')
- available_skills_lines.append(f'')
+ available_skills_lines.append('')
available_skills_lines.append(f'{skill_name}')
available_skills_lines.append(f'{description}')
- available_skills_lines.append(f'')
+ available_skills_lines.append('')
available_skills_lines.append('')
available_skills_block = '\n'.join(available_skills_lines)
- return f'''Activate a skill within the main conversation.
+ return f"""Activate a skill within the main conversation.
When users ask you to perform tasks, check if any of the available skills
@@ -299,7 +298,7 @@ The skill is activated
- The skill's instructions will be provided in the tool result
- Examples:
- skill_name: "pdf" - invoke the pdf skill
- - skill_name: "create-skill" - invoke the create-skill skill for creating new skills
+ - skill_name: "data-analysis" - invoke the data-analysis skill
Important:
- Only use skills listed in below
@@ -307,4 +306,4 @@ Important:
- To create a new skill: prepare it in /workspace, then use register_skill tool
-{available_skills_block}'''
\ No newline at end of file
+{available_skills_block}"""
diff --git a/src/langbot/pkg/skill/manager.py b/src/langbot/pkg/skill/manager.py
index 1db8afba..c5578743 100644
--- a/src/langbot/pkg/skill/manager.py
+++ b/src/langbot/pkg/skill/manager.py
@@ -2,7 +2,6 @@ from __future__ import annotations
import datetime as dt
import os
-import shutil
import typing
from ..core import app
@@ -16,9 +15,7 @@ if typing.TYPE_CHECKING:
class SkillManager:
"""Skill manager backed by filesystem packages.
- Skills are loaded from two sources:
- 1. Builtin skills: templates/skills/ (shipped with LangBot)
- 2. User skills: data/skills/ (created by users)
+ Skills are loaded from data/skills/ and managed by users.
Skills are activated through the `activate` tool (Tool Call mechanism),
aligned with Claude Code's design. This protects KV Cache and follows
@@ -38,8 +35,7 @@ class SkillManager:
async def reload_skills(self):
"""Reload all skills.
- Builtin skills (templates/skills/) are copied to data/skills/ on first run,
- then all skills are loaded from data/skills/.
+ All skills are loaded from data/skills/.
NOTE: This performs a full scan. For registering a single new skill,
consider adding it directly to self.skills instead of reloading all.
@@ -51,25 +47,6 @@ class SkillManager:
managed_root = self.get_managed_skills_root()
os.makedirs(managed_root, exist_ok=True)
- # Copy builtin skills to data/skills/ if not already present
- builtin_root = self.get_builtin_skills_root()
- if os.path.isdir(builtin_root):
- for package_root, entry_file in self._discover_skill_directories(builtin_root):
- skill_data = {
- 'package_root': package_root,
- 'entry_file': entry_file,
- }
- if not self._load_skill_file(skill_data):
- continue
-
- skill_name = skill_data['name']
- target_path = os.path.join(managed_root, skill_name)
-
- # Only copy if target doesn't exist (preserve user modifications)
- if not os.path.exists(target_path):
- shutil.copytree(package_root, target_path)
- self.ap.logger.info(f'Copied builtin skill "{skill_name}" to data/skills/')
-
# Load all skills from data/skills/
if os.path.isdir(managed_root):
for package_root, entry_file in self._discover_skill_directories(managed_root):
@@ -105,11 +82,6 @@ class SkillManager:
"""Get the root directory for managed user skills."""
return paths.get_data_path('skills')
- @staticmethod
- def get_builtin_skills_root() -> str:
- """Get the root directory for builtin skills (templates/skills/)."""
- return paths.get_resource_path('templates/skills')
-
def _discover_skill_directories(self, root_path: str, max_depth: int = 6) -> list[tuple[str, str]]:
"""Discover all skill directories under root_path."""
discovered: list[tuple[str, str]] = []
@@ -189,4 +161,4 @@ class SkillManager:
def get_skill_by_name(self, name: str) -> dict | None:
"""Get skill data by name."""
- return self.skills.get(name)
\ No newline at end of file
+ return self.skills.get(name)
diff --git a/src/langbot/templates/skills/create-skill/SKILL.md b/src/langbot/templates/skills/create-skill/SKILL.md
deleted file mode 100644
index 2ae143c5..00000000
--- a/src/langbot/templates/skills/create-skill/SKILL.md
+++ /dev/null
@@ -1,104 +0,0 @@
----
-name: create-skill
-description: Create new skills for LangBot. Use when users want to create, modify, or register a skill. Helps draft SKILL.md with proper frontmatter and register skills from sandbox directories.
----
-
-# Create Skill
-
-A skill for creating new LangBot skills.
-
-## Skill Structure
-
-A skill is a directory containing, at minimum, a `SKILL.md` file:
-
-```
-skill-name/
-├── SKILL.md # Required: metadata + instructions
-├── scripts/ # Optional: helper scripts
-├── references/ # Optional: documentation
-├── assets/ # Optional: templates, resources
-```
-
-## SKILL.md Format
-
-SKILL.md must contain YAML frontmatter followed by Markdown content:
-
-```markdown
----
-name: skill-name
-display_name: Skill Display Name
-description: What this skill does and when to use it.
----
-
-# Skill Title
-
-Instructions for how to use this skill...
-
-## Examples
-
-- Example 1
-- Example 2
-```
-
-### Frontmatter Fields
-
-| Field | Required | Description |
-|-------|----------|-------------|
-| `name` | Yes | Skill identifier. 1-64 chars, lowercase letters, numbers, hyphens only. Must not start or end with hyphen. |
-| `display_name` | No | Human-readable name shown in UI. |
-| `description` | Yes | What the skill does and when to use it. Max 1024 chars. Include keywords for triggering. |
-| `license` | No | License name or reference. |
-| `metadata` | No | Additional key-value metadata. |
-
-### Body Content
-
-The Markdown body contains skill instructions. Recommended sections:
-- Step-by-step instructions
-- Examples
-- Common edge cases
-- Guidelines
-
-Keep SKILL.md under 500 lines. Move detailed content to `references/` directory.
-
-## Creating a Skill
-
-1. **Understand requirements**: Ask what the skill should do
-2. **Draft SKILL.md**: Create frontmatter + instructions
-3. **Create in sandbox**: Write files to `/workspace/{skill-name}/`
-4. **Register**: Use `register_skill` tool to register to LangBot store
-
-### Name Rules
-
-- Lowercase letters, numbers, hyphens only
-- Cannot start or end with hyphen
-- No consecutive hyphens (`--`)
-- 1-64 characters
-
-Valid: `pdf-processing`, `data-analysis`, `code-review`
-Invalid: `PDF-Processing`, `-pdf`, `pdf--processing`
-
-### Description Tips
-
-Good description describes both what and when:
-```yaml
-description: Extracts text from PDF files. Use when working with PDF documents or when user mentions PDFs.
-```
-
-Poor description:
-```yaml
-description: Helps with PDFs.
-```
-
-## Workflow Example
-
-1. User: "Create a skill for generating reports"
-2. Ask clarifying questions about report format, templates, etc.
-3. Create `/workspace/report-generator/SKILL.md`
-4. Optionally create helper scripts in `/workspace/report-generator/scripts/`
-5. Call `register_skill(path="/workspace/report-generator", name="report-generator")`
-6. Skill is now available via `activate(skill_name="report-generator")`
-
-## After Registration
-
-The skill package is copied to `data/skills/` and loaded by LangBot.
-User can activate it immediately with the `activate` tool.
\ No newline at end of file
diff --git a/web/src/app/home/layout.tsx b/web/src/app/home/layout.tsx
index 15e75061..9ac9770a 100644
--- a/web/src/app/home/layout.tsx
+++ b/web/src/app/home/layout.tsx
@@ -76,18 +76,26 @@ export default function HomeLayout({
// Auto-redirect to wizard on first visit (wizard not yet completed on this instance)
useEffect(() => {
+ let cancelled = false;
+
const checkWizard = async () => {
try {
// Always re-fetch to ensure we have the latest wizard_status from backend
- await initializeSystemInfo();
- if (systemInfo.wizard_status === 'none') {
- navigate('/wizard');
+ await initializeSystemInfo({ throwOnError: true });
+ if (!cancelled && systemInfo.wizard_status === 'none') {
+ navigate('/wizard', { replace: true });
}
} catch {
- // If fetching system info fails, don't redirect
+ if (!cancelled) {
+ navigate('/backend-unavailable', { replace: true });
+ }
}
};
checkWizard();
+
+ return () => {
+ cancelled = true;
+ };
}, [navigate]);
return (
diff --git a/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx
index d7587f71..d6b50940 100644
--- a/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx
+++ b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx
@@ -50,17 +50,6 @@ export default function ExtensionCardComponent({
cardVO.iconURL || httpClient.getPluginIconURL(cardVO.author, cardVO.name);
const showFallback = iconFailed || !iconSrc;
- const getTypeBadgeColor = (type: ExtensionType) => {
- switch (type) {
- case 'mcp':
- return 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300';
- case 'skill':
- return 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300';
- default:
- return 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300';
- }
- };
-
const getTypeLabel = (type: ExtensionType) => {
switch (type) {
case 'mcp':
@@ -72,6 +61,30 @@ export default function ExtensionCardComponent({
}
};
+ const getTypeIcon = (type: ExtensionType) => {
+ switch (type) {
+ case 'mcp':
+ return Server;
+ case 'skill':
+ return Sparkles;
+ default:
+ return Puzzle;
+ }
+ };
+
+ const renderTypeBadge = (type: ExtensionType) => {
+ const TypeIcon = getTypeIcon(type);
+ return (
+
+
+ {getTypeLabel(type)}
+
+ );
+ };
+
const renderPluginContent = () => (
<>