diff --git a/.gitignore b/.gitignore
index 77dd181..51d57b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,9 +28,12 @@ build/
.env
!.env.example
-# MCP files
+# MCP config files
openspace/config/config_mcp.json
+# Communication config files
+openspace/config/config_communication.json
+
# Logs
logs/
diff --git a/README.md b/README.md
index da61f1c..83eecee 100644
--- a/README.md
+++ b/README.md
@@ -15,10 +15,31 @@
[](./COMMUNICATION.md)
[](./README_CN.md)
+**One Command to Evolve All Your AI Agents**: OpenClaw, nanobot, Claude Code, Codex, Cursor and etc.
+
+
+
---
+## 📢 News
+
+- **2026-04-09** 💬 Multi-channel **communication gateway**. OpenSpace can now receive and respond to messages from external platforms. Ships with **WhatsApp** (Baileys bridge + QR auth) and **Feishu** (HTTP webhook) adapters, session management, attachment caching, and allowlist-based access control. See [`openspace/config/README.md`](openspace/config/README.md) for setup.
+- **2026-04-07** 🌐 OpenSpace MCP now supports standalone **SSE** and **streamable HTTP** startup, making it easier for remote hosts to connect over HTTP instead of stdio and bypass stdio-bound MCP server timeout bottlenecks. See the [host integration guide](openspace/host_skills/README.md) for setup details.
+- **2026-04-06** 🛠️ Fixed multiple runtime issues across grounding, MCP serving, skill evolution, and persistence, improving execution stability and recovery in long-running workflows.
+- **2026-04-05** 🧭 Cleaned up LLM credential resolution: centralized `.env` loading, improved host config auto-detection, and made provider-native env handling more consistent.
+- **2026-04-03** 🚀 Released **v0.1.0** — Skill quality monitoring: structural patterns extracted from high-quality skills now evaluate every new submission daily. Faster, more relevant cloud search. Production-grade vertical skill clusters emerging organically from the community. Frontend now supports Chinese (zh) i18n.
+- **2026-04-02** ⚡ Cloud search upgraded for higher relevance and lower latency.
+- **2026-03-31** 🛡️ Security hardening: hardened zip extraction and `import_skill` against path traversal. CLI now respects `OPENSPACE_MODEL` and `OPENSPACE_LLM_*` env vars; MiniMax compatibility; workflow ID collision fixes.
+- **2026-03-29** 🔒 Pinned litellm to <1.82.7 to avoid PYSEC-2026-2 supply-chain attack.
+- **2026-03-28** 🔧 Idempotent skill registration — `register_skill_dir` now returns existing `SkillMeta` for already-registered skills. Updated OpenClaw setup docs.
+- **2026-03-27** 🪟 Fixed stdio deadlock on Windows; improved evolver confirmation parsing with stem-style keyword matching.
+- **2026-03-26** 🌱 Dynamic skill directory re-scanning on each call, lightweight local skill search, and streamlined documentation.
+- **2026-03-25** 🎉 OpenSpace is now open source!
+
+---
+
## The Problem with Today's AI Agents
Today's AI agents — [OpenClaw](https://github.com/openclaw/openclaw), [nanobot](https://github.com/HKUDS/nanobot), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [Cursor](https://cursor.com), etc. — are powerful, but they have a critical weakness: they never **Learn**, **Adapt**, and **Evolve** from real-world experience — let alone **Share** with each other.
@@ -140,6 +161,15 @@ pip install -e .
openspace-mcp --help # verify installation
```
+> [!TIP]
+> **Slow clone?** The `assets/` folder (~50 MB of images) makes the default clone large. Use this lightweight alternative to skip it:
+> ```bash
+> git clone --filter=blob:none --sparse https://github.com/HKUDS/OpenSpace.git
+> cd OpenSpace
+> git sparse-checkout set '/*' '!assets/'
+> pip install -e .
+> ```
+
**Choose your path:**
- **[Path A](#-path-a-for-your-agent)** — Plug OpenSpace into your agent
- **[Path B](#-path-b-as-your-co-worker)** — Use OpenSpace directly as your AI co-worker
@@ -169,6 +199,18 @@ Works with any agent that supports skills (`SKILL.md`) — [Claude Code](https:/
> [!TIP]
> Credentials (API key, model) are **auto-detected** from your agent's config; you usually don't need to set them manually.
+> [!NOTE]
+> OpenSpace supports 3 launch modes:
+> - **stdio**: keep `command: "openspace-mcp"` in the host config.
+> - **SSE**: start `openspace-mcp --transport sse --host 127.0.0.1 --port 8080`.
+> - **streamable HTTP**: start `openspace-mcp --transport streamable-http --host 127.0.0.1 --port 8081`.
+>
+> Common remote endpoints:
+> - SSE endpoint: `http://127.0.0.1:8080/sse`
+> - streamable HTTP endpoint: `http://127.0.0.1:8081/mcp`
+>
+> `stdio` is the simplest option. HTTP modes keep OpenSpace as a standalone server, but **host-specific registration syntax** and **host-side timeouts** still apply.
+
**② Copy skills** into your agent's skills directory:
```bash
@@ -480,6 +522,14 @@ OpenSpace/
│ │ ├── auth.py # API key management
│ │ └── cli/ # CLI tools (download_skill, upload_skill)
│ │
+│ ├── 💬 communication/ # Multi-Channel Communication Gateway
+│ │ ├── gateway.py # Message routing, session management, reply dispatch
+│ │ ├── adapters/ # Platform adapters (WhatsApp, Feishu)
+│ │ ├── bridges/ # Non-Python runtimes (WhatsApp Baileys bridge)
+│ │ ├── config.py # Communication config loader
+│ │ ├── session_store.py # Per-channel session persistence
+│ │ └── types.py # ChannelMessage, ChannelSource, SendResult
+│ │
│ ├── 🔧 platform/ # Platform abstraction (system info, screenshots)
│ ├── 🔧 host_detection/ # Auto-detect nanobot / openclaw credentials
│ ├── 🔧 host_skills/ # SKILL.md definitions for agent integration
@@ -531,7 +581,19 @@ OpenSpace builds upon the following open-source projects. We sincerely thank the
-**🌟 Star us if OpenSpace helps your agent!**
+## ⭐ Star History
+
+If you find OpenSpace helpful, please consider giving us a star! ⭐
+
+
**🧬 Make You Agent Self-Evolve · 🌐 A Community That Grows Together · 💰 Fewer Tokens, Smarter Agents**
diff --git a/README_CN.md b/README_CN.md
index a47342e..1014b1b 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -15,10 +15,31 @@
[](./COMMUNICATION.md)
[](./COMMUNICATION.md)
+**一条命令,进化你所有的 AI Agent**:OpenClaw、nanobot、Claude Code、Codex、Cursor 等
+
+

+
---
+## 📢 最新动态
+
+- **2026-04-09** 💬 多渠道**通信网关**上线。OpenSpace 现可接收并回复外部平台消息。内置 **WhatsApp**(Baileys bridge + 扫码认证)与**飞书**(HTTP webhook)适配器,支持会话管理、附件缓存和白名单访问控制。配置方式见 [`openspace/config/README.md`](openspace/config/README.md)。
+- **2026-04-07** 🌐 OpenSpace MCP 新增独立 **SSE** 与 **streamable HTTP** 启动方式,便于远端 host 通过 HTTP 接入,绕过基于 stdio 的 MCP server timeout 瓶颈。具体接入方式见 [host integration 文档](openspace/host_skills/README.md)。
+- **2026-04-06** 🛠️ 修复多项运行时问题,覆盖 grounding、MCP 服务、skill 进化与持久化链路,长流程执行的稳定性与恢复能力进一步提升。
+- **2026-04-05** 🧭 LLM 凭证解析清理完成:统一 `.env` 加载逻辑,改进宿主配置自动识别,并让 provider 原生环境变量处理更一致。
+- **2026-04-03** 🚀 发布 **v0.1.0** — Skill 质量监控上线:从优质 Skill 中提取结构模式,每日自动评估所有新提交;云端搜索全面升级,匹配更准、响应更快;社区自发形成生产级垂直 Skill 集群。前端新增中文(zh)国际化支持。
+- **2026-04-02** ⚡ 云端搜索升级,提升匹配质量、降低响应延迟。
+- **2026-03-31** 🛡️ 安全加固:zip 解压与 `import_skill` 新增路径穿越防护;CLI 启动时读取 `OPENSPACE_MODEL` 及 `OPENSPACE_LLM_*` 环境变量;修复 MiniMax 兼容性问题与 workflow ID 冲突。
+- **2026-03-29** 🔒 锁定 litellm 版本至 <1.82.7,规避 PYSEC-2026-2 供应链投毒。
+- **2026-03-28** 🔧 Skill 注册幂等化——`register_skill_dir` 对已注册目录直接返回已有 `SkillMeta`,不再重复创建。同步更新 OpenClaw 部署文档。
+- **2026-03-27** 🪟 修复 Windows 下 stdio 死锁;evolver 确认解析改用词干匹配,消除误判。
+- **2026-03-26** 🌱 Skill 目录支持每次调用时动态重扫描,本地搜索更轻量,文档同步精简。
+- **2026-03-25** 🎉 OpenSpace 正式开源!
+
+---
+
## 当前 AI Agent 面临的问题
如今的 AI Agent——[OpenClaw](https://github.com/openclaw/openclaw)、[nanobot](https://github.com/HKUDS/nanobot)、[Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)、[Cursor](https://cursor.com) 等——能力强大,但有一个致命弱点:它们从不从真实世界的经验中**学习**、**适应**和**进化**——更不用说相互之间的**共享**了。
@@ -140,6 +161,15 @@ pip install -e .
openspace-mcp --help # 验证安装
```
+> [!TIP]
+> **Clone 太慢?** `assets/` 目录包含约 50 MB 的图片文件,导致仓库较大。使用以下轻量方式跳过它:
+> ```bash
+> git clone --filter=blob:none --sparse https://github.com/HKUDS/OpenSpace.git
+> cd OpenSpace
+> git sparse-checkout set '/*' '!assets/'
+> pip install -e .
+> ```
+
**选择你的路径:**
- **[路径 A](#-路径-a为你的-agent-接入)** — 将 OpenSpace 接入你的 Agent
- **[路径 B](#-路径-b作为你的-ai-协作者)** — 直接使用 OpenSpace 作为你的 AI 协作者
@@ -169,6 +199,18 @@ openspace-mcp --help # 验证安装
> [!TIP]
> 凭证(API 密钥、模型)会从你的 Agent 配置中**自动检测**,通常无需手动设置。
+> [!NOTE]
+> OpenSpace 支持 3 种启动方式:
+> - **stdio**:在宿主配置里保留 `command: "openspace-mcp"`。
+> - **SSE**:先启动 `openspace-mcp --transport sse --host 127.0.0.1 --port 8080`。
+> - **streamable HTTP**:先启动 `openspace-mcp --transport streamable-http --host 127.0.0.1 --port 8081`。
+>
+> 通用远端 endpoint:
+> - SSE: `http://127.0.0.1:8080/sse`
+> - streamable HTTP: `http://127.0.0.1:8081/mcp`
+>
+> `stdio` 最简单。HTTP 模式会把 OpenSpace 作为独立服务常驻,但 **不同宿主的注册写法不同**,而且 **调用方自己的 timeout 仍然生效**。
+
**② 将 Skill 复制**到你的 Agent Skill 目录:
```bash
@@ -480,6 +522,14 @@ OpenSpace/
│ │ ├── auth.py # API 密钥管理
│ │ └── cli/ # CLI 工具(download_skill、upload_skill)
│ │
+│ ├── 💬 communication/ # 多渠道通信网关
+│ │ ├── gateway.py # 消息路由、会话管理、回复分发
+│ │ ├── adapters/ # 平台适配器(WhatsApp、飞书)
+│ │ ├── bridges/ # 非 Python 运行时(WhatsApp Baileys bridge)
+│ │ ├── config.py # 通信配置加载
+│ │ ├── session_store.py # 按频道的会话持久化
+│ │ └── types.py # ChannelMessage, ChannelSource, SendResult
+│ │
│ ├── 🔧 platform/ # 平台抽象(系统信息、截图)
│ ├── 🔧 host_detection/ # 自动检测 nanobot / openclaw 凭证
│ ├── 🔧 host_skills/ # 面向 Agent 集成的 SKILL.md 定义
diff --git a/assets/cli-typing.gif b/assets/cli-typing.gif
new file mode 100644
index 0000000..ec67739
Binary files /dev/null and b/assets/cli-typing.gif differ
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 6de88c8..b4630ac 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,9 +9,11 @@
"version": "0.1.0",
"dependencies": {
"axios": "^1.7.9",
+ "i18next": "^26.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-force-graph-2d": "^1.25.4",
+ "react-i18next": "^17.0.2",
"react-router-dom": "^7.1.1"
},
"devDependencies": {
@@ -272,6 +274,15 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -2297,6 +2308,46 @@
"node": ">= 0.4"
}
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "26.0.3",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.3.tgz",
+ "integrity": "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://www.locize.com/i18next"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.locize.com"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2"
+ },
+ "peerDependencies": {
+ "typescript": "^5 || ^6"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/index-array-by": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
@@ -2916,6 +2967,33 @@
"react": "*"
}
},
+ "node_modules/react-i18next": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.2.tgz",
+ "integrity": "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2",
+ "html-parse-stringify": "^3.0.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "i18next": ">= 26.0.1",
+ "react": ">= 16.8.0",
+ "typescript": "^5 || ^6"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -3319,7 +3397,7 @@
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -3360,6 +3438,15 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3473,6 +3560,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index ef1907d..2005cf7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,9 +10,11 @@
},
"dependencies": {
"axios": "^1.7.9",
+ "i18next": "^26.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-force-graph-2d": "^1.25.4",
+ "react-i18next": "^17.0.2",
"react-router-dom": "^7.1.1"
},
"devDependencies": {
diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx
index c50f737..855d55c 100644
--- a/frontend/src/components/ErrorBoundary.tsx
+++ b/frontend/src/components/ErrorBoundary.tsx
@@ -1,4 +1,5 @@
import { Component, ErrorInfo, ReactNode } from 'react';
+import i18n from '../i18n';
interface Props {
children: ReactNode;
@@ -21,7 +22,6 @@ export class ErrorBoundary extends Component {
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
- // Log error to reporting service
if (import.meta.env.DEV) {
console.error('Error caught by boundary:', error, errorInfo);
}
@@ -29,25 +29,26 @@ export class ErrorBoundary extends Component {
render() {
if (this.state.hasError) {
+ const t = i18n.t.bind(i18n);
return (
this.props.fallback || (
- Something went wrong
+ {t('errorBoundary.title')}
- An unexpected error occurred
+ {t('errorBoundary.message')}
{import.meta.env.DEV && this.state.error && (
- Error details
+ {t('errorBoundary.details')}
{this.state.error.stack}
diff --git a/frontend/src/components/skill-detail/DiffViewer.tsx b/frontend/src/components/skill-detail/DiffViewer.tsx
index 3a37b65..49b37b9 100644
--- a/frontend/src/components/skill-detail/DiffViewer.tsx
+++ b/frontend/src/components/skill-detail/DiffViewer.tsx
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState, type KeyboardEvent } from 'react';
+import { useTranslation } from 'react-i18next';
import type { DiffFile, DiffLine } from '../../utils/diffParser';
interface DiffViewerProps {
@@ -124,6 +125,7 @@ function buildSplitRows(lines: DiffLine[], header: string): SplitDiffRow[] {
}
export default function DiffViewer({ files }: DiffViewerProps) {
+ const { t } = useTranslation();
const [selectedIndex, setSelectedIndex] = useState(0);
const renderableFiles = useMemo(
() => files.filter((file) => file.hunks.some((hunk) => hunk.lines.length > 0)),
@@ -135,7 +137,7 @@ export default function DiffViewer({ files }: DiffViewerProps) {
}, [renderableFiles]);
if (renderableFiles.length === 0) {
- return No files in diff
;
+ return {t('diffViewer.noFiles')}
;
}
const activeIndex = selectedIndex < renderableFiles.length ? selectedIndex : 0;
@@ -216,8 +218,8 @@ export default function DiffViewer({ files }: DiffViewerProps) {
return (
-
Old
-
New
+
{t('diffViewer.old')}
+
{t('diffViewer.new')}
{hunk.header}
diff --git a/frontend/src/components/skill-detail/SkillEvolutionGraph.tsx b/frontend/src/components/skill-detail/SkillEvolutionGraph.tsx
index 689749c..7675e7f 100644
--- a/frontend/src/components/skill-detail/SkillEvolutionGraph.tsx
+++ b/frontend/src/components/skill-detail/SkillEvolutionGraph.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import ForceGraph2D from 'react-force-graph-2d';
import type { SkillGraphNode } from '../../hooks/useSkillEvolutionGraphData';
@@ -25,6 +26,7 @@ export default function SkillEvolutionGraph({
onNodeClick,
onBackgroundClick,
}: SkillEvolutionGraphProps) {
+ const { t } = useTranslation();
const graphContainerRef = useRef
(null);
const fgRef = useRef(null);
const [graphDim, setGraphDim] = useState({ width: 0, height: 0 });
@@ -180,7 +182,7 @@ export default function SkillEvolutionGraph({
}, []);
if (graphData.nodes.length === 0) {
- return No lineage graph data.
;
+ return {t('graph.noGraphData')}
;
}
return (
@@ -197,9 +199,9 @@ export default function SkillEvolutionGraph({
const graphNode = node as SkillGraphNode;
return [
graphNode.name,
- `score: ${graphNode.score.toFixed(1)}`,
- `generation: ${graphNode.generation}`,
- `origin: ${graphNode.origin}`,
+ t('graph.tooltipScore', { value: graphNode.score.toFixed(1) }),
+ t('graph.tooltipGeneration', { value: graphNode.generation }),
+ t('graph.tooltipOrigin', { value: graphNode.origin }),
].join('\n');
}}
onNodeClick={(node) => {
diff --git a/frontend/src/components/skill-detail/SkillVersionDrawer.tsx b/frontend/src/components/skill-detail/SkillVersionDrawer.tsx
index b842b92..363f560 100644
--- a/frontend/src/components/skill-detail/SkillVersionDrawer.tsx
+++ b/frontend/src/components/skill-detail/SkillVersionDrawer.tsx
@@ -1,6 +1,7 @@
import { useLayoutEffect, useMemo, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
import type { SkillDetail } from '../../api';
import { parseDiff } from '../../utils/diffParser';
import EmptyState from '../EmptyState';
@@ -72,6 +73,7 @@ function lockScroll() {
}
export default function SkillVersionDrawer({ skill, isOpen, onClose }: SkillVersionDrawerProps) {
+ const { t } = useTranslation();
const closeButtonRef = useRef(null);
const rawDiff = skill?.lineage.content_diff ?? '';
@@ -135,16 +137,16 @@ export default function SkillVersionDrawer({ skill, isOpen, onClose }: SkillVers
@@ -153,13 +155,13 @@ export default function SkillVersionDrawer({ skill, isOpen, onClose }: SkillVers
-
Version Summary
-
{skill.description || 'No description available for this version.'}
+
{t('drawer.versionSummary')}
+
{skill.description || t('drawer.noDescriptionAvailable')}
{skill.category}
{skill.origin}
- gen {skill.generation}
- {skill.is_active ? 'active' : 'inactive'}
+ {t('drawer.gen', { generation: skill.generation })}
+ {skill.is_active ? t('common.active') : t('common.inactive')}
{skill.tags.map((tag) => (
{tag}
))}
@@ -167,67 +169,67 @@ export default function SkillVersionDrawer({ skill, isOpen, onClose }: SkillVers
{skill.score.toFixed(1)}
-
version score
+
{t('drawer.versionScore')}
-
Metrics
-
Execution quality
+
{t('drawer.metrics')}
+
{t('drawer.executionQuality')}
-
Selections
{skill.total_selections}
-
Applied
{skill.total_applied}
-
Completions
{skill.total_completions}
-
Fallbacks
{skill.total_fallbacks}
+
{t('drawer.selectionsLabel')}
{skill.total_selections}
+
{t('drawer.appliedLabel')}
{skill.total_applied}
+
{t('drawer.completionsLabel')}
{skill.total_completions}
+
{t('drawer.fallbacksLabel')}
{skill.total_fallbacks}
- Version Metadata
- Origin: {skill.origin}
- Generation: {skill.generation}
- Visibility: {skill.visibility}
- Created: {formatDate(skill.lineage.created_at)}
- First seen: {formatDate(skill.first_seen)}
- Last updated: {formatDate(skill.last_updated)}
- Skill path: {skill.path || 'Unavailable'}
- Skill dir: {skill.skill_dir || 'Unavailable'}
- Parent IDs: {skill.parent_skill_ids.length ? skill.parent_skill_ids.join(', ') : 'None'}
- Change summary: {skill.lineage.change_summary || 'None'}
- Effective score: {formatPercent(skill.effective_rate)}
+ {t('drawer.versionMetadata')}
+ {t('drawer.origin')} {skill.origin}
+ {t('drawer.generation')} {skill.generation}
+ {t('drawer.visibility')} {skill.visibility}
+ {t('drawer.created')} {formatDate(skill.lineage.created_at)}
+ {t('drawer.firstSeen')} {formatDate(skill.first_seen)}
+ {t('drawer.lastUpdated')} {formatDate(skill.last_updated)}
+ {t('drawer.skillPath')} {skill.path || t('common.unavailable')}
+ {t('drawer.skillDir')} {skill.skill_dir || t('common.unavailable')}
+ {t('drawer.parentIds')} {skill.parent_skill_ids.length ? skill.parent_skill_ids.join(', ') : t('common.none')}
+ {t('drawer.changeSummary')} {skill.lineage.change_summary || t('common.none')}
+ {t('drawer.effectiveScore')} {formatPercent(skill.effective_rate)}
-
Diff
-
Content diff
+
{t('drawer.diff')}
+
{t('drawer.contentDiff')}
{isOversizedDiff ? (
-
+
) : canShowDiff ? (
diffFiles.length > 0 ? (
) : (
-
+
)
) : (
-
+
)}
-
Source
-
SKILL.md preview
+
{t('drawer.source')}
+
{t('drawer.skillMdPreview')}
{sourcePreview ? (
@@ -235,14 +237,14 @@ export default function SkillVersionDrawer({ skill, isOpen, onClose }: SkillVers
{sourcePreview.content}
) : (
-
+
)}
-
Analyses
-
Recent execution analyses
+
{t('drawer.analyses')}
+
{t('drawer.recentAnalyses')}
{skill.recent_analyses.length > 0 ? (
@@ -252,15 +254,19 @@ export default function SkillVersionDrawer({ skill, isOpen, onClose }: SkillVers
{analysis.task_id}
{formatDate(analysis.timestamp)}
- {truncate(analysis.execution_note || 'No execution note', 220)}
+ {truncate(analysis.execution_note || t('drawer.noExecutionNote'), 220)}
- completed: {analysis.task_completed ? 'yes' : 'no'} · tool issues: {analysis.tool_issues.length} · suggestions: {analysis.evolution_suggestions.length}
+ {t('drawer.analysisCompleted', {
+ value: analysis.task_completed ? t('common.yes') : t('common.no'),
+ toolIssues: analysis.tool_issues.length,
+ suggestions: analysis.evolution_suggestions.length,
+ })}
))}
) : (
-
+
)}
diff --git a/frontend/src/components/skill-detail/SkillVersionFilterBar.tsx b/frontend/src/components/skill-detail/SkillVersionFilterBar.tsx
index 381b3a2..02aaddd 100644
--- a/frontend/src/components/skill-detail/SkillVersionFilterBar.tsx
+++ b/frontend/src/components/skill-detail/SkillVersionFilterBar.tsx
@@ -1,3 +1,5 @@
+import { useTranslation } from 'react-i18next';
+
interface SkillVersionFilterBarProps {
originFilter: string;
onOriginFilterChange: (value: string) => void;
@@ -15,16 +17,18 @@ export default function SkillVersionFilterBar({
allOrigins,
allTags,
}: SkillVersionFilterBarProps) {
+ const { t } = useTranslation();
+
return (
-
+
-
+