Bläddra i källkod

Update platform frontend and service changes

Jax Docker 1 månad sedan
förälder
incheckning
7a50684696
81 ändrade filer med 7200 tillägg och 3052 borttagningar
  1. 16 0
      .claude/settings.local.json
  2. 895 0
      docs/web-post-api-contract.md
  3. 1 1
      services/tool-service/app/api/routes.py
  4. 3 1
      services/tool-service/app/application/services.py
  5. 7 0
      services/tool-service/app/domain/repositories.py
  6. 3 0
      web/src/App.tsx
  7. 26 11
      web/src/api/agents.ts
  8. 2 0
      web/src/api/index.ts
  9. 9 9
      web/src/api/knowledge.ts
  10. 39 0
      web/src/api/memories.ts
  11. 729 67
      web/src/api/mock.ts
  12. 0 6
      web/src/api/models.ts
  13. 36 0
      web/src/api/skills.ts
  14. 3 12
      web/src/api/teams.ts
  15. 103 13
      web/src/api/tools.ts
  16. 3 1
      web/src/components/layout/AppLayout.tsx
  17. 29 3
      web/src/components/layout/Breadcrumb.tsx
  18. 1 1
      web/src/components/layout/Header.tsx
  19. 3 3
      web/src/components/layout/Sidebar.tsx
  20. 6 3
      web/src/components/shared/ApiErrorState.tsx
  21. 4 2
      web/src/components/shared/ConfirmDialog.tsx
  22. 3 2
      web/src/components/shared/ErrorBoundary.tsx
  23. 3 1
      web/src/components/shared/JsonViewer.tsx
  24. 4 1
      web/src/components/shared/SearchInput.tsx
  25. 9 1
      web/src/components/shared/StatusBadge.tsx
  26. 47 0
      web/src/components/ui/collapsible.tsx
  27. 3 1
      web/src/components/ui/dialog.tsx
  28. 16 14
      web/src/components/ui/tabs.tsx
  29. 3 3
      web/src/hooks/useAgents.ts
  30. 7 3
      web/src/lib/constants.ts
  31. 55 0
      web/src/lib/demo-text.ts
  32. 10 20
      web/src/lib/utils.ts
  33. 511 205
      web/src/locales/en.json
  34. 550 338
      web/src/locales/zh.json
  35. 523 188
      web/src/pages/agents/AgentListPage.tsx
  36. 0 30
      web/src/pages/agents/components/AgentCard.tsx
  37. 0 61
      web/src/pages/agents/components/AgentDetailSheet.tsx
  38. 188 55
      web/src/pages/agents/components/AgentOverview.tsx
  39. 3 9
      web/src/pages/agents/components/AgentRuns.tsx
  40. 0 54
      web/src/pages/agents/components/AgentVersions.tsx
  41. 227 209
      web/src/pages/agents/components/CreateAgentDialog.tsx
  42. 142 5
      web/src/pages/dashboard/DashboardPage.tsx
  43. 64 0
      web/src/pages/dashboard/components/ActivityFeed.tsx
  44. 22 18
      web/src/pages/dashboard/components/ExecutionTrendChart.tsx
  45. 62 30
      web/src/pages/dashboard/components/RecentRunsTable.tsx
  46. 77 0
      web/src/pages/dashboard/components/RunMixPanel.tsx
  47. 56 13
      web/src/pages/dashboard/components/ServiceHealthList.tsx
  48. 68 11
      web/src/pages/dashboard/components/StatsCards.tsx
  49. 285 252
      web/src/pages/knowledge/KnowledgePage.tsx
  50. 354 0
      web/src/pages/memories/MemoryPage.tsx
  51. 7 25
      web/src/pages/models/ModelProvidersPage.tsx
  52. 453 206
      web/src/pages/models/ModelsPage.tsx
  53. 110 12
      web/src/pages/sessions/SessionChatPage.tsx
  54. 2 2
      web/src/pages/sessions/components/ChatInput.tsx
  55. 2 2
      web/src/pages/sessions/components/ChatPanel.tsx
  56. 21 8
      web/src/pages/sessions/components/CreateSessionDialog.tsx
  57. 30 4
      web/src/pages/sessions/components/MessageBubble.tsx
  58. 38 25
      web/src/pages/sessions/components/SessionListPanel.tsx
  59. 297 347
      web/src/pages/skills/SkillsPage.tsx
  60. 110 77
      web/src/pages/teams/components/CreateTeamDialog.tsx
  61. 101 74
      web/src/pages/teams/components/TeamOverview.tsx
  62. 34 52
      web/src/pages/teams/components/TeamRuns.tsx
  63. 0 57
      web/src/pages/teams/components/TeamVersions.tsx
  64. 285 479
      web/src/pages/tools/ToolsPage.tsx
  65. 168 0
      web/src/pages/tools/components/ConnectMcpServerDialog.tsx
  66. 113 0
      web/src/pages/tools/components/CreateToolDialog.tsx
  67. 113 0
      web/src/pages/tools/components/ToolDetailSheet.tsx
  68. 3 2
      web/src/types/agent.ts
  69. 0 2
      web/src/types/app.ts
  70. 0 1
      web/src/types/auth.ts
  71. 1 1
      web/src/types/common.ts
  72. 2 0
      web/src/types/index.ts
  73. 0 1
      web/src/types/knowledge.ts
  74. 56 0
      web/src/types/memory.ts
  75. 0 4
      web/src/types/model-provider.ts
  76. 0 6
      web/src/types/model.ts
  77. 0 1
      web/src/types/runtime.ts
  78. 39 0
      web/src/types/skill.ts
  79. 2 1
      web/src/types/team.ts
  80. 2 2
      web/src/types/tool.ts
  81. 1 4
      web/src/types/workflow.ts

+ 16 - 0
.claude/settings.local.json

@@ -0,0 +1,16 @@
+{
+  "permissions": {
+    "allow": [
+      "Read(//c/Users/Administrator/.claude/plugins/marketplaces/ui-ux-pro-max-skill/.claude/skills/ui-ux-pro-max/**)",
+      "Bash(python3 scripts/search.py \"SaaS platform dashboard admin AI agent workflow orchestration dark mode professional\" --design-system -p \"Auto Platform\")",
+      "Bash(python scripts/search.py \"SaaS platform dashboard admin AI agent workflow orchestration dark mode professional\" --design-system -p \"Auto Platform\")",
+      "Bash(python scripts/search.py \"dashboard admin panel workflow\" --domain product)",
+      "Bash(python scripts/search.py \"dark mode professional dashboard\" --domain style)",
+      "Bash(python scripts/search.py \"saas platform technology\" --domain color)",
+      "Bash(python scripts/search.py \"dashboard data analytics\" --domain typography)",
+      "Bash(python scripts/search.py \"workflow flowchart status pipeline\" --domain chart)",
+      "Bash(python scripts/search.py \"sidebar navigation dashboard admin\" --domain ux)",
+      "Bash(npm install:*)"
+    ]
+  }
+}

+ 895 - 0
docs/web-post-api-contract.md

@@ -0,0 +1,895 @@
+# Web 前端业务闭环与 POST API 合约
+
+本文档用于前端、网关、后端服务共同对齐。它不是当前接口的逐字转录,而是面向产品落地的目标合约:所有请求统一使用 `POST`,页面只使用 `id` 做关联,不向用户暴露 `code`、版本、启用禁用、原始 JSON 等内部概念。
+
+## 1. 统一规则
+
+### 1.1 请求规则
+
+| 项 | 约定 |
+| --- | --- |
+| Base URL | `/gateway` |
+| Method | 全部使用 `POST` |
+| Content-Type | `application/json; charset=utf-8` |
+| Auth | `Authorization: Bearer <access_token>` |
+| User Context | `x-user-id: <user_id>` |
+| Request ID | 前端可传 `x-request-id`,后端必须回传 `request_id` |
+| Timezone | 入库与接口输出建议 UTC,前端展示固定为 `YYYY-MM-DD HH:mm` |
+
+### 1.2 字段规则
+
+| 规则 | 说明 |
+| --- | --- |
+| `*_time` | 所有以 `time` 结尾的字段类型都必须是 `datetime`,JSON 传输为 ISO 8601 字符串,后端必须映射为数据库 datetime 类型 |
+| 关联字段 | 单对象关联使用 `id`,例如 `agent_id`、`model_id`;多对多关系必须使用中间表资源,不允许在主对象上直接保存 `xxx_ids` |
+| 批量参数 | 一次性命令可以使用 `ids` 数组,例如批量运行评估;但这类数组不能代表持久化关系 |
+| 禁止前端字段 | 前端页面、接口入参、业务文案不使用 `code`、`version_no`、`enabled` |
+| 状态字段 | 不使用启用禁用状态。仅运行、任务、文档索引、API Key 撤销等真实生命周期允许有状态 |
+| JSON 字段命名 | 对外统一使用 `metadata`、`config`、`policy`、`schema`,后端可内部映射到 `*_json` |
+| Agent | 不暴露智能体类型,不暴露版本概念,不直接选择工具,只选择模型、技能、记忆、运行策略 |
+| Tool | 前端以 MCP 服务为管理对象,一个 MCP 服务下可发现多个工具;不提供“新增单个 Tool”的主流程 |
+| Knowledge | 先管理知识库列表,再进入单个知识库内部管理文档、检索、评估、任务、设置 |
+
+### 1.3 通用响应包
+
+所有接口都返回统一包裹格式。
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `success` | boolean | 是 | 请求是否成功 |
+| `data` | object \| array \| null | 是 | 成功时为业务数据,失败时为 `null` |
+| `error` | `ApiError` \| null | 是 | 失败时为错误对象,成功时为 `null` |
+| `request_id` | string | 是 | 请求追踪 ID |
+| `server_time` | datetime | 是 | 服务端响应时间 |
+
+`ApiError`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `error_code` | string | 是 | 机器可读错误码 |
+| `message` | string | 是 | 用户可读错误消息 |
+| `details` | object | 否 | 字段级错误或调试信息,不直接展示原始堆栈 |
+
+### 1.4 分页、排序、筛选
+
+`PageRequest`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `page` | integer | 否 | `1` | 页码,从 1 开始 |
+| `page_size` | integer | 否 | `20` | 每页数量,最大 200 |
+| `keyword` | string | 否 | 空 | 搜索关键字 |
+| `sort_by` | string | 否 | `created_time` | 排序字段,必须是后端白名单字段 |
+| `sort_order` | `"asc"` \| `"desc"` | 否 | `"desc"` | 排序方向 |
+
+`PageResult<T>`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `items` | `T[]` | 是 | 当前页数据 |
+| `total` | integer | 是 | 总数量 |
+| `page` | integer | 是 | 当前页码 |
+| `page_size` | integer | 是 | 每页数量 |
+| `has_more` | boolean | 是 | 是否还有下一页 |
+
+## 2. 前端界面与业务闭环排查
+
+| 模块 | 路由 | 当前界面能力 | 主要问题 | API 闭环要求 |
+| --- | --- | --- | --- | --- |
+| 登录 | `/login` | 账号密码登录、写入 token | 缺少 `me/logout` 闭环 | 增加当前用户、退出登录、权限检查 |
+| 仪表盘 | `/dashboard` | 展示智能体、会话、运行、服务健康 | 多个接口聚合,刷新成本高 | 提供 `dashboard/summary`,详情仍可按模块查 |
+| 模型 | `/models` | 模型列表、新建、编辑、删除、测试 | 目前仍有 provider 细节,字段偏技术 | 模型管理只围绕“模型配置列表”,Agent 只用 `model_id` |
+| 智能体 | `/agents` | 列表、创建、编辑、技能选择、测试弹窗 | 执行是本地模拟,仍有版本、类型、状态残留 | 去版本化,提供配置保存、运行、轮询、取消、运行详情 |
+| 会话 | `/sessions` | 会话列表、创建、消息发送、上下文弹窗 | 发送消息后未形成真实 Agent/Workflow 执行闭环 | 消息发送要可触发运行请求并返回最新消息 |
+| 记忆 | `/memories` | 记忆列表、筛选、详情弹窗 | 不应有人工新建主入口;仍有结构化字段展示痕迹 | 提供只读、搜索、详情、归档;创建主要给系统内部调用 |
+| 工具 | `/tools` | MCP 服务列表、连接弹窗、详情抽屉 | 缺少粘贴 MCP 配置、连接测试、工具发现;不应添加单个 Tool | 以 MCP 服务为单位导入、测试、发现、展示内部工具参数 |
+| 技能 | `/skills` | 当前是本地 mock CRUD | 未接真实 API,仍有状态字段 | 提供技能定义、参数 schema、工具引用、安装、测试执行 |
+| 知识库 | `/knowledge` | 知识库列表、文档、搜索、设置、任务、评估 | 部分是 mock;任务、评估、设置未落库 | 以知识库为入口,提供文档解析、索引、检索、Rerank 设置、评估 |
+| 团队 | `/teams` | 团队列表、创建、成员、运行 | 仍有类型、状态、版本残留 | 去版本化,团队直接保存协作配置并可启动运行 |
+| 设置 | `/settings` | API Key 列表、新建、撤销 | 状态展示可保留为安全生命周期,不是启用禁用 | API Key 创建后只展示一次密钥,支持撤销 |
+| Workflow 设计器 | 未挂路由 | 前端类型存在,但无页面 | 业务闭环缺失 | 预留设计、校验、调试、运行查询 API |
+| 模型供应商 | 页面存在未挂路由 | 可创建/发现 provider | 与当前产品“模型列表管理”冲突 | 不作为主入口,能力可并入模型创建弹窗或后端内部 |
+
+## 3. 共享数据模型
+
+### 3.1 Auth
+
+`User`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 用户 ID |
+| `username` | string | 是 | 登录名 |
+| `display_name` | string \| null | 否 | 展示名 |
+| `email` | string \| null | 否 | 邮箱 |
+| `metadata` | object | 是 | 扩展信息 |
+| `last_login_time` | datetime \| null | 否 | 最近登录时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`Role`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 角色 ID |
+| `name` | string | 是 | 角色名称 |
+| `description` | string \| null | 否 | 说明 |
+| `permission_count` | integer | 是 | 已绑定权限数量 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`RolePermissionBinding`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 角色权限绑定 ID |
+| `role_id` | string | 是 | 角色 ID |
+| `permission` | string | 是 | 权限标识 |
+| `scope_type` | string \| null | 否 | 权限范围类型 |
+| `scope_id` | string \| null | 否 | 权限范围对象 ID |
+| `created_time` | datetime | 是 | 创建时间 |
+
+### 3.2 Model
+
+`Model`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 模型配置 ID |
+| `name` | string | 是 | 前端展示名称 |
+| `provider_type` | string | 是 | 供应商类型,例如 `openai`、`ollama`、`custom` |
+| `provider_base_url` | string | 是 | 接入地址 |
+| `has_provider_api_key` | boolean | 是 | 是否已保存密钥 |
+| `model_name` | string | 是 | 供应商模型名 |
+| `capabilities` | string[] | 是 | 能力,例如 `chat`、`embedding`、`rerank` |
+| `context_window` | integer \| null | 否 | 上下文窗口 |
+| `max_output_tokens` | integer \| null | 否 | 最大输出 token |
+| `default_temperature` | number \| null | 否 | 默认温度 |
+| `timeout_seconds` | integer | 是 | 调用超时时间 |
+| `metadata` | object | 否 | 扩展信息 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`ModelTestResult`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `model` | `Model` | 是 | 被测试模型 |
+| `content` | string | 是 | 文本输出 |
+| `finish_reason` | string \| null | 否 | 结束原因 |
+| `tool_calls` | object[] | 否 | 模型返回的工具调用 |
+| `usage` | object | 是 | token 用量 |
+| `latency_ms` | integer | 是 | 调用耗时 |
+
+### 3.3 Agent
+
+`Agent`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 智能体 ID |
+| `name` | string | 是 | 名称 |
+| `owner_user_id` | string \| null | 否 | 创建人 ID |
+| `model_id` | string \| null | 否 | 绑定模型 ID |
+| `system_prompt` | string | 是 | 系统提示词,仅编辑页使用 |
+| `skill_binding_count` | integer | 是 | 已绑定技能数量 |
+| `memory_policy` | `AgentMemoryPolicy` | 是 | 记忆配置 |
+| `runtime_policy` | `AgentRuntimePolicy` | 是 | 运行策略 |
+| `metadata` | object | 否 | 扩展信息 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`AgentSkillBinding`
+
+Agent 与 Skill 是多对多关系,必须通过该中间表维护。Agent 主表和 Agent DTO 不保存技能 ID 数组。
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 智能体技能绑定 ID |
+| `agent_id` | string | 是 | 智能体 ID |
+| `skill_id` | string | 是 | 技能 ID |
+| `order_index` | integer | 是 | 技能展示和调用优先级顺序 |
+| `config` | object | 是 | 该智能体使用该技能时的局部配置 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`AgentMemoryPolicy`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `memory_scope` | `"session"` \| `"user"` \| `"agent"` \| `"team"` \| `"global"` | 是 | 记忆作用域 |
+| `read_memory` | boolean | 是 | 是否读取记忆 |
+| `write_memory` | boolean | 是 | 是否写入记忆 |
+| `max_items` | integer | 是 | 最大召回数量 |
+| `min_score` | number | 是 | 最低召回分数 |
+
+`AgentRuntimePolicy`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `temperature` | number | 是 | 温度 |
+| `max_tokens` | integer | 是 | 最大输出 token |
+| `timeout_seconds` | integer | 是 | 总超时时间 |
+| `retry_attempts` | integer | 是 | 最大重试次数 |
+| `retry_backoff_ms` | integer | 是 | 重试退避毫秒 |
+| `tool_call_limit` | integer | 是 | 单次运行最大工具调用次数 |
+| `output_format` | `"text"` \| `"json"` \| `"markdown"` | 是 | 输出格式 |
+| `human_approval_policy` | `"never"` \| `"before_tool"` \| `"before_final"` | 是 | 人工审批策略 |
+
+`AgentRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 运行 ID |
+| `agent_id` | string | 是 | 智能体 ID |
+| `session_id` | string \| null | 否 | 会话 ID |
+| `input_text` | string \| null | 否 | 输入文本 |
+| `input` | object \| null | 否 | 结构化输入 |
+| `output_text` | string \| null | 否 | 输出文本 |
+| `output` | object \| null | 否 | 结构化输出 |
+| `status` | `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` \| `"paused"` | 是 | 运行生命周期 |
+| `tool_call_count` | integer | 是 | 工具调用次数 |
+| `error_message` | string \| null | 否 | 错误消息 |
+| `queued_time` | datetime \| null | 否 | 排队时间 |
+| `started_time` | datetime \| null | 否 | 开始时间 |
+| `finished_time` | datetime \| null | 否 | 结束时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+### 3.4 Session
+
+`Session`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 会话 ID |
+| `app_id` | string | 是 | 应用 ID |
+| `user_id` | string | 是 | 用户 ID |
+| `channel_type` | string | 是 | 渠道,例如 `web` |
+| `title` | string \| null | 否 | 会话标题 |
+| `started_time` | datetime \| null | 否 | 开始时间 |
+| `last_active_time` | datetime \| null | 否 | 最近活跃时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`Message`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 消息 ID |
+| `session_id` | string | 是 | 会话 ID |
+| `turn_id` | string \| null | 否 | 轮次 ID |
+| `role` | `"user"` \| `"assistant"` \| `"system"` \| `"tool"` | 是 | 消息角色 |
+| `content_type` | `"text"` \| `"markdown"` \| `"image"` \| `"file"` \| `"object"` | 是 | 内容类型 |
+| `content_text` | string \| null | 否 | 文本内容 |
+| `content` | object \| null | 否 | 结构化内容 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`RunRequest`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 请求 ID |
+| `session_id` | string | 是 | 会话 ID |
+| `app_id` | string | 是 | 应用 ID |
+| `workflow_id` | string \| null | 否 | 工作流 ID |
+| `agent_id` | string \| null | 否 | 智能体 ID |
+| `trigger_type` | string | 是 | 触发类型 |
+| `payload` | object | 是 | 请求负载 |
+| `status` | `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` | 是 | 请求状态 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+### 3.5 Tool and MCP
+
+`McpServer`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | MCP 服务 ID |
+| `name` | string | 是 | 服务名称 |
+| `transport` | `"sse"` \| `"streamable_http"` \| `"stdio"` | 是 | 连接协议 |
+| `url` | string \| null | 否 | SSE 或 HTTP 地址 |
+| `headers_masked` | object | 是 | 脱敏请求头 |
+| `timeout_seconds` | integer | 是 | 连接超时 |
+| `sse_read_timeout_seconds` | integer \| null | 否 | SSE 读取超时 |
+| `tool_count` | integer | 是 | 已发现工具数量 |
+| `last_test_result` | `ConnectionTestResult` \| null | 否 | 最近连接测试 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`McpTool`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | MCP 内部工具 ID |
+| `mcp_server_id` | string | 是 | MCP 服务 ID |
+| `name` | string | 是 | 工具名称 |
+| `description` | string \| null | 否 | 工具说明 |
+| `input_schema` | object | 是 | 参数 schema |
+| `output_schema` | object | 否 | 输出 schema |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`ConnectionTestResult`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `success` | boolean | 是 | 是否连通 |
+| `message` | string | 是 | 结果说明 |
+| `latency_ms` | integer \| null | 否 | 延迟 |
+| `tool_count` | integer | 是 | 可发现工具数 |
+| `tested_time` | datetime | 是 | 测试时间 |
+
+### 3.6 Skill
+
+`Skill`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 技能 ID |
+| `name` | string | 是 | 技能名称 |
+| `category` | string | 是 | 分类 |
+| `description` | string \| null | 否 | 技能说明 |
+| `instruction` | string | 是 | 技能指令 |
+| `parameter_schema` | object | 是 | 入参 schema |
+| `output_schema` | object | 是 | 出参 schema |
+| `tool_binding_count` | integer | 是 | 已绑定 MCP 工具数量 |
+| `metadata` | object | 否 | 扩展信息 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`SkillToolBinding`
+
+Skill 与 MCP Tool 是多对多关系,必须通过该中间表维护。Skill 主表和 Skill DTO 不保存工具 ID 数组。
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 技能工具绑定 ID |
+| `skill_id` | string | 是 | 技能 ID |
+| `tool_id` | string | 是 | MCP 工具 ID |
+| `order_index` | integer | 是 | 工具展示和调用优先级顺序 |
+| `parameter_mapping` | object | 是 | 技能参数到工具参数的映射 |
+| `config` | object | 是 | 该技能使用该工具时的局部配置 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`SkillInstallation`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 安装 ID |
+| `skill_id` | string | 是 | 技能 ID |
+| `install_scope` | `"global"` \| `"user"` \| `"agent"` \| `"team"` | 是 | 安装范围 |
+| `scope_id` | string \| null | 否 | 范围对象 ID |
+| `config` | object | 是 | 安装配置 |
+| `installed_by` | string \| null | 否 | 安装人 ID |
+| `installed_time` | datetime | 是 | 安装时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+### 3.7 Knowledge
+
+`KnowledgeBase`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 知识库 ID |
+| `name` | string | 是 | 知识库名称 |
+| `description` | string \| null | 否 | 说明 |
+| `document_count` | integer | 是 | 文档数量 |
+| `indexed_document_count` | integer | 是 | 已索引文档数量 |
+| `chunk_count` | integer | 是 | 切片数量 |
+| `settings` | `KnowledgeSettings` | 是 | 检索设置 |
+| `metadata` | object | 否 | 扩展信息 |
+| `archived_time` | datetime \| null | 否 | 归档时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`KnowledgeSettings`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `retrieval_mode` | `"keyword"` \| `"vector"` \| `"hybrid"` | 是 | 检索模式 |
+| `embedding_model_id` | string \| null | 否 | Embedding 模型 ID |
+| `rerank_model_id` | string \| null | 否 | Rerank 模型 ID |
+| `chunk_size` | integer | 是 | 切片大小 |
+| `chunk_overlap` | integer | 是 | 切片重叠 |
+| `top_k` | integer | 是 | 默认返回数量 |
+| `min_score` | number | 是 | 最低分数 |
+| `max_candidates` | integer | 是 | 候选数量 |
+| `keyword_weight` | number | 是 | 关键词权重 |
+| `vector_weight` | number | 是 | 向量权重 |
+| `rerank_weight` | number | 是 | Rerank 权重 |
+| `query_rewrite` | boolean | 是 | 是否查询改写 |
+| `require_citations` | boolean | 是 | 是否要求引用来源 |
+
+`KnowledgeDocument`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 文档 ID |
+| `knowledge_base_id` | string | 是 | 知识库 ID |
+| `title` | string | 是 | 标题 |
+| `source_type` | `"text"` \| `"markdown"` \| `"json"` \| `"html"` \| `"pdf"` \| `"docx"` \| `"url"` | 是 | 来源类型 |
+| `source_uri` | string \| null | 否 | 来源地址 |
+| `index_status` | `"draft"` \| `"queued"` \| `"indexed"` \| `"failed"` \| `"archived"` | 是 | 索引状态 |
+| `content_hash` | string \| null | 否 | 内容 hash |
+| `metadata` | object | 否 | 扩展信息 |
+| `indexed_time` | datetime \| null | 否 | 索引完成时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`KnowledgeChunk`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 切片 ID |
+| `knowledge_base_id` | string | 是 | 知识库 ID |
+| `document_id` | string | 是 | 文档 ID |
+| `chunk_index` | integer | 是 | 切片序号 |
+| `content_text` | string | 是 | 切片内容 |
+| `token_count` | integer | 是 | token 数 |
+| `embedding_model_id` | string \| null | 否 | 向量模型 ID |
+| `metadata` | object | 否 | 扩展信息 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`KnowledgeSearchResult`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `chunk` | `KnowledgeChunk` | 是 | 命中的切片 |
+| `document` | `KnowledgeDocument` | 是 | 来源文档 |
+| `score` | number | 是 | 最终分数 |
+| `score_detail` | object | 是 | 分数组成 |
+| `citation` | object | 否 | 引用信息 |
+
+`KnowledgeJob`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 任务 ID |
+| `knowledge_base_id` | string | 是 | 知识库 ID |
+| `document_id` | string \| null | 否 | 文档 ID |
+| `job_type` | `"parse"` \| `"index"` \| `"reindex"` \| `"sync"` \| `"delete"` | 是 | 任务类型 |
+| `status` | `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` | 是 | 任务状态 |
+| `progress` | integer | 是 | 进度 0 到 100 |
+| `message` | string \| null | 否 | 任务消息 |
+| `error_message` | string \| null | 否 | 错误消息 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `started_time` | datetime \| null | 否 | 开始时间 |
+| `finished_time` | datetime \| null | 否 | 完成时间 |
+
+### 3.8 Memory
+
+`MemoryItem`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 记忆 ID |
+| `scope_type` | `"global"` \| `"user"` \| `"session"` \| `"agent"` \| `"team"` | 是 | 作用域类型 |
+| `scope_id` | string | 是 | 作用域对象 ID |
+| `memory_type` | string | 是 | 记忆类型 |
+| `content_text` | string | 是 | 记忆内容 |
+| `content` | object \| null | 否 | 结构化内容 |
+| `metadata` | object | 是 | 扩展信息 |
+| `embedding_model_id` | string \| null | 否 | Embedding 模型 ID |
+| `owner_agent_id` | string \| null | 否 | 归属智能体 ID |
+| `user_id` | string \| null | 否 | 用户 ID |
+| `session_id` | string \| null | 否 | 会话 ID |
+| `source_ref` | string \| null | 否 | 来源引用 |
+| `importance_score` | number | 是 | 重要度 |
+| `last_accessed_time` | datetime \| null | 否 | 最近访问时间 |
+| `expires_time` | datetime \| null | 否 | 过期时间 |
+| `archived_time` | datetime \| null | 否 | 归档时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+### 3.9 Team
+
+`Team`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 团队 ID |
+| `name` | string | 是 | 团队名称 |
+| `description` | string \| null | 否 | 说明 |
+| `owner_user_id` | string \| null | 否 | 创建人 ID |
+| `coordination_mode` | `"supervisor"` \| `"collaborative"` \| `"sequential"` \| `"debate"` | 是 | 协作模式 |
+| `objective` | string \| null | 否 | 团队目标 |
+| `member_count` | integer | 是 | 已绑定成员数量 |
+| `policy` | `TeamPolicy` | 是 | 团队策略 |
+| `metadata` | object | 否 | 扩展信息 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`TeamMember`
+
+Team 与 Agent 成员关系必须通过该中间表维护。Team 主表和 Team DTO 不保存成员数组。
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 团队成员绑定 ID |
+| `team_id` | string | 是 | 团队 ID |
+| `agent_id` | string | 是 | 智能体 ID |
+| `role` | string | 是 | 团队内角色 |
+| `responsibility` | string \| null | 否 | 职责说明 |
+| `order_index` | integer | 是 | 顺序 |
+| `config` | object | 是 | 成员局部配置 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`TeamPolicy`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `max_rounds` | integer | 是 | 最大轮次 |
+| `handoff` | `"supervisor"` \| `"round_robin"` \| `"auto"` | 是 | 交接策略 |
+| `failure_mode` | `"stop_on_critical"` \| `"continue"` \| `"fallback"` | 是 | 失败策略 |
+| `timeout_seconds` | integer | 是 | 总超时 |
+| `human_approval_policy` | `"never"` \| `"before_final"` \| `"on_risk"` | 是 | 人工审批策略 |
+
+`TeamRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 团队运行 ID |
+| `team_id` | string | 是 | 团队 ID |
+| `session_id` | string \| null | 否 | 会话 ID |
+| `input_text` | string \| null | 否 | 输入文本 |
+| `input` | object \| null | 否 | 结构化输入 |
+| `output_text` | string \| null | 否 | 输出文本 |
+| `output` | object \| null | 否 | 结构化输出 |
+| `status` | `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` \| `"paused"` | 是 | 运行状态 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `started_time` | datetime \| null | 否 | 开始时间 |
+| `finished_time` | datetime \| null | 否 | 结束时间 |
+
+### 3.10 Runtime and Observability
+
+`WorkflowRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 运行 ID |
+| `app_id` | string | 是 | 应用 ID |
+| `workflow_id` | string | 是 | 工作流 ID |
+| `session_id` | string \| null | 否 | 会话 ID |
+| `parent_run_id` | string \| null | 否 | 父运行 ID |
+| `root_run_id` | string \| null | 否 | 根运行 ID |
+| `run_type` | string | 是 | 运行类型 |
+| `status` | `"pending"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` \| `"paused"` | 是 | 运行状态 |
+| `trigger_type` | string | 是 | 触发方式 |
+| `priority` | integer | 是 | 优先级 |
+| `current_node_count` | integer | 是 | 当前节点数量 |
+| `started_time` | datetime \| null | 否 | 开始时间 |
+| `finished_time` | datetime \| null | 否 | 结束时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`NodeRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 节点运行 ID |
+| `run_id` | string | 是 | 运行 ID |
+| `node_id` | string | 是 | 节点 ID |
+| `node_type` | string | 是 | 节点类型 |
+| `attempt_no` | integer | 是 | 第几次尝试 |
+| `status` | `"pending"` \| `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"skipped"` | 是 | 状态 |
+| `output_text` | string \| null | 否 | 输出文本 |
+| `output` | object \| null | 否 | 结构化输出 |
+| `scheduled_time` | datetime \| null | 否 | 计划时间 |
+| `timeout_time` | datetime \| null | 否 | 超时时间 |
+| `queued_time` | datetime \| null | 否 | 排队时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`ExecutionLog`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 日志 ID |
+| `run_id` | string | 是 | 运行 ID |
+| `node_run_id` | string \| null | 否 | 节点运行 ID |
+| `event_type` | string | 是 | 事件类型 |
+| `level` | `"debug"` \| `"info"` \| `"warning"` \| `"error"` | 是 | 级别 |
+| `message` | string | 是 | 日志内容 |
+| `detail` | object \| null | 否 | 详情 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`TraceSpan`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | Span ID |
+| `run_id` | string | 是 | 运行 ID |
+| `node_run_id` | string \| null | 否 | 节点运行 ID |
+| `parent_span_id` | string \| null | 否 | 父 Span ID |
+| `span_type` | string | 是 | Span 类型 |
+| `name` | string | 是 | 名称 |
+| `status` | string | 是 | 状态 |
+| `started_time` | datetime | 是 | 开始时间 |
+| `ended_time` | datetime \| null | 否 | 结束时间 |
+| `duration_ms` | integer \| null | 否 | 耗时 |
+| `attributes` | object \| null | 否 | 属性 |
+| `error_message` | string \| null | 否 | 错误消息 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+## 4. API 明细
+
+下面所有接口的 Method 都是 `POST`。输出均使用统一响应包,表格中的“输出”指 `data` 字段。
+
+### 4.1 Auth
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /auth/login` | `username:string` 必填;`password:string` 必填 | `access_token:string`;`token_type:"bearer"`;`expires_time:datetime`;`user:User` | 登录 |
+| `POST /auth/logout` | 无 | `ok:boolean` | 退出登录,服务端可加入 token 黑名单 |
+| `POST /auth/me` | 无 | `user:User`;`roles:Role[]`;`permissions:string[]` | 获取当前用户 |
+| `POST /auth/users/list` | `PageRequest` | `PageResult<User>` | 用户列表 |
+| `POST /auth/roles/list` | `PageRequest` | `PageResult<Role>` | 角色列表 |
+| `POST /auth/roles/permissions/list` | `PageRequest`;`role_id:string` 必填 | `PageResult<RolePermissionBinding>` | 查询角色权限绑定 |
+| `POST /auth/roles/permissions/add` | `role_id:string` 必填;`permission:string` 必填;`scope_type:string|null`;`scope_id:string|null` | `RolePermissionBinding` | 新增角色权限绑定 |
+| `POST /auth/roles/permissions/remove` | `binding_id:string` 必填 | `deleted:boolean`;`binding_id:string` | 删除角色权限绑定 |
+| `POST /auth/permissions/check` | `user_id:string` 必填;`permission:string` 必填;`scope_type:string|null`;`scope_id:string|null` | `allowed:boolean`;`reason:string`;`matched_role_ids:string[]` | 权限检查 |
+
+### 4.2 Dashboard and Health
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /dashboard/summary` | `time_range:"24h"|"7d"|"30d"` 默认 `7d` | `agent_count:integer`;`session_count:integer`;`run_count:integer`;`failed_run_count:integer`;`live_run_count:integer`;`healthy_service_count:integer`;`trend:Array<{date:string,total:integer,successful:integer,failed:integer}>` | 仪表盘聚合,减少前端多次请求 |
+| `POST /health/get` | 无 | `service:string`;`status:string`;`database:string|null`;`checked_time:datetime` | 网关健康 |
+| `POST /health/services` | 无 | `service:string`;`status:string`;`downstream_services:Array<{service:string,status:string,url:string,http_status:integer|null,error_message:string|null}>` | 下游服务健康 |
+
+### 4.3 Models
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /models/list` | `PageRequest`;`provider_type:string|null`;`capability:string|null` | `PageResult<Model>` | 模型配置列表 |
+| `POST /models/get` | `model_id:string` 必填 | `Model` | 模型详情 |
+| `POST /models/create` | `name:string` 必填;`provider_type:string` 必填;`provider_base_url:string` 必填;`provider_api_key:string|null`;`model_name:string` 必填;`capabilities:string[]`;`context_window:integer|null`;`max_output_tokens:integer|null`;`default_temperature:number|null`;`timeout_seconds:integer`;`metadata:object` | `Model` | 新增模型配置 |
+| `POST /models/update` | `model_id:string` 必填;其余字段同 create,均可选 | `Model` | 更新模型配置 |
+| `POST /models/delete` | `model_id:string` 必填 | `deleted:boolean`;`model_id:string` | 删除模型配置 |
+| `POST /models/test` | `model_id:string` 必填;`prompt:string` 必填;`system_prompt:string|null`;`temperature:number|null`;`max_tokens:integer|null` | `ModelTestResult` | 测试模型 |
+| `POST /models/discover` | `provider_type:string` 必填;`provider_base_url:string` 必填;`provider_api_key:string|null` | `models:Array<{model_name:string,display_name:string,capabilities:string[],context_window:integer|null}>` | 自动发现供应商模型,可合并到新建弹窗 |
+
+### 4.4 Agents
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /agents/list` | `PageRequest`;`owner_user_id:string|null`;`skill_id:string|null`;`model_id:string|null` | `PageResult<Agent>` | 智能体列表 |
+| `POST /agents/get` | `agent_id:string` 必填 | `Agent` | 智能体详情 |
+| `POST /agents/create` | `name:string` 必填;`owner_user_id:string|null`;`model_id:string|null`;`system_prompt:string`;`memory_policy:AgentMemoryPolicy`;`runtime_policy:AgentRuntimePolicy`;`metadata:object` | `Agent` | 新建智能体,不传类型、状态、版本、技能 ID 数组 |
+| `POST /agents/update` | `agent_id:string` 必填;`name:string|null`;`model_id:string|null`;`system_prompt:string|null`;`memory_policy:AgentMemoryPolicy|null`;`runtime_policy:AgentRuntimePolicy|null`;`metadata:object|null` | `Agent` | 编辑智能体,技能绑定通过中间表 API 独立维护 |
+| `POST /agents/delete` | `agent_id:string` 必填;`delete_runs:boolean` 默认 `false` | `deleted:boolean`;`agent_id:string` | 删除智能体 |
+| `POST /agents/skills/list` | `PageRequest`;`agent_id:string` 必填 | `PageResult<AgentSkillBinding>` | 查询智能体技能绑定 |
+| `POST /agents/skills/add` | `agent_id:string` 必填;`skill_id:string` 必填;`order_index:integer|null`;`config:object` | `AgentSkillBinding` | 新增智能体技能绑定 |
+| `POST /agents/skills/update` | `binding_id:string` 必填;`order_index:integer|null`;`config:object|null` | `AgentSkillBinding` | 更新智能体技能绑定 |
+| `POST /agents/skills/remove` | `binding_id:string` 必填 | `deleted:boolean`;`binding_id:string` | 删除智能体技能绑定 |
+| `POST /agents/skills/reorder` | `agent_id:string` 必填;`items:Array<{binding_id:string,order_index:integer}>` 必填 | `PageResult<AgentSkillBinding>` | 调整智能体技能顺序 |
+| `POST /agents/runs/list` | `PageRequest`;`agent_id:string|null`;`session_id:string|null`;`status:string|null`;`start_time:datetime|null`;`end_time:datetime|null` | `PageResult<AgentRun>` | 运行历史 |
+| `POST /agents/runs/start` | `agent_id:string` 必填;`session_id:string|null`;`input_text:string|null`;`input:object|null`;`stream:boolean` 默认 `false` | `AgentRun` | 启动真实智能体执行 |
+| `POST /agents/runs/get` | `run_id:string` 必填 | `AgentRun` | 运行详情 |
+| `POST /agents/runs/poll` | `run_id:string` 必填;`after_time:datetime|null` | `run:AgentRun`;`messages:Message[]`;`logs:ExecutionLog[]`;`tool_calls:Array<{id:string,skill_id:string|null,tool_id:string|null,name:string,input:object,output:object|null,status:string,started_time:datetime|null,finished_time:datetime|null}>` | POST 轮询运行进度 |
+| `POST /agents/runs/cancel` | `run_id:string` 必填;`reason:string|null` | `AgentRun` | 取消运行 |
+
+### 4.5 Sessions
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /sessions/list` | `PageRequest`;`app_id:string|null`;`user_id:string|null`;`channel_type:string|null` | `PageResult<Session>` | 会话列表 |
+| `POST /sessions/get` | `session_id:string` 必填 | `Session` | 会话详情 |
+| `POST /sessions/create` | `app_id:string` 必填;`user_id:string` 必填;`channel_type:string` 默认 `web`;`title:string|null` | `Session` | 创建会话 |
+| `POST /sessions/messages/list` | `PageRequest`;`session_id:string` 必填;`after_time:datetime|null` | `PageResult<Message>` | 消息列表 |
+| `POST /sessions/messages/send` | `session_id:string` 必填;`content_text:string` 必填;`content_type:"text"|"markdown"` 默认 `text`;`agent_id:string|null`;`workflow_id:string|null`;`trigger_run:boolean` 默认 `true` | `message:Message`;`run_request:RunRequest|null` | 发送消息,可触发运行 |
+| `POST /sessions/run-requests/list` | `PageRequest`;`session_id:string` 必填 | `PageResult<RunRequest>` | 会话运行请求 |
+| `POST /sessions/context/get` | `session_id:string` 必填 | `session:Session`;`message_count:integer`;`run_count:integer`;`recent_run_requests:RunRequest[]` | 右侧上下文弹窗 |
+
+### 4.6 MCP Tools
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /tools/mcp/list` | `PageRequest`;`keyword:string|null` | `PageResult<McpServer>` | MCP 服务列表 |
+| `POST /tools/mcp/get` | `mcp_server_id:string` 必填 | `server:McpServer`;`tools:McpTool[]` | MCP 服务详情 |
+| `POST /tools/mcp/import-config` | `config:object` 必填,格式为 `{server_name:{url,headers,timeout,sse_read_timeout}}`;`test_connection:boolean` 默认 `true`;`discover_tools:boolean` 默认 `true` | `servers:McpServer[]`;`test_results:ConnectionTestResult[]`;`discovered_tools:McpTool[]` | 支持用户粘贴 MCP JSON 配置 |
+| `POST /tools/mcp/create` | `name:string` 必填;`transport:string` 默认 `sse`;`url:string|null`;`headers:object`;`timeout_seconds:integer`;`sse_read_timeout_seconds:integer|null` | `McpServer` | 表单创建 MCP 服务 |
+| `POST /tools/mcp/update` | `mcp_server_id:string` 必填;`name:string|null`;`url:string|null`;`headers:object|null`;`timeout_seconds:integer|null`;`sse_read_timeout_seconds:integer|null` | `McpServer` | 更新 MCP 服务 |
+| `POST /tools/mcp/delete` | `mcp_server_id:string` 必填 | `deleted:boolean`;`mcp_server_id:string` | 删除 MCP 服务 |
+| `POST /tools/mcp/test` | `mcp_server_id:string|null`;`name:string|null`;`transport:string|null`;`url:string|null`;`headers:object|null`;`timeout_seconds:integer|null`;`sse_read_timeout_seconds:integer|null` | `ConnectionTestResult` | 测试已保存或临时连接 |
+| `POST /tools/mcp/discover` | `mcp_server_id:string` 必填;`refresh:boolean` 默认 `true` | `mcp_server_id:string`;`tools:McpTool[]`;`discovered_time:datetime` | 发现 MCP 内部工具 |
+| `POST /tools/mcp/tools/list` | `PageRequest`;`mcp_server_id:string|null` | `PageResult<McpTool>` | MCP 内部工具列表 |
+| `POST /tools/mcp/tools/get` | `tool_id:string` 必填 | `McpTool` | 工具参数详情 |
+
+### 4.7 Skills
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /skills/list` | `PageRequest`;`category:string|null`;`tool_id:string|null` | `PageResult<Skill>` | 技能列表 |
+| `POST /skills/get` | `skill_id:string` 必填 | `Skill` | 技能详情 |
+| `POST /skills/create` | `name:string` 必填;`category:string` 必填;`description:string|null`;`instruction:string` 必填;`parameter_schema:object`;`output_schema:object`;`metadata:object` | `Skill` | 创建技能,不传工具 ID 数组 |
+| `POST /skills/update` | `skill_id:string` 必填;其余字段同 create,均可选 | `Skill` | 编辑技能,工具绑定通过中间表 API 独立维护 |
+| `POST /skills/delete` | `skill_id:string` 必填 | `deleted:boolean`;`skill_id:string` | 删除技能 |
+| `POST /skills/tools/list` | `PageRequest`;`skill_id:string` 必填 | `PageResult<SkillToolBinding>` | 查询技能工具绑定 |
+| `POST /skills/tools/add` | `skill_id:string` 必填;`tool_id:string` 必填;`order_index:integer|null`;`parameter_mapping:object`;`config:object` | `SkillToolBinding` | 新增技能工具绑定 |
+| `POST /skills/tools/update` | `binding_id:string` 必填;`order_index:integer|null`;`parameter_mapping:object|null`;`config:object|null` | `SkillToolBinding` | 更新技能工具绑定 |
+| `POST /skills/tools/remove` | `binding_id:string` 必填 | `deleted:boolean`;`binding_id:string` | 删除技能工具绑定 |
+| `POST /skills/tools/reorder` | `skill_id:string` 必填;`items:Array<{binding_id:string,order_index:integer}>` 必填 | `PageResult<SkillToolBinding>` | 调整技能工具顺序 |
+| `POST /skills/installations/list` | `PageRequest`;`skill_id:string|null`;`install_scope:string|null`;`scope_id:string|null` | `PageResult<SkillInstallation>` | 安装列表 |
+| `POST /skills/install` | `skill_id:string` 必填;`install_scope:string` 必填;`scope_id:string|null`;`config:object` | `SkillInstallation` | 安装技能 |
+| `POST /skills/uninstall` | `installation_id:string` 必填 | `deleted:boolean`;`installation_id:string` | 卸载技能 |
+| `POST /skills/test` | `skill_id:string` 必填;`input:object` 必填;`agent_id:string|null`;`session_id:string|null` | `output:object`;`logs:ExecutionLog[]`;`latency_ms:integer` | 测试技能执行 |
+
+### 4.8 Knowledge
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /knowledge/bases/list` | `PageRequest`;`include_archived:boolean` 默认 `false` | `PageResult<KnowledgeBase>` | 知识库列表页 |
+| `POST /knowledge/bases/get` | `knowledge_base_id:string` 必填 | `KnowledgeBase` | 知识库详情 |
+| `POST /knowledge/bases/create` | `name:string` 必填;`description:string|null`;`settings:KnowledgeSettings|null`;`metadata:object` | `KnowledgeBase` | 新建知识库 |
+| `POST /knowledge/bases/update` | `knowledge_base_id:string` 必填;`name:string|null`;`description:string|null`;`metadata:object|null` | `KnowledgeBase` | 编辑知识库基础信息 |
+| `POST /knowledge/bases/archive` | `knowledge_base_id:string` 必填 | `KnowledgeBase` | 归档知识库 |
+| `POST /knowledge/bases/restore` | `knowledge_base_id:string` 必填 | `KnowledgeBase` | 恢复知识库 |
+| `POST /knowledge/settings/get` | `knowledge_base_id:string` 必填 | `KnowledgeSettings` | 获取检索设置 |
+| `POST /knowledge/settings/save` | `knowledge_base_id:string` 必填;`settings:KnowledgeSettings` 必填 | `KnowledgeSettings` | 保存设置,必须支持 Rerank 模型 |
+| `POST /knowledge/documents/list` | `PageRequest`;`knowledge_base_id:string` 必填;`source_type:string|null`;`index_status:string|null` | `PageResult<KnowledgeDocument>` | 文档列表 |
+| `POST /knowledge/documents/parse` | `source_type:string` 必填;`source_uri:string|null`;`content_text:string|null`;`content_base64:string|null`;`file_name:string|null` | `content_text:string`;`source_type:string`;`metadata:object` | 文档解析预览 |
+| `POST /knowledge/documents/create` | `knowledge_base_id:string` 必填;`title:string` 必填;`source_type:string` 必填;`source_uri:string|null`;`content_text:string|null`;`content_base64:string|null`;`metadata:object`;`chunk_size:integer|null`;`chunk_overlap:integer|null` | `document:KnowledgeDocument`;`chunks:KnowledgeChunk[]`;`job:KnowledgeJob|null` | 导入文档 |
+| `POST /knowledge/documents/delete` | `document_id:string` 必填 | `deleted:boolean`;`document_id:string` | 删除文档 |
+| `POST /knowledge/documents/reindex` | `document_id:string` 必填;`settings:KnowledgeSettings|null` | `KnowledgeJob` | 重建索引 |
+| `POST /knowledge/search` | `knowledge_base_id:string` 必填;`query:string` 必填;`top_k:integer` 默认 5;`filters:object`;`rerank_model_id:string|null`;`include_score_detail:boolean` 默认 `true` | `KnowledgeSearchResult[]` | 检索测试 |
+| `POST /knowledge/jobs/list` | `PageRequest`;`knowledge_base_id:string|null`;`document_id:string|null`;`status:string|null` | `PageResult<KnowledgeJob>` | 索引任务 |
+| `POST /knowledge/jobs/create` | `knowledge_base_id:string` 必填;`document_id:string|null`;`job_type:string` 必填;`payload:object` | `KnowledgeJob` | 创建索引或同步任务 |
+| `POST /knowledge/jobs/retry` | `job_id:string` 必填 | `KnowledgeJob` | 重试任务 |
+| `POST /knowledge/jobs/cancel` | `job_id:string` 必填;`reason:string|null` | `KnowledgeJob` | 取消任务 |
+| `POST /knowledge/evals/list` | `PageRequest`;`knowledge_base_id:string` 必填 | `PageResult<{id:string,knowledge_base_id:string,query:string,expected:string,recall:number,precision:number,last_run_time:datetime|null,created_time:datetime}>` | 评估集列表 |
+| `POST /knowledge/evals/create` | `knowledge_base_id:string` 必填;`query:string` 必填;`expected:string` 必填 | `id:string`;`knowledge_base_id:string`;`query:string`;`expected:string`;`created_time:datetime` | 新增评估问题 |
+| `POST /knowledge/evals/run` | `knowledge_base_id:string` 必填;`eval_ids:string[]|null` | `job:KnowledgeJob` | 运行评估 |
+
+### 4.9 Memories
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /memories/list` | `PageRequest`;`scope_type:string|null`;`scope_id:string|null`;`memory_type:string|null`;`owner_agent_id:string|null`;`user_id:string|null`;`session_id:string|null` | `PageResult<MemoryItem>` | 记忆列表,主 UI 以只读为主 |
+| `POST /memories/get` | `memory_id:string` 必填 | `MemoryItem` | 记忆详情 |
+| `POST /memories/search` | `query:string` 必填;`scope_type:string|null`;`scope_id:string|null`;`owner_agent_id:string|null`;`user_id:string|null`;`session_id:string|null`;`limit:integer` 默认 10 | `Array<{item:MemoryItem,score:number,score_detail:object}>` | 语义搜索 |
+| `POST /memories/create` | `scope_type:string` 必填;`scope_id:string` 必填;`memory_type:string` 必填;`content_text:string` 必填;`content:object|null`;`metadata:object`;`owner_agent_id:string|null`;`user_id:string|null`;`session_id:string|null`;`source_ref:string|null`;`importance_score:number`;`expires_time:datetime|null` | `MemoryItem` | 系统内部写入记忆,前端不做主入口按钮 |
+| `POST /memories/archive` | `memory_id:string` 必填 | `MemoryItem` | 归档记忆 |
+| `POST /memories/delete` | `memory_id:string` 必填 | `deleted:boolean`;`memory_id:string` | 删除记忆 |
+
+### 4.10 Teams
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /teams/list` | `PageRequest`;`owner_user_id:string|null`;`agent_id:string|null` | `PageResult<Team>` | 团队列表 |
+| `POST /teams/get` | `team_id:string` 必填 | `Team` | 团队详情 |
+| `POST /teams/create` | `name:string` 必填;`description:string|null`;`owner_user_id:string|null`;`coordination_mode:string`;`objective:string|null`;`policy:TeamPolicy`;`metadata:object` | `Team` | 新建团队,不传类型、状态、版本、成员数组 |
+| `POST /teams/update` | `team_id:string` 必填;其余字段同 create,均可选 | `Team` | 编辑团队,成员绑定通过中间表 API 独立维护 |
+| `POST /teams/delete` | `team_id:string` 必填;`delete_runs:boolean` 默认 `false` | `deleted:boolean`;`team_id:string` | 删除团队 |
+| `POST /teams/members/list` | `PageRequest`;`team_id:string` 必填 | `PageResult<TeamMember>` | 查询团队成员绑定 |
+| `POST /teams/members/add` | `team_id:string` 必填;`agent_id:string` 必填;`role:string` 必填;`responsibility:string|null`;`order_index:integer|null`;`config:object` | `TeamMember` | 新增团队成员 |
+| `POST /teams/members/update` | `member_id:string` 必填;`role:string|null`;`responsibility:string|null`;`order_index:integer|null`;`config:object|null` | `TeamMember` | 更新团队成员 |
+| `POST /teams/members/remove` | `member_id:string` 必填 | `deleted:boolean`;`member_id:string` | 删除团队成员 |
+| `POST /teams/members/reorder` | `team_id:string` 必填;`items:Array<{member_id:string,order_index:integer}>` 必填 | `PageResult<TeamMember>` | 调整团队成员顺序 |
+| `POST /teams/runs/list` | `PageRequest`;`team_id:string|null`;`session_id:string|null`;`status:string|null`;`start_time:datetime|null`;`end_time:datetime|null` | `PageResult<TeamRun>` | 团队运行列表 |
+| `POST /teams/runs/start` | `team_id:string` 必填;`session_id:string|null`;`input_text:string|null`;`input:object|null` | `TeamRun` | 启动团队执行 |
+| `POST /teams/runs/get` | `run_id:string` 必填 | `TeamRun` | 团队运行详情 |
+| `POST /teams/runs/cancel` | `run_id:string` 必填;`reason:string|null` | `TeamRun` | 取消团队运行 |
+
+### 4.11 Runtime and Logs
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /runtime/runs/list` | `PageRequest`;`app_id:string|null`;`workflow_id:string|null`;`session_id:string|null`;`status:string|null`;`start_time:datetime|null`;`end_time:datetime|null` | `PageResult<WorkflowRun>` | 工作流运行 |
+| `POST /runtime/runs/get` | `run_id:string` 必填 | `WorkflowRun` | 运行详情 |
+| `POST /runtime/node-runs/list` | `PageRequest`;`run_id:string` 必填;`status:string|null` | `PageResult<NodeRun>` | 节点运行 |
+| `POST /runtime/execution-logs/list` | `PageRequest`;`run_id:string` 必填;`node_run_id:string|null`;`level:string|null` | `PageResult<ExecutionLog>` | 执行日志 |
+| `POST /runtime/trace-spans/list` | `PageRequest`;`run_id:string` 必填;`node_run_id:string|null` | `PageResult<TraceSpan>` | Trace 数据 |
+
+### 4.12 Settings and API Keys
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /api-keys/list` | `PageRequest` | `PageResult<ApiKey>` | API Key 列表 |
+| `POST /api-keys/create` | `name:string` 必填;`scopes:string|null`;`expires_time:datetime|null` | `ApiKeyCreated` | 创建 API Key,明文只返回一次 |
+| `POST /api-keys/revoke` | `api_key_id:string` 必填 | `ApiKey` | 撤销 API Key |
+
+`ApiKey`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | API Key ID |
+| `name` | string | 是 | 名称 |
+| `key_prefix` | string | 是 | 前缀 |
+| `revoked_time` | datetime \| null | 否 | 撤销时间 |
+| `scopes` | string \| null | 否 | 权限范围 |
+| `expires_time` | datetime \| null | 否 | 过期时间 |
+| `last_used_time` | datetime \| null | 否 | 最近使用时间 |
+| `created_time` | datetime | 是 | 创建时间 |
+
+`ApiKeyCreated` = `ApiKey` 加上以下字段:
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `api_key` | string | 是 | 明文密钥,仅创建后返回一次 |
+
+### 4.13 Workflow Designer and Debugger
+
+当前前端未挂 Workflow 路由,但平台需要多应用、多智能体、团队模式,建议保留以下 API,后续用于设计器和调试器闭环。
+
+`Workflow`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 工作流 ID |
+| `app_id` | string | 是 | 应用 ID |
+| `name` | string | 是 | 名称 |
+| `workflow_type` | string | 是 | 类型 |
+| `dsl` | `WorkflowDSL` | 是 | 设计器 DSL |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+`WorkflowDSL`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `name` | string | 是 | 工作流名称 |
+| `nodes` | `WorkflowNode[]` | 是 | 节点列表 |
+| `edges` | `WorkflowEdge[]` | 是 | 连线列表 |
+
+`WorkflowNode`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 节点 ID |
+| `type` | string | 是 | 节点类型 |
+| `name` | string \| null | 否 | 展示名称 |
+| `config` | object | 是 | 节点配置 |
+
+`WorkflowEdge`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `source` | string | 是 | 来源节点 ID |
+| `target` | string | 是 | 目标节点 ID |
+| `condition` | string \| null | 否 | 条件表达式 |
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /workflows/list` | `PageRequest`;`app_id:string|null`;`workflow_type:string|null` | `PageResult<Workflow>` | 工作流列表 |
+| `POST /workflows/get` | `workflow_id:string` 必填 | `Workflow` | 工作流详情 |
+| `POST /workflows/create` | `app_id:string` 必填;`name:string` 必填;`workflow_type:string` 必填;`dsl:WorkflowDSL` | `Workflow` | 新建工作流 |
+| `POST /workflows/save` | `workflow_id:string` 必填;`name:string|null`;`dsl:WorkflowDSL` 必填 | `Workflow` | 保存设计器 |
+| `POST /workflows/delete` | `workflow_id:string` 必填 | `deleted:boolean`;`workflow_id:string` | 删除工作流 |
+| `POST /workflows/validate` | `dsl:WorkflowDSL` 必填 | `valid:boolean`;`diagnostics:Array<{severity:string,diagnostic_id:string,message:string,node_id:string|null,edge_index:integer|null}>`;`node_count:integer`;`edge_count:integer`;`entry_node_ids:string[]`;`terminal_node_ids:string[]`;`isolated_node_ids:string[]`;`unreachable_node_ids:string[]`;`cycle_detected:boolean` | 校验 DSL |
+| `POST /workflows/debug/start` | `workflow_id:string|null`;`dsl:WorkflowDSL|null`;`input:object`;`breakpoints:string[]` | `debug_session_id:string`;`run_id:string`;`created_time:datetime` | 启动调试 |
+| `POST /workflows/debug/step` | `debug_session_id:string` 必填;`action:"next"|"continue"|"pause"` 必填;`input_patch:object|null` | `run:WorkflowRun`;`current_node:NodeRun|null`;`logs:ExecutionLog[]` | 单步调试 |
+| `POST /workflows/debug/stop` | `debug_session_id:string` 必填 | `stopped:boolean`;`finished_time:datetime` | 停止调试 |
+
+### 4.14 Apps
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /apps/list` | `PageRequest` | `PageResult<App>` | 应用列表 |
+| `POST /apps/create` | `name:string` 必填;`description:string|null`;`owner_user_id:string|null`;`settings:object` | `App` | 新建应用 |
+| `POST /apps/update` | `app_id:string` 必填;`name:string|null`;`description:string|null`;`settings:object|null` | `App` | 编辑应用 |
+| `POST /apps/delete` | `app_id:string` 必填 | `deleted:boolean`;`app_id:string` | 删除应用 |
+
+`App`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 应用 ID |
+| `name` | string | 是 | 应用名 |
+| `description` | string \| null | 否 | 说明 |
+| `owner_user_id` | string \| null | 否 | 创建人 |
+| `settings` | object | 是 | 应用设置 |
+| `created_time` | datetime | 是 | 创建时间 |
+| `updated_time` | datetime | 是 | 更新时间 |
+
+## 5. 前端迁移清单
+
+| 优先级 | 任务 | 说明 |
+| --- | --- | --- |
+| P0 | API Client 改造 | `apiClient.get/patch/delete` 全部迁移为 `post`,路径改为动作式 `/list`、`/update`、`/delete` |
+| P0 | 类型清理 | 前端类型删除 `version_no`、`agent_type`、`team_type`、`enabled`、直接 `*_json` 字段 |
+| P0 | Agent 真实运行 | 替换本地模拟 `AgentRuns`,接入 `/agents/runs/start`、`/poll`、`/cancel` |
+| P0 | MCP 闭环 | 工具页支持粘贴 MCP JSON、连接测试、发现工具、展示参数说明 |
+| P1 | Skills 接真实 API | 当前 Skills 页仍是 mock,需要接 `/skills/*` |
+| P1 | Knowledge mock 落库 | Jobs、Evaluation、Settings 需要接真实 API |
+| P1 | Teams 去版本化 | 团队配置合并到 `Team`,前端不排序 `version_no` |
+| P1 | Memory 展示收敛 | 继续避免 raw JSON,详情改成业务字段卡片 |
+| P2 | Workflow 设计器 | 挂路由并接入校验、调试 API |
+| P2 | 可观测性深度 | Dashboard 增加 trace、节点耗时、错误聚合 |

+ 1 - 1
services/tool-service/app/api/routes.py

@@ -71,7 +71,7 @@ def create_tool_version(
 
 @router.get("/versions", response_model=list[ToolVersionResponse])
 def list_tool_versions(
-    tool_id: str = Query(...),
+    tool_id: str | None = Query(default=None),
     service: ToolApplicationService = Depends(get_tool_application_service)) -> list[ToolVersionResponse]:
     return [
         ToolVersionResponse.from_entity(item)

+ 3 - 1
services/tool-service/app/application/services.py

@@ -50,7 +50,9 @@ class ToolApplicationService:
             timeout_ms=payload.timeout_ms,
             retry_policy_json=payload.retry_policy_json)
 
-    def list_tool_versions(self, tool_id: str) -> list[ToolVersion]:
+    def list_tool_versions(self, tool_id: str | None = None) -> list[ToolVersion]:
+        if tool_id is None:
+            return self.tool_version_repository.list_all()
         return self.tool_version_repository.list_by_tool(tool_id=tool_id)
 
     def create_tool_binding(self, payload: ToolBindingCreateRequest) -> ToolBinding:

+ 7 - 0
services/tool-service/app/domain/repositories.py

@@ -77,6 +77,13 @@ class ToolVersionRepository:
         )
         return list(self.db.scalars(stmt))
 
+    def list_all(self) -> list[ToolVersion]:
+        stmt = (
+            select(ToolVersion)
+            .order_by(ToolVersion.created_time.desc())
+        )
+        return list(self.db.scalars(stmt))
+
     def get_by_id(self, *, tool_version_id: str) -> ToolVersion | None:
         stmt = (
             select(ToolVersion)

+ 3 - 0
web/src/App.tsx

@@ -13,6 +13,7 @@ const LoginPage = lazy(() => import("@/pages/login/LoginPage").then((module) =>
 const DashboardPage = lazy(() => import("@/pages/dashboard/DashboardPage").then((module) => ({ default: module.DashboardPage })));
 const AgentListPage = lazy(() => import("@/pages/agents/AgentListPage").then((module) => ({ default: module.AgentListPage })));
 const SessionChatPage = lazy(() => import("@/pages/sessions/SessionChatPage").then((module) => ({ default: module.SessionChatPage })));
+const MemoryPage = lazy(() => import("@/pages/memories/MemoryPage").then((module) => ({ default: module.MemoryPage })));
 const ToolsPage = lazy(() => import("@/pages/tools/ToolsPage").then((module) => ({ default: module.ToolsPage })));
 const ModelsPage = lazy(() => import("@/pages/models/ModelsPage").then((module) => ({ default: module.ModelsPage })));
 const KnowledgePage = lazy(() => import("@/pages/knowledge/KnowledgePage").then((module) => ({ default: module.KnowledgePage })));
@@ -42,6 +43,7 @@ export default function App() {
               <Route path="/dashboard" element={<DashboardPage />} />
               <Route path="/agents" element={<AgentListPage />} />
               <Route path="/sessions" element={<SessionChatPage />} />
+              <Route path="/memories" element={<MemoryPage />} />
               <Route path="/tools" element={<ToolsPage />} />
               <Route path="/models" element={<ModelsPage />} />
               <Route path="/knowledge" element={<KnowledgePage />} />
@@ -74,6 +76,7 @@ function RoutePreloader() {
         import("@/pages/dashboard/DashboardPage"),
         import("@/pages/agents/AgentListPage"),
         import("@/pages/sessions/SessionChatPage"),
+        import("@/pages/memories/MemoryPage"),
         import("@/pages/tools/ToolsPage"),
         import("@/pages/models/ModelsPage"),
         import("@/pages/knowledge/KnowledgePage"),

+ 26 - 11
web/src/api/agents.ts

@@ -6,35 +6,53 @@ export async function listAgents() {
   return data;
 }
 
+export async function getAgent(id: string) {
+  const { data } = await apiClient.get<AgentDefinition>(`/agents/${id}`);
+  return data;
+}
+
 export async function createAgent(payload: {
-  code?: string;
   name: string;
   description?: string;
   agent_type: string;
   owner_user_id?: string;
   metadata_json?: JSONObject;
 }) {
-  const { data } = await apiClient.post<AgentDefinition>("/agents", {
-    ...payload,
-    code: payload.code || slugifyName(payload.name, "agent"),
-  });
+  const { data } = await apiClient.post<AgentDefinition>("/agents", payload);
   return data;
 }
 
-export async function listAgentVersions(agentId?: string) {
+export async function updateAgent(
+  id: string,
+  payload: {
+    name?: string;
+    description?: string;
+    metadata_json?: JSONObject;
+  }
+) {
+  const { data } = await apiClient.patch<AgentDefinition>(`/agents/${id}`, payload);
+  return data;
+}
+
+export async function deleteAgent(id: string) {
+  await apiClient.delete(`/agents/${id}`);
+}
+
+export async function listAgentConfigs(agentId?: string) {
   const { data } = await apiClient.get<AgentVersion[]>("/agents/versions", {
     params: { agent_id: agentId },
   });
   return data;
 }
 
-export async function createAgentVersion(payload: {
+export async function createAgentConfig(payload: {
   agent_id: string;
   role?: string;
   goal?: string | null;
   system_prompt?: string;
   model_config_json?: JSONObject;
   memory_policy_json?: JSONObject;
+  runtime_policy_json?: JSONObject;
   tool_refs_json?: JSONObject[];
   skill_refs_json?: JSONObject[];
   status?: "draft" | "published" | "deprecated";
@@ -45,6 +63,7 @@ export async function createAgentVersion(payload: {
     system_prompt: "",
     model_config_json: {},
     memory_policy_json: {},
+    runtime_policy_json: {},
     tool_refs_json: [],
     skill_refs_json: [],
     ...payload,
@@ -57,7 +76,3 @@ export async function listAgentRuns(agentId?: string) {
   return data;
 }
 
-function slugifyName(value: string, fallback: string) {
-  const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
-  return slug || `${fallback}_${Date.now().toString(36)}`;
-}

+ 2 - 0
web/src/api/index.ts

@@ -6,7 +6,9 @@ export * from "./agents";
 export * from "./sessions";
 export * from "./runtime";
 export * from "./tools";
+export * from "./skills";
 export * from "./knowledge";
 export * from "./teams";
 export * from "./model-providers";
 export * from "./models";
+export * from "./memories";

+ 9 - 9
web/src/api/knowledge.ts

@@ -2,6 +2,7 @@ import { apiClient } from "./client";
 import type {
   JSONObject,
   KnowledgeBase,
+  KnowledgeChunk,
   KnowledgeDocument,
   KnowledgeDocumentIngestResponse,
   KnowledgeDocumentParseResponse,
@@ -20,6 +21,13 @@ export async function listKnowledgeDocuments(knowledgeBaseId?: string) {
   return data;
 }
 
+export async function listKnowledgeChunks(knowledgeBaseId?: string, documentId?: string) {
+  const { data } = await apiClient.get<KnowledgeChunk[]>("/knowledge/chunks", {
+    params: { knowledge_base_id: knowledgeBaseId, document_id: documentId },
+  });
+  return data;
+}
+
 export async function searchKnowledge(knowledgeBaseId: string, query: string, topK = 5, filters: JSONObject = {}) {
   const { data } = await apiClient.post<SearchResult[]>("/knowledge/search", {
     knowledge_base_id: knowledgeBaseId,
@@ -41,15 +49,11 @@ export async function updateKnowledgeBaseStatus(payload: {
 }
 
 export async function createKnowledgeBase(payload: {
-  code?: string;
   name: string;
   description?: string | null;
   metadata_json?: JSONObject;
 }) {
-  const { data } = await apiClient.post<KnowledgeBase>("/knowledge/bases", {
-    ...payload,
-    code: payload.code || slugifyName(payload.name, "knowledge"),
-  });
+  const { data } = await apiClient.post<KnowledgeBase>("/knowledge/bases", payload);
   return data;
 }
 
@@ -78,7 +82,3 @@ export async function parseKnowledgeDocument(payload: {
   return data;
 }
 
-function slugifyName(value: string, fallback: string) {
-  const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
-  return slug || `${fallback}_${Date.now().toString(36)}`;
-}

+ 39 - 0
web/src/api/memories.ts

@@ -0,0 +1,39 @@
+import { apiClient } from "./client";
+import type {
+  MemoryCreateRequest,
+  MemoryItem,
+  MemoryScopeType,
+  MemorySearchRequest,
+  MemorySearchResult,
+  MemoryStatus,
+} from "@/types";
+
+export async function listMemories(params?: {
+  scope_type?: MemoryScopeType | null;
+  scope_id?: string | null;
+  status?: MemoryStatus | null;
+  limit?: number;
+}) {
+  const { data } = await apiClient.get<MemoryItem[]>("/memories", { params });
+  return data;
+}
+
+export async function createMemory(payload: MemoryCreateRequest) {
+  const { data } = await apiClient.post<MemoryItem>("/memories", {
+    metadata_json: {},
+    content_json: null,
+    importance_score: 0,
+    ...payload,
+  });
+  return data;
+}
+
+export async function searchMemories(payload: MemorySearchRequest) {
+  const { data } = await apiClient.post<MemorySearchResult[]>("/memories/search", payload);
+  return data;
+}
+
+export async function updateMemoryStatus(memoryId: string, status: MemoryStatus) {
+  const { data } = await apiClient.patch<MemoryItem>(`/memories/${memoryId}/status`, { status });
+  return data;
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 729 - 67
web/src/api/mock.ts


+ 0 - 6
web/src/api/models.ts

@@ -2,7 +2,6 @@ import { apiClient } from "./client";
 import type {
   ModelCreateRequest,
   ModelDefinition,
-  ModelStatus,
   ModelTestRequest,
   ModelTestResponse,
   ModelUpdateRequest,
@@ -23,11 +22,6 @@ export async function updateModel(modelId: string, payload: ModelUpdateRequest)
   return data;
 }
 
-export async function updateModelStatus(modelId: string, status: ModelStatus) {
-  const { data } = await apiClient.patch<ModelDefinition>(`/models/${modelId}/status`, { status });
-  return data;
-}
-
 export async function deleteModel(modelId: string) {
   await apiClient.delete(`/models/${modelId}`);
 }

+ 36 - 0
web/src/api/skills.ts

@@ -0,0 +1,36 @@
+import { apiClient } from "./client";
+import type { SkillDefinition, SkillVersion, SkillInstallation } from "@/types";
+
+export async function listSkills() {
+  const { data } = await apiClient.get<SkillDefinition[]>("/skills", { params: {} });
+  return data;
+}
+
+export async function getSkill(id: string) {
+  const { data } = await apiClient.get<SkillDefinition>(`/skills/${id}`);
+  return data;
+}
+
+export async function createSkill(payload: {
+  name: string;
+  skill_type?: string;
+  description?: string;
+}) {
+  const { data } = await apiClient.post<SkillDefinition>("/skills", {
+    skill_type: "template",
+    ...payload,
+  });
+  return data;
+}
+
+export async function listSkillVersions(skillId?: string) {
+  const { data } = await apiClient.get<SkillVersion[]>("/skills/versions", {
+    params: { skill_id: skillId },
+  });
+  return data;
+}
+
+export async function listSkillInstallations() {
+  const { data } = await apiClient.get<SkillInstallation[]>("/skills/installations", { params: {} });
+  return data;
+}

+ 3 - 12
web/src/api/teams.ts

@@ -6,12 +6,12 @@ export async function listTeams() {
   return data;
 }
 
-export async function listTeamVersions(teamId?: string) {
+export async function listTeamConfigs(teamId?: string) {
   const { data } = await apiClient.get<TeamVersion[]>("/teams/versions", { params: { team_id: teamId } });
   return data;
 }
 
-export async function createTeamVersion(payload: {
+export async function createTeamConfig(payload: {
   team_id: string;
   coordination_mode: string;
   objective?: string | null;
@@ -44,21 +44,12 @@ export async function createTeamRun(payload: {
 }
 
 export async function createTeam(payload: {
-  code?: string;
   name: string;
   description?: string | null;
   team_type: string;
   owner_user_id?: string | null;
   metadata_json?: JSONObject;
 }) {
-  const { data } = await apiClient.post<TeamDefinition>("/teams", {
-    ...payload,
-    code: payload.code || slugifyName(payload.name, "team"),
-  });
+  const { data } = await apiClient.post<TeamDefinition>("/teams", payload);
   return data;
 }
-
-function slugifyName(value: string, fallback: string) {
-  const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
-  return slug || `${fallback}_${Date.now().toString(36)}`;
-}

+ 103 - 13
web/src/api/tools.ts

@@ -6,12 +6,42 @@ export async function listTools() {
   return data;
 }
 
-export async function listToolVersions(toolId?: string) {
+export async function getTool(id: string) {
+  const { data } = await apiClient.get<ToolDefinition>(`/tools/${id}`);
+  return data;
+}
+
+export async function createTool(payload: {
+  plugin_id?: string | null;
+  name: string;
+  tool_type: string;
+  description?: string | null;
+}) {
+  const { data } = await apiClient.post<ToolDefinition>("/tools", payload);
+  return data;
+}
+
+export async function updateTool(
+  id: string,
+  payload: {
+    name?: string;
+    description?: string | null;
+  }
+) {
+  const { data } = await apiClient.patch<ToolDefinition>(`/tools/${id}`, payload);
+  return data;
+}
+
+export async function deleteTool(id: string) {
+  await apiClient.delete(`/tools/${id}`);
+}
+
+export async function listToolConnections(toolId?: string) {
   const { data } = await apiClient.get<ToolVersion[]>("/tools/versions", { params: { tool_id: toolId } });
   return data;
 }
 
-export async function createToolVersion(payload: {
+export async function createToolConnection(payload: {
   tool_id: string;
   timeout_ms?: number | null;
   input_schema_json?: JSONObject;
@@ -29,30 +59,74 @@ export async function createToolVersion(payload: {
   return data;
 }
 
+export async function updateToolConnection(
+  id: string,
+  payload: {
+    timeout_ms?: number | null;
+    input_schema_json?: JSONObject;
+    output_schema_json?: JSONObject;
+    invoke_config_json?: JSONObject;
+    retry_policy_json?: JSONObject;
+  }
+) {
+  const { data } = await apiClient.patch<ToolVersion>(`/tools/versions/${id}`, payload);
+  return data;
+}
+
+export async function deleteToolConnection(id: string) {
+  await apiClient.delete(`/tools/versions/${id}`);
+}
+
 export async function listToolBindings(appId?: string) {
   const { data } = await apiClient.get<ToolBinding[]>("/tools/bindings", { params: { app_id: appId } });
   return data;
 }
 
-export async function createTool(payload: {
-  plugin_id?: string | null;
-  code?: string;
-  name: string;
-  tool_type: string;
-  description?: string | null;
+export async function getToolBinding(id: string) {
+  const { data } = await apiClient.get<ToolBinding>(`/tools/bindings/${id}`);
+  return data;
+}
+
+export async function createToolBinding(payload: {
+  app_id: string;
+  tool_version_id: string;
+  credential_id?: string | null;
+  binding_scope?: string;
+  config_json?: JSONObject;
 }) {
-  const { data } = await apiClient.post<ToolDefinition>("/tools", {
+  const { data } = await apiClient.post<ToolBinding>("/tools/bindings", {
+    binding_scope: "app",
     ...payload,
-    code: payload.code || slugifyName(payload.name, "tool"),
   });
   return data;
 }
 
+export async function updateToolBinding(
+  id: string,
+  payload: {
+    credential_id?: string | null;
+    binding_scope?: string;
+    config_json?: JSONObject;
+  }
+) {
+  const { data } = await apiClient.patch<ToolBinding>(`/tools/bindings/${id}`, payload);
+  return data;
+}
+
+export async function deleteToolBinding(id: string) {
+  await apiClient.delete(`/tools/bindings/${id}`);
+}
+
 export async function listToolCredentials() {
   const { data } = await apiClient.get<ToolCredential[]>("/tools/credentials", { params: {} });
   return data;
 }
 
+export async function getToolCredential(id: string) {
+  const { data } = await apiClient.get<ToolCredential>(`/tools/credentials/${id}`);
+  return data;
+}
+
 export async function createToolCredential(payload: {
   name: string;
   credential_type: string;
@@ -63,7 +137,23 @@ export async function createToolCredential(payload: {
   return data;
 }
 
-function slugifyName(value: string, fallback: string) {
-  const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
-  return slug || `${fallback}_${Date.now().toString(36)}`;
+export async function updateToolCredential(
+  id: string,
+  payload: {
+    name?: string;
+    metadata_json?: JSONObject;
+  }
+) {
+  const { data } = await apiClient.patch<ToolCredential>(`/tools/credentials/${id}`, payload);
+  return data;
 }
+
+export async function deleteToolCredential(id: string) {
+  await apiClient.delete(`/tools/credentials/${id}`);
+}
+
+export async function revealToolCredential(id: string) {
+  const { data } = await apiClient.post<{ secret_json: JSONObject }>(`/tools/credentials/${id}/reveal`);
+  return data;
+}
+

+ 3 - 1
web/src/components/layout/AppLayout.tsx

@@ -1,12 +1,14 @@
 import { Outlet } from "react-router-dom";
+import { useTranslation } from "react-i18next";
 import { Header } from "./Header";
 import { MobileSidebar, Sidebar } from "./Sidebar";
 
 export function AppLayout() {
+  const { t } = useTranslation();
   return (
     <div className="flex min-h-screen bg-surface-base">
       <a href="#main-content" className="skip-link">
-        Skip to main content
+        {t("nav.skipToContent")}
       </a>
       <Sidebar />
       <MobileSidebar />

+ 29 - 3
web/src/components/layout/Breadcrumb.tsx

@@ -1,21 +1,23 @@
 import { Link, useLocation } from "react-router-dom";
 import { ChevronRight } from "lucide-react";
+import { useTranslation } from "react-i18next";
 
 export function Breadcrumb() {
+  const { t } = useTranslation();
   const { pathname } = useLocation();
   const parts = pathname.split("/").filter(Boolean);
   return (
     <div className="flex items-center gap-1 text-sm text-muted-foreground">
       <Link to="/dashboard" className="hover:text-foreground">
-        Studio
+        {t("app.name")}
       </Link>
       {parts.map((part, index) => {
         const href = `/${parts.slice(0, index + 1).join("/")}`;
         return (
           <span key={href} className="flex items-center gap-1">
             <ChevronRight className="h-3 w-3" />
-            <Link to={href} className="capitalize hover:text-foreground">
-              {part.replace(/-/g, " ")}
+            <Link to={href} className="hover:text-foreground">
+              {crumbLabel(part, parts, index, t)}
             </Link>
           </span>
         );
@@ -23,3 +25,27 @@ export function Breadcrumb() {
     </div>
   );
 }
+
+function crumbLabel(part: string, parts: string[], index: number, t: ReturnType<typeof useTranslation>["t"]) {
+  const routeKeys: Record<string, string> = {
+    dashboard: "nav.dashboard",
+    agents: "nav.agents",
+    sessions: "nav.sessions",
+    memories: "nav.memories",
+    tools: "nav.tools",
+    knowledge: "nav.knowledge",
+    teams: "nav.teams",
+    skills: "nav.skills",
+    models: "nav.models",
+    settings: "nav.settings",
+  };
+  if (index > 0 && parts[0] === "knowledge") {
+    return t(`knowledge.sections.${part}`, humanizeCrumb(part));
+  }
+  const key = routeKeys[part];
+  return key ? t(key) : humanizeCrumb(part);
+}
+
+function humanizeCrumb(value: string) {
+  return value.replace(/-/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
+}

+ 1 - 1
web/src/components/layout/Header.tsx

@@ -16,7 +16,7 @@ export function Header() {
   return (
     <header className="glass sticky top-0 z-30 flex h-16 items-center justify-between px-4 md:px-6">
       <div className="flex items-center gap-3">
-        <Button className="md:hidden" variant="ghost" size="icon" onClick={toggleMobileSidebar} aria-label="Open navigation">
+        <Button className="md:hidden" variant="ghost" size="icon" onClick={toggleMobileSidebar} aria-label={t("nav.openNavigation")}>
           <Menu className="h-4 w-4" />
         </Button>
         <Breadcrumb />

+ 3 - 3
web/src/components/layout/Sidebar.tsx

@@ -11,7 +11,7 @@ function NavItems({ collapsed, onNavigate }: { collapsed?: boolean; onNavigate?:
   const { t } = useTranslation();
   return (
     <>
-      {NAV_ITEMS.map((item, index) => (
+      {NAV_ITEMS.map((item) => (
         <NavLink
           key={item.path}
           to={item.path}
@@ -20,7 +20,7 @@ function NavItems({ collapsed, onNavigate }: { collapsed?: boolean; onNavigate?:
             cn(
               "flex min-h-11 items-center gap-3 rounded-md px-3 text-sm text-muted-foreground transition hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary",
               isActive && "bg-primary/15 text-primary",
-              index === 8 && "mt-auto",
+              item.path === "/settings" && "mt-auto",
               collapsed && "justify-center px-0",
             )
           }
@@ -72,7 +72,7 @@ export function MobileSidebar() {
     <div className="fixed inset-0 z-50 md:hidden">
       <button
         className="absolute inset-0 bg-black/45"
-        aria-label="Close navigation"
+        aria-label={t("nav.closeNavigation")}
         onClick={() => setMobileSidebarOpen(false)}
       />
       <aside className="relative flex h-full w-[min(82vw,320px)] flex-col border-r border-border bg-surface-deep shadow-glow">

+ 6 - 3
web/src/components/shared/ApiErrorState.tsx

@@ -1,17 +1,20 @@
 import { AlertTriangle } from "lucide-react";
+import { useTranslation } from "react-i18next";
 import { Button } from "@/components/ui/button";
 
 export function ApiErrorState({ message, onRetry }: { message?: string; onRetry?: () => void }) {
+  const { t } = useTranslation();
+
   return (
     <div className="rounded-md border border-red-500/20 bg-red-500/10 p-5">
       <div className="flex items-start gap-3">
         <AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-red-700 dark:text-red-300" />
         <div className="min-w-0 flex-1">
-          <h2 className="text-sm font-semibold text-red-100">Unable to load data</h2>
-          <p className="mt-1 text-sm text-red-100/70">{message ?? "Check the gateway connection and credentials."}</p>
+          <h2 className="text-sm font-semibold text-red-100">{t("errors.unableToLoadData")}</h2>
+          <p className="mt-1 text-sm text-red-100/70">{message ?? t("errors.checkGatewayConnection")}</p>
           {onRetry ? (
             <Button className="mt-4" variant="outline" onClick={onRetry}>
-              Retry
+              {t("common.retry")}
             </Button>
           ) : null}
         </div>

+ 4 - 2
web/src/components/shared/ConfirmDialog.tsx

@@ -1,5 +1,6 @@
 import { Dialog } from "@/components/ui/dialog";
 import { Button } from "@/components/ui/button";
+import { useTranslation } from "react-i18next";
 
 export function ConfirmDialog({
   open,
@@ -14,11 +15,12 @@ export function ConfirmDialog({
   description: string;
   onConfirm: () => void;
 }) {
+  const { t } = useTranslation();
   return (
     <Dialog open={open} onOpenChange={onOpenChange} title={title} description={description}>
       <div className="flex justify-end gap-2">
         <Button variant="ghost" onClick={() => onOpenChange(false)}>
-          Cancel
+          {t("common.cancel")}
         </Button>
         <Button
           variant="destructive"
@@ -27,7 +29,7 @@ export function ConfirmDialog({
             onOpenChange(false);
           }}
         >
-          Confirm
+          {t("common.confirm")}
         </Button>
       </div>
     </Dialog>

+ 3 - 2
web/src/components/shared/ErrorBoundary.tsx

@@ -1,5 +1,6 @@
 import * as React from "react";
 import { Button } from "@/components/ui/button";
+import i18n from "@/i18n";
 
 export class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { error?: Error }> {
   state: { error?: Error } = {};
@@ -12,10 +13,10 @@ export class ErrorBoundary extends React.Component<{ children: React.ReactNode }
     if (this.state.error) {
       return (
         <div className="rounded-md border border-red-500/20 bg-red-500/10 p-6">
-          <h2 className="text-lg font-semibold text-red-200">Something broke</h2>
+          <h2 className="text-lg font-semibold text-red-200">{i18n.t("errors.somethingBroke")}</h2>
           <p className="mt-2 text-sm text-red-100/70">{this.state.error.message}</p>
           <Button className="mt-4" onClick={() => this.setState({ error: undefined })}>
-            Retry
+            {i18n.t("common.retry")}
           </Button>
         </div>
       );

+ 3 - 1
web/src/components/shared/JsonViewer.tsx

@@ -1,7 +1,9 @@
 import * as React from "react";
+import { useTranslation } from "react-i18next";
 import { Button } from "@/components/ui/button";
 
 export function JsonViewer({ value, collapsed = true }: { value: unknown; collapsed?: boolean }) {
+  const { t } = useTranslation();
   const [open, setOpen] = React.useState(!collapsed);
   const text = JSON.stringify(value ?? {}, null, 2);
   return (
@@ -9,7 +11,7 @@ export function JsonViewer({ value, collapsed = true }: { value: unknown; collap
       <div className="flex items-center justify-between border-b border-border px-3 py-2">
         <span className="font-mono text-xs text-muted-foreground">JSON</span>
         <Button size="sm" variant="ghost" onClick={() => setOpen((value) => !value)}>
-          {open ? "Collapse" : "Expand"}
+          {open ? t("common.collapse") : t("common.expand")}
         </Button>
       </div>
       {open ? <pre className="max-h-96 overflow-auto p-3 font-mono text-xs text-foreground">{text}</pre> : null}

+ 4 - 1
web/src/components/shared/SearchInput.tsx

@@ -1,17 +1,20 @@
 import { Search } from "lucide-react";
 import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
 
 export function SearchInput({
   value,
   onChange,
   placeholder = "Search",
+  className,
 }: {
   value: string;
   onChange: (value: string) => void;
   placeholder?: string;
+  className?: string;
 }) {
   return (
-    <label className="relative block w-full sm:w-72">
+    <label className={cn("relative block w-full sm:w-72", className)}>
       <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
       <Input
         aria-label={placeholder}

+ 9 - 1
web/src/components/shared/StatusBadge.tsx

@@ -1,13 +1,21 @@
 import { Badge } from "@/components/ui/badge";
 import { STATUS_COLOR_MAP } from "@/lib/constants";
 import { cn } from "@/lib/utils";
+import { useTranslation } from "react-i18next";
 
 export function StatusBadge({ status }: { status?: string | null }) {
+  const { t } = useTranslation();
   const value = status ?? "unknown";
   return (
     <Badge className={cn(STATUS_COLOR_MAP[value] ?? "border-border bg-muted/50 text-muted-foreground")}>
       {value === "running" ? <span className="mr-1 h-1.5 w-1.5 rounded-full bg-current motion-safe:animate-pulse" /> : null}
-      {value}
+      {t(`status.${value}`, humanizeStatus(value))}
     </Badge>
   );
 }
+
+function humanizeStatus(value: string) {
+  return value
+    .replace(/_/g, " ")
+    .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}

+ 47 - 0
web/src/components/ui/collapsible.tsx

@@ -0,0 +1,47 @@
+import * as React from "react";
+import { ChevronRight } from "lucide-react";
+
+interface CollapsibleProps {
+  title: string;
+  count?: number;
+  defaultOpen?: boolean;
+  icon?: React.ReactNode;
+  badge?: React.ReactNode;
+  children: React.ReactNode;
+}
+
+export function Collapsible({
+  title,
+  count,
+  defaultOpen = true,
+  icon,
+  badge,
+  children,
+}: CollapsibleProps) {
+  const [open, setOpen] = React.useState(defaultOpen);
+
+  return (
+    <div className="rounded-lg border border-border bg-muted/10">
+      <button
+        type="button"
+        className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/20"
+        onClick={() => setOpen((prev) => !prev)}
+      >
+        <ChevronRight
+          className={`h-4 w-4 shrink-0 text-muted-foreground transition-transform ${
+            open ? "rotate-90" : ""
+          }`}
+        />
+        {icon && <span className="shrink-0 text-muted-foreground">{icon}</span>}
+        <span className="text-sm font-semibold">{title}</span>
+        {count !== undefined && (
+          <span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
+            {count}
+          </span>
+        )}
+        {badge && <span className="ml-auto">{badge}</span>}
+      </button>
+      {open && <div className="border-t border-border px-4 py-4">{children}</div>}
+    </div>
+  );
+}

+ 3 - 1
web/src/components/ui/dialog.tsx

@@ -1,4 +1,5 @@
 import * as React from "react";
+import { useTranslation } from "react-i18next";
 import { X } from "lucide-react";
 import { Button } from "./button";
 import { cn } from "@/lib/utils";
@@ -13,6 +14,7 @@ interface DialogProps {
 }
 
 export function Dialog({ open, onOpenChange, title, description, children, className }: DialogProps) {
+  const { t } = useTranslation();
   const titleId = React.useId();
 
   React.useEffect(() => {
@@ -59,7 +61,7 @@ export function Dialog({ open, onOpenChange, title, description, children, class
             <h2 id={titleId} className="text-lg font-semibold">{title}</h2>
             {description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
           </div>
-          <Button variant="ghost" size="icon" onClick={() => onOpenChange(false)} aria-label="Close">
+          <Button variant="ghost" size="icon" onClick={() => onOpenChange(false)} aria-label={t("common.close")}>
             <X className="h-4 w-4" />
           </Button>
         </div>

+ 16 - 14
web/src/components/ui/tabs.tsx

@@ -12,21 +12,23 @@ export function Tabs({
 }) {
   const active = tabs.find((tab) => tab.value === value) ?? tabs[0];
   return (
-    <div>
-      <div className="flex rounded-md border border-border bg-muted/40 p-1">
-        {tabs.map((tab) => (
-          <button
-            key={tab.value}
-            className={cn(
-              "flex-1 rounded-sm px-3 py-1.5 text-sm text-muted-foreground transition",
-              value === tab.value && "bg-muted text-foreground")}
-            onClick={() => onChange(tab.value)}
-          >
-            {tab.label}
-          </button>
-        ))}
+    <div className="min-w-0">
+      <div className="overflow-x-auto rounded-md border border-border bg-muted/40 p-1">
+        <div className="flex min-w-max gap-1">
+          {tabs.map((tab) => (
+            <button
+              key={tab.value}
+              className={cn(
+                "shrink-0 rounded-sm px-3 py-1.5 text-sm text-muted-foreground transition",
+                value === tab.value && "bg-muted text-foreground")}
+              onClick={() => onChange(tab.value)}
+            >
+              {tab.label}
+            </button>
+          ))}
+        </div>
       </div>
-      <div className="mt-4">{active?.content}</div>
+      <div className="mt-4 min-w-0">{active?.content}</div>
     </div>
   );
 }

+ 3 - 3
web/src/hooks/useAgents.ts

@@ -1,8 +1,8 @@
-import { listAgentRuns, listAgentVersions, listAgents } from "@/api";
+import { listAgentConfigs, listAgentRuns, listAgents } from "@/api";
 import { useApi } from "./useApi";
 
 export const useAgentList = () => useApi(() => listAgents(), []);
-export const useAgentVersions = (agentId?: string) =>
-  useApi(() => (agentId ? listAgentVersions(agentId) : Promise.resolve([])), [agentId]);
+export const useAgentConfigs = (agentId?: string) =>
+  useApi(() => (agentId ? listAgentConfigs(agentId) : Promise.resolve([])), [agentId]);
 export const useAgentRuns = (agentId?: string) =>
   useApi(() => (agentId ? listAgentRuns(agentId) : Promise.resolve([])), [agentId]);

+ 7 - 3
web/src/lib/constants.ts

@@ -1,9 +1,11 @@
 import {
   Bot,
   BookOpen,
-  Cpu,
+  Brain,
+  BrainCircuit,
   LayoutDashboard,
   MessageSquare,
+  Puzzle,
   Settings,
   Users,
   Wrench,
@@ -15,6 +17,7 @@ export const ROUTE_PATHS = {
   dashboard: "/dashboard",
   agents: "/agents",
   sessions: "/sessions",
+  memories: "/memories",
   tools: "/tools",
   knowledge: "/knowledge",
   teams: "/teams",
@@ -27,11 +30,12 @@ export const NAV_ITEMS: Array<{ labelKey: string; path: string; icon: LucideIcon
   { labelKey: "nav.dashboard", path: ROUTE_PATHS.dashboard, icon: LayoutDashboard },
   { labelKey: "nav.agents", path: ROUTE_PATHS.agents, icon: Bot },
   { labelKey: "nav.sessions", path: ROUTE_PATHS.sessions, icon: MessageSquare },
+  { labelKey: "nav.memories", path: ROUTE_PATHS.memories, icon: Brain },
   { labelKey: "nav.tools", path: ROUTE_PATHS.tools, icon: Wrench },
   { labelKey: "nav.knowledge", path: ROUTE_PATHS.knowledge, icon: BookOpen },
   { labelKey: "nav.teams", path: ROUTE_PATHS.teams, icon: Users },
-  { labelKey: "nav.skills", path: ROUTE_PATHS.skills, icon: Cpu },
-  { labelKey: "nav.models", path: ROUTE_PATHS.models, icon: Cpu },
+  { labelKey: "nav.skills", path: ROUTE_PATHS.skills, icon: Puzzle },
+  { labelKey: "nav.models", path: ROUTE_PATHS.models, icon: BrainCircuit },
   { labelKey: "nav.settings", path: ROUTE_PATHS.settings, icon: Settings },
 ];
 

+ 55 - 0
web/src/lib/demo-text.ts

@@ -0,0 +1,55 @@
+import type { TFunction } from "i18next";
+
+const demoTextKeys: Record<string, string> = {
+  "Support Agent": "supportAgent",
+  "Researcher": "researcher",
+  "Handles first-response customer support.": "supportAgentDescription",
+  "Finds, compares, and summarizes knowledge sources.": "researcherDescription",
+  "Local Chat": "localChat",
+  "Cloud Primary": "cloudPrimary",
+  "Local OpenAI-compatible chat endpoint.": "localChatDescription",
+  "Cloud fallback model configuration.": "cloudPrimaryDescription",
+  "Product Docs": "productDocs",
+  "Public product and support documentation.": "productDocsDescription",
+  "Billing and Invoice FAQ": "billingInvoiceFaq",
+  "GitHub MCP Server": "githubMcpServer",
+  "Slack MCP Server": "slackMcpServer",
+  "Database MCP Server": "databaseMcpServer",
+  "Filesystem MCP Server": "filesystemMcpServer",
+  "Web Search MCP Server": "webSearchMcpServer",
+  "GitHub API integration via MCP protocol for repository management.": "githubMcpDescription",
+  "Slack workspace integration for messaging and channel management.": "slackMcpDescription",
+  "Database query and management tools via MCP protocol.": "databaseMcpDescription",
+  "File system operations for reading, writing, and managing files.": "filesystemMcpDescription",
+  "Web search and content extraction capabilities.": "webSearchMcpDescription",
+  "List repositories for the authenticated user": "listRepos",
+  "Get a repository by owner and name": "getRepo",
+  "Create a new issue in a repository": "createIssue",
+  "List issues in a repository": "listIssues",
+  "Search repository content": "searchRepositoryContent",
+  "Send a message to a channel": "sendMessage",
+  "List all channels in the workspace": "listChannels",
+  "Get message history for a channel": "getChannelHistory",
+  "Upload a file to a channel": "uploadFile",
+  "Execute a SQL query": "executeSql",
+  "List all tables in the database": "listTables",
+  "Get schema of a table": "describeTable",
+  "Insert a row into a table": "insertRow",
+  "Update rows in a table": "updateRows",
+  "Delete rows from a table": "deleteRows",
+  "Read content of a file": "readFile",
+  "Write content to a file": "writeFile",
+  "List files and directories": "listDirectory",
+  "Create a new directory": "createDirectory",
+  "Delete a file": "deleteFile",
+  "Move or rename a file": "moveFile",
+  "Search the web for information": "webSearch",
+  "Extract content from a URL": "extractContent",
+  "Summarize a web page": "summarizePage",
+};
+
+export function demoText(value: string | null | undefined, t: TFunction) {
+  if (!value) return value ?? "";
+  const key = demoTextKeys[value];
+  return key ? t(`demo.${key}`, value) : value;
+}

+ 10 - 20
web/src/lib/utils.ts

@@ -6,29 +6,19 @@ export function cn(...inputs: ClassValue[]) {
 }
 
 export function formatDateTime(value?: string | null) {
-  if (!value) return "Never";
-  return new Intl.DateTimeFormat(undefined, {
-    month: "short",
-    day: "numeric",
-    hour: "2-digit",
-    minute: "2-digit",
-  }).format(new Date(value));
+  if (!value) return "-";
+  const date = new Date(value);
+  if (Number.isNaN(date.getTime())) return "-";
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+  const hour = String(date.getHours()).padStart(2, "0");
+  const minute = String(date.getMinutes()).padStart(2, "0");
+  return `${year}-${month}-${day} ${hour}:${minute}`;
 }
 
 export function relativeTime(value?: string | null) {
-  if (!value) return "Never";
-  const deltaSeconds = Math.round((new Date(value).getTime() - Date.now()) / 1000);
-  const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
-    ["year", 31536000],
-    ["month", 2592000],
-    ["day", 86400],
-    ["hour", 3600],
-    ["minute", 60],
-    ["second", 1],
-  ];
-  const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
-  const [unit, seconds] = units.find(([, amount]) => Math.abs(deltaSeconds) >= amount) ?? ["second", 1];
-  return formatter.format(Math.round(deltaSeconds / seconds), unit);
+  return formatDateTime(value);
 }
 
 export function truncateMiddle(value: string, max = 18) {

+ 511 - 205
web/src/locales/en.json

@@ -18,13 +18,12 @@
     "close": "Close",
     "refresh": "Refresh",
     "copy": "Copy",
-    "copyCode": "Copy Code",
+    "copyId": "Copy ID",
     "clear": "Clear",
     "clearFilters": "Clear filters",
     "details": "Details",
     "overview": "Overview",
     "settings": "Settings",
-    "createVersion": "Create Version",
     "start": "Start",
     "stop": "Stop",
     "run": "Run",
@@ -34,13 +33,11 @@
     "starting": "Starting...",
     "createNew": "New",
     "noResults": "No results",
-    "version": "Version",
-    "versions": "Versions",
     "status": "Status",
     "type": "Type",
     "name": "Name",
     "description": "Description",
-    "code": "Code",
+    "identifier": "ID",
     "created": "Created",
     "updated": "Updated",
     "actions": "Actions",
@@ -54,8 +51,6 @@
     "selectAll": "Select all",
     "required": "Required",
     "optional": "Optional",
-    "enabled": "Enabled",
-    "disabled": "Disabled",
     "yes": "Yes",
     "no": "No",
     "queued": "Queued",
@@ -69,24 +64,102 @@
     "gridView": "Grid view",
     "list": "List",
     "grid": "Grid",
-    "filterRunsByStatus": "Filter runs by status"
+    "filterRunsByStatus": "Filter runs by status",
+    "saving": "Saving...",
+    "add": "Add",
+    "notSet": "Not set",
+    "configured": "Configured",
+    "collapse": "Collapse",
+    "expand": "Expand"
+  },
+  "status": {
+    "unknown": "Unknown",
+    "pending": "Pending",
+    "queued": "Queued",
+    "running": "Running",
+    "completed": "Completed",
+    "active": "Active",
+    "indexed": "Indexed",
+    "ok": "OK",
+    "failed": "Failed",
+    "disabled": "Disabled",
+    "draft": "Draft",
+    "archived": "Archived",
+    "cancelled": "Cancelled",
+    "paused": "Paused",
+    "published": "Published",
+    "deprecated": "Deprecated",
+    "passed": "Passed",
+    "revoked": "Revoked",
+    "closed": "Closed"
   },
   "nav": {
     "dashboard": "Dashboard",
     "agents": "Agents",
     "sessions": "Sessions",
+    "memories": "Memory",
     "tools": "Tools",
     "knowledge": "Knowledge",
     "teams": "Teams",
     "skills": "Skills",
     "models": "Model Providers",
     "settings": "Settings",
-    "collapse": "Collapse"
+    "collapse": "Collapse",
+    "skipToContent": "Skip to main content",
+    "openNavigation": "Open navigation",
+    "closeNavigation": "Close navigation"
   },
   "app": {
     "name": "Auto Platform",
     "loadingStudio": "Loading studio"
   },
+  "demo": {
+    "supportAgent": "Support Agent",
+    "researcher": "Researcher",
+    "supportAgentDescription": "Handles first-response customer support.",
+    "researcherDescription": "Finds, compares, and summarizes knowledge sources.",
+    "localChat": "Local Chat",
+    "cloudPrimary": "Cloud Primary",
+    "localChatDescription": "Local OpenAI-compatible chat endpoint.",
+    "cloudPrimaryDescription": "Cloud fallback model configuration.",
+    "productDocs": "Product Docs",
+    "productDocsDescription": "Public product and support documentation.",
+    "billingInvoiceFaq": "Billing and Invoice FAQ",
+    "githubMcpServer": "GitHub MCP Server",
+    "slackMcpServer": "Slack MCP Server",
+    "databaseMcpServer": "Database MCP Server",
+    "filesystemMcpServer": "Filesystem MCP Server",
+    "webSearchMcpServer": "Web Search MCP Server",
+    "githubMcpDescription": "GitHub API integration via MCP protocol for repository management.",
+    "slackMcpDescription": "Slack workspace integration for messaging and channel management.",
+    "databaseMcpDescription": "Database query and management tools via MCP protocol.",
+    "filesystemMcpDescription": "File system operations for reading, writing, and managing files.",
+    "webSearchMcpDescription": "Web search and content extraction capabilities.",
+    "listRepos": "List repositories for the authenticated user",
+    "getRepo": "Get a repository by owner and name",
+    "createIssue": "Create a new issue in a repository",
+    "listIssues": "List issues in a repository",
+    "searchRepositoryContent": "Search repository content",
+    "sendMessage": "Send a message to a channel",
+    "listChannels": "List all channels in the workspace",
+    "getChannelHistory": "Get message history for a channel",
+    "uploadFile": "Upload a file to a channel",
+    "executeSql": "Execute a SQL query",
+    "listTables": "List all tables in the database",
+    "describeTable": "Get schema of a table",
+    "insertRow": "Insert a row into a table",
+    "updateRows": "Update rows in a table",
+    "deleteRows": "Delete rows from a table",
+    "readFile": "Read content of a file",
+    "writeFile": "Write content to a file",
+    "listDirectory": "List files and directories",
+    "createDirectory": "Create a new directory",
+    "deleteFile": "Delete a file",
+    "moveFile": "Move or rename a file",
+    "webSearch": "Search the web for information",
+    "extractContent": "Extract content from a URL",
+    "summarizePage": "Summarize a web page"
+  },
   "theme": {
     "switchToLight": "Switch to light mode",
     "switchToDark": "Switch to dark mode"
@@ -108,7 +181,11 @@
     "showKey": "Show API key",
     "hideKey": "Hide API key",
     "connected": "Connected to gateway",
-    "rejected": "Gateway rejected the credentials"
+    "rejected": "Gateway rejected the credentials",
+    "username": "Username",
+    "password": "Password",
+    "showPassword": "Show password",
+    "hidePassword": "Hide password"
   },
   "dashboard": {
     "title": "Dashboard",
@@ -120,11 +197,57 @@
     "serviceHealth": "Service Health",
     "recentRuns": "Recent Runs",
     "allRequestsFailed": "All dashboard requests failed. Check the API gateway and credentials.",
-    "services": "Services"
+    "services": "Services",
+    "liveWorkspace": "Live workspace",
+    "commandCenter": "Agent operations command center",
+    "commandCenterDescription": "Track execution health, workflow throughput, and service readiness from one operational surface.",
+    "readiness": "Readiness",
+    "successRate": "Success rate",
+    "serviceUptime": "Services ok",
+    "latestRun": "Latest run",
+    "nodes": "Nodes",
+    "activeAgents": "{{count}} active",
+    "executionVolume": "Execution volume today",
+    "liveConversations": "Live conversations",
+    "completedVsFailed": "Completed vs failed runs",
+    "avgNodes": "Avg nodes",
+    "avgNodesDetail": "Workflow depth per run",
+    "healthScore": "Health score",
+    "healthScoreDetail": "Derived from recent run quality",
+    "activityFeed": "Activity feed",
+    "activityFeedDescription": "Newest workflow runs and their current execution state.",
+    "noRecentActivity": "No recent run activity",
+    "executionTrendDescription": "Seven-day completed and failed workflow volume.",
+    "recentRunsDescription": "Latest runtime records with trigger, priority, and workflow depth.",
+    "noRuns": "No workflow runs yet",
+    "trigger": "Trigger",
+    "priority": "Priority",
+    "runMix": "Run mix",
+    "runMixDescription": "Workflow composition, live load, and failure pressure.",
+    "totalRuns": "Total runs",
+    "liveRuns": "Live runs",
+    "failedRuns": "Failed runs",
+    "noRunMix": "No run types yet",
+    "serviceHealthDescription": "Gateway dependencies and downstream availability.",
+    "serviceReadiness": "Service readiness",
+    "servicesReady": "{{healthy}} of {{total}} services ready",
+    "noServices": "No downstream services reported",
+    "runTypes": {
+      "main": "Main",
+      "scheduled": "Scheduled"
+    },
+    "triggers": {
+      "chat": "Chat",
+      "scheduler": "Scheduler",
+      "chat_debug": "Chat debug",
+      "tool_loop": "Tool loop",
+      "human_review": "Human review",
+      "research_digest": "Research digest"
+    }
   },
   "agents": {
     "title": "Agents",
-    "description": "Design, inspect, and operate agent definitions, versions, prompts, and runs from one workspace.",
+    "description": "Design, inspect, and operate agent definitions, configs, prompts, and runs from one workspace.",
     "create": "Create Agent",
     "newAgent": "New Agent",
     "empty": "No agents",
@@ -132,7 +255,7 @@
     "refresh": "Refresh",
     "agentDirectory": "Agent Directory",
     "agentsShown": "of {{count}} agents shown",
-    "searchByNameCodeType": "Search by name, code, type, or description",
+    "searchByNameType": "Search by name, id, type, or description",
     "allStatuses": "All statuses",
     "allTypes": "All types",
     "newestFirst": "Newest first",
@@ -145,8 +268,7 @@
     "adjustFiltersAgent": "Adjust search or filters to find a matching agent definition.",
     "agentDetails": "Agent Details",
     "selectAgent": "Select an agent to inspect its operating surface.",
-    "copyCode": "Copy Code",
-    "newVersion": "New Version",
+    "copyId": "Copy ID",
     "runs": "Runs",
     "testInput": "Test input",
     "resultPreview": "Result Preview",
@@ -155,8 +277,7 @@
     "noDescription": "No description provided.",
     "selectAnAgent": "Select an agent",
     "noAgents": "No agents",
-    "createAgentStart": "Create an agent to start building definitions and versions.",
-    "versions": "Versions",
+    "createAgentStart": "Create an agent to start building definitions and configs.",
     "failures": "Failures",
     "latest": "Latest",
     "none": "None",
@@ -171,10 +292,6 @@
     "memoryPolicy": "Memory Policy",
     "toolReferences": "Tool References",
     "skillReferences": "Skill References",
-    "noConfig": "No config",
-    "publishOrDraft": "Publish or draft a version before inspecting prompt and runtime configuration.",
-    "noVersions": "No versions",
-    "createVersionDefine": "Create a version to define role, goal, prompt, tools, and runtime config.",
     "noRuns": "No runs",
     "noRunRecords": "No run records match the current agent and status filter.",
     "created": "Created",
@@ -185,51 +302,115 @@
     "loaded": "Loaded",
     "runsCount": "Runs",
     "agentCreated": "Agent created",
-    "definitionDescription": "Create the reusable agent identity first. Versions, prompts, tools, and runtime config live under this definition.",
+    "definitionDescription": "Create the reusable agent identity first. configs, prompts, tools, and runtime config live under this definition.",
     "failedRuns": "Failed Runs",
     "simulationPlaceholder": "Summarize the last customer request and recommend the next action.",
     "namePlaceholder": "Support Agent",
-    "codePlaceholder": "support_agent",
+    "idPlaceholder": "support_agent",
     "descriptionPlaceholder": "What this agent is responsible for, when to use it, and what good output looks like.",
-    "typeAssistant": "Assistant",
-    "typePlanner": "Planner",
-    "typeExecutor": "Executor",
-    "typeResearch": "Research",
-    "typeToolUser": "Tool User",
     "searchPlaceholder": "Search agents...",
-    "codeCopied": "Agent code copied",
-    "versionCreated": "Agent version created",
-    "failedCreateVersion": "Failed to create agent version",
+    "idCopied": "Agent id copied",
     "basicInfo": "Basic Info",
     "basicInfoDesc": "Define the agent identity and purpose.",
-    "agentRole": "Role",
     "goal": "Goal",
     "goalPlaceholder": "What this agent should accomplish.",
-    "systemPrompt": "System Prompt",
     "systemPromptPlaceholder": "You are a helpful assistant that...",
     "modelSettings": "Model Settings",
     "modelSettingsDesc": "Choose the LLM model and parameters.",
     "provider": "Provider",
-    "model": "Model",
     "temperature": "Temperature",
     "maxTokens": "Max Tokens",
     "toolsSection": "Tools",
     "toolsSectionDesc": "Attach tools this agent can use.",
     "memorySection": "Memory",
     "memorySectionDesc": "Configure how the agent manages conversation memory.",
-    "memoryEnabled": "Enabled",
     "memoryScope": "Scope",
     "memoryScopeSession": "Session",
     "memoryScopePersistent": "Persistent",
-    "memoryScopeNone": "None"
+    "memoryScopeNone": "None",
+    "noModelSelected": "No model selected",
+    "providerNotSet": "Provider not set",
+    "health": "Health",
+    "selectedCount": "{{count}} selected",
+    "noSkillsSelected": "No skills selected",
+    "attachSkillsHint": "Attach reusable skills in Edit Agent so this agent can invoke business capabilities.",
+    "runtimeConfiguration": "Runtime configuration",
+    "context": "Context",
+    "auto": "Auto",
+    "execution": "Execution",
+    "retries": "Retries",
+    "backoff": "Backoff",
+    "toolLimit": "Tool limit",
+    "review": "Review",
+    "needsSetup": "Needs setup",
+    "setupHint": "Use Edit Agent to add model, prompt, memory, skills, and runtime policy.",
+    "notSet": "Not set",
+    "setupChecklist": "Setup checklist",
+    "chooseModel": "Choose model",
+    "chooseModelHint": "Select the model used by this agent.",
+    "attachSkills": "Attach skills",
+    "attachSkillsShortHint": "Pick reusable skills this agent can invoke.",
+    "tuneRuntime": "Tune runtime",
+    "tuneRuntimeHint": "Set memory, retries, limits, and review policy.",
+    "basicAgent": "Basic Agent",
+    "skills": "Skills",
+    "loadingSkills": "Loading skills...",
+    "failedToLoadSkills": "Failed to load skills",
+    "noSkillsYet": "No skills yet. Create reusable skills first, then attach them here.",
+    "modelAndMemory": "Model & Memory",
+    "modelAndMemoryHint": "Choose the model and memory scope for this agent.",
+    "timeoutSeconds": "Timeout Seconds",
+    "executionPolicy": "Execution Policy",
+    "retryAttempts": "Retry Attempts",
+    "retryBackoffMs": "Retry Backoff Ms",
+    "toolCallLimit": "Tool Call Limit",
+    "outputFormat": "Output Format",
+    "outputText": "Text",
+    "humanApproval": "Human Approval",
+    "approvalNever": "Never",
+    "approvalSensitiveActions": "Sensitive actions",
+    "approvalBeforeFinal": "Before final answer",
+    "approvalAlways": "Always",
+    "agentDeleted": "Agent deleted",
+    "failedToDeleteAgent": "Failed to delete agent",
+    "agentUpdated": "Agent updated",
+    "failedToUpdateAgent": "Failed to update agent",
+    "editAgent": "Edit Agent",
+    "contextWindow": "Context Window",
+    "runId": "Run ID",
+    "started": "Started",
+    "manageDescription": "Manage prompt, model, skills, memory, and run history from one place.",
+    "test": "Test",
+    "shown": "{{shown}} of {{total}} agents",
+    "runsBadge": "{{count}} runs",
+    "skillsBadge": "{{count}} skills",
+    "testAgent": "Test Agent",
+    "testAgentDescription": "Validate {{name}} and inspect recent execution history.",
+    "deleteAgent": "Delete Agent",
+    "deleteConfirm": "Are you sure you want to delete \"{{name}}\"? This will also delete its configuration and runs.",
+    "valueLabels": {
+      "session": "Session",
+      "persistent": "Persistent",
+      "none": "None",
+      "text": "Text",
+      "never": "Never",
+      "sensitive_actions": "Sensitive actions",
+      "before_final": "Before final answer",
+      "always": "Always",
+      "workflow": "Workflow",
+      "prompt": "Prompt",
+      "tool_orchestration": "Tool orchestration"
+    }
   },
   "sessions": {
     "title": "Sessions",
-    "description": "Inspect channel sessions and chat messages.",
+    "description": "Review conversations, continue debugging, and inspect lightweight context.",
     "create": "New Session",
+    "createDescription": "Start a clean conversation against an application.",
     "newSession": "New Session",
+    "sessionList": "Conversation List",
     "empty": "No sessions",
-    "emptyDescription": "Start a new session to begin.",
+    "emptyDescription": "No conversations match the current filter.",
     "typeMessage": "Type a message...",
     "searchSessions": "Search sessions",
     "noMessages": "No messages",
@@ -242,11 +423,31 @@
     "selectSession": "Select a session",
     "sessionCreated": "Session created",
     "messageSent": "Message sent",
-    "channelType": "Channel Type"
+    "channelType": "Channel",
+    "application": "Application",
+    "sessionName": "Session name",
+    "sessionNamePlaceholder": "For example: Customer support test",
+    "channelWeb": "Web chat",
+    "channelDebug": "Debug console",
+    "channelApi": "API channel",
+    "context": "Context",
+    "lastActive": "Last active",
+    "user": "User",
+    "messages": "Messages",
+    "runActivity": "Run activity",
+    "recentActivity": "Recent activity",
+    "noRunActivity": "No run activity yet",
+    "unknownApplication": "Unknown application",
+    "manualRun": "Manual run",
+    "roleUser": "You",
+    "roleAssistant": "Assistant",
+    "noTextContent": "No text content",
+    "structuredItems": "{{count}} structured items",
+    "structuredMessage": "Structured message content"
   },
   "tools": {
     "title": "Tools",
-    "description": "Manage tool definitions, versions, readiness, and quick payload tests.",
+    "description": "Description",
     "create": "Create Tool",
     "newTool": "New Tool",
     "empty": "No tools available",
@@ -256,61 +457,90 @@
     "searchTools": "Search tools",
     "allTypes": "All types",
     "allStatus": "All status",
-    "hasVersion": "Has version",
     "bound": "Bound",
-    "needsVersion": "Needs version",
     "noToolsFound": "No tools found",
     "adjustFiltersTool": "Adjust filters or create a new tool.",
     "toolDetails": "Tool Details",
-    "selectTool": "Select a tool to view versions and run a quick test.",
+    "selectTool": "Select a tool to view configs and run a quick test.",
     "definition": "Definition",
-    "version": "Version",
     "binding": "Binding",
+    "bindings": "Bindings",
     "credential": "Credential",
+    "credentials": "Credentials",
     "readiness": "Readiness",
     "basicInfo": "Basic Info",
+    "overview": "Overview",
+    "name": "Name",
     "plugin": "Plugin",
     "standalone": "Standalone",
     "timeout": "Timeout",
     "notSet": "Not set",
-    "latestVersion": "Latest Version",
     "inputs": "inputs",
     "outputs": "outputs",
     "retry": "retry",
     "payloadTest": "Payload Test",
-    "runMockRequest": "Run a mock request against the selected version.",
+    "runMockRequest": "Run a mock request against the selected config.",
     "run": "Run",
     "result": "Result",
     "noRunYet": "No run yet",
-    "selectVersionRun": "Select a version and run JSON.",
     "testPayloadSimulated": "Test payload simulated",
     "payloadMustBeJson": "Payload must be valid JSON",
     "inputSchema": "Input Schema",
     "invokeConfig": "Invoke Config",
     "retryPolicy": "Retry Policy",
-    "noVersionsYet": "No versions yet",
-    "createVersionBeforeTesting": "Create a version before testing this tool.",
-    "createToolVersion": "Create Tool Version",
     "endpoint": "Endpoint",
     "timeoutMs": "Timeout (ms)",
     "retryAttempts": "Retry attempts",
     "createTool": "Create Tool",
     "type": "Type",
     "ready": "Ready",
-    "bindings": "Bindings",
-    "versionsCount": "Versions",
     "none": "None",
-    "versionDescription": "Endpoint, timeout, retry, and schema snapshots.",
-    "toolVersionCreated": "Tool version created",
     "toolCreated": "Tool created",
-    "createVersion": "Create Version",
     "test": "Test",
     "filterByType": "Filter by tool type",
-    "filterByStatus": "Filter by tool status"
+    "filterByStatus": "Filter by tool status",
+    "mcpServers": "MCP Servers",
+    "httpTools": "HTTP Tools",
+    "retrievalTools": "Retrieval Tools",
+    "exposedTools": "Exposed Tools",
+    "connected": "Connected",
+    "needsConfig": "Needs Config",
+    "configured": "Configured",
+    "connection": "Connection",
+    "connections": "Connections",
+    "connectMcp": "Connect MCP",
+    "mcpServer": "MCP Server",
+    "mcpServerConnection": "MCP server connection",
+    "notConfigured": "Not configured",
+    "mcpToolsInside": "MCP tools inside this server",
+    "mcpToolsInsideDesc": "A single MCP connection can provide multiple callable tools for agents.",
+    "discovered": "{{count}} discovered",
+    "discoveryPending": "Discovery pending",
+    "notConnected": "Not connected",
+    "noMcpToolDescription": "No description provided by MCP discovery.",
+    "parametersSchemaDetected": "Parameters schema detected",
+    "searchToolPlaceholder": "Search by tool name, type, or endpoint",
+    "statusLabel": "Status",
+    "failedToLoadDetails": "Failed to load tool details",
+    "availableToolCount": "{{count}} tools available",
+    "noToolsDiscovered": "No tools discovered yet.",
+    "parameters": "Parameters:",
+    "mcpConnected": "MCP server connected",
+    "failedToConnectMcp": "Failed to connect MCP server",
+    "connectMcpServer": "Connect MCP Server",
+    "serverName": "Server Name",
+    "serverNamePlaceholder": "e.g. GitHub MCP Server",
+    "sseEndpointUrl": "SSE Endpoint URL",
+    "headersOptional": "Headers (Optional)",
+    "headerKey": "Key",
+    "headerValue": "Value",
+    "timeoutSeconds": "Timeout (seconds)",
+    "connecting": "Connecting...",
+    "connect": "Connect"
   },
   "knowledge": {
     "title": "Knowledge",
-    "description": "Manage retrieval bases, ingest documents, inspect indexing state, and test semantic search.",
+    "description": "Description",
     "refresh": "Refresh",
     "reindex": "Re-index",
     "reindexStarted": "Re-index job started",
@@ -335,7 +565,7 @@
     "selectKnowledgeBase": "Select a Knowledge Base",
     "chooseBaseManage": "Choose a base to manage documents, retrieval, ingestion, and settings.",
     "selectKnowledgeBaseFirst": "Select a Knowledge Base First",
-    "chooseBaseManageDocs": "Choose a knowledge base from the scope bar above to manage its documents.",
+    "chooseBaseManageDocs": "Choose a base to manage documents, retrieval, ingestion, and settings.",
     "noDescription": "No description",
     "archive": "Archive",
     "restoreCurrent": "Restore Current",
@@ -428,9 +658,8 @@
     "apiScopePreview": "API Scope Preview",
     "accessControl": "Access Control",
     "dataProtection": "Data Protection",
-    "exportEnabled": "Export Enabled",
     "allowDataExport": "Allow knowledge base data to be exported",
-    "auditLog": "Audit Log",
+    "auditLog": "Audit log",
     "enableAuditLogging": "Enable Audit Logging",
     "trackAccessModifications": "Track document access and modifications",
     "dataRetention": "Data Retention",
@@ -465,11 +694,11 @@
     "knowledgeBaseCreated": "Knowledge base created",
     "knowledgeBaseRestored": "Knowledge base restored",
     "knowledgeBaseArchived": "Knowledge base archived",
-    "searchResults": "search results",
+    "searchResults": "{{count}} search results",
     "noMatchingChunks": "No matching chunks found",
     "searchFailed": "Search failed",
     "failedToLoadDocuments": "Failed to load documents",
-    "lastIngestCreated": "Last ingest created {{count}} chunks",
+    "lastIngestCreated": "Last ingest created {{count}} chunk{{count === 1 ? '' : 's'}}.",
     "selectDocument": "Select a document",
     "documentDetailsAppear": "Document details, metadata, and matching chunks appear here.",
     "noDocumentSearchResults": "No document search results",
@@ -529,7 +758,6 @@
     "archiveRestoreBase": "Archive / restore base",
     "tenantIsolation": "Tenant isolation",
     "documentAclPii": "Document ACL / PII rules",
-    "auditLog": "Audit log",
     "agentBinding": "Agent binding",
     "workflowRetrievalNode": "Workflow retrieval node",
     "knowledgeSearchTool": "Knowledge search tool",
@@ -538,160 +766,144 @@
     "evaluationCaseFinished": "Evaluation case finished",
     "knowledgeBases_plural": "{{count}} Knowledge Bases",
     "selectAKnowledgeBase": "Select a Knowledge Base",
-    "chooseBaseManageDocs": "Choose a base to manage documents, retrieval, ingestion, and settings.",
-    "archive": "Archive",
-    "restoreCurrent": "Restore Current",
-    "reindexSet": "Re-index Set",
-    "selected": "Selected",
-    "documentsTab": "Documents",
-    "source": "Source",
-    "indexed_status": "Indexed",
-    "pending": "Pending",
-    "contentHash": "Content Hash",
-    "notAvailable": "Not available",
-    "notProvided": "Not provided",
-    "lastIngestCreated": "Last ingest created {{count}} chunk{{count === 1 ? '' : 's'}}.",
     "inspector": "Inspector",
-    "overview": "Overview",
     "search": "Search",
-    "metadata": "Metadata",
-    "chunks": "Chunks",
     "chunkTokenCount": "chunk #{{index}} / {{count}} tokens",
-    "selectDocument": "Select a document",
-    "documentDetailsAppear": "Document details, metadata, and matching chunks appear here.",
-    "noDocumentSearchResults": "No document search results",
-    "runQueryPlayground": "Run a query in Playground to inspect matching chunks for this document.",
-    "noChunksLoaded": "No chunks loaded",
-    "runSearchOrIndex": "Run search or index a new document to inspect chunks.",
-    "searching": "Searching...",
-    "searchResultCount": "Search {{count}}",
-    "searchResults": "{{count}} search results",
-    "noSearchResultsYet": "No search results yet",
-    "runRetrievalInspect": "Run a retrieval query to inspect chunks, citations, and score JSON.",
     "searchPlaceholder": "Ask a retrieval question",
-    "searchSourceFilter": "Search source filter",
-    "topK": "Top K",
     "add": "Add",
     "loadingDocuments": "Loading documents",
     "noDocumentsMatchFilters": "No documents match filters",
     "adjustSearchStatus": "Adjust the search text or status filter.",
-    "noDocuments": "No documents",
     "addTextMarkdownJsonHtml": "Add a text, markdown, JSON, HTML, or file-derived document to index it for retrieval.",
-    "goldenQuery": "Golden Query",
-    "buildEvaluationSet": "Build an evaluation set and run frontend benchmark simulations.",
-    "query": "Query",
-    "expectedSource": "Expected Source",
-    "addCase": "Add Case",
-    "avgRecall": "Avg Recall",
-    "avgPrecision": "Avg Precision",
-    "evaluationCases": "Evaluation Cases",
-    "expected": "Expected",
-    "recall": "Recall",
-    "precision": "Precision",
-    "indexJobs": "Index Jobs",
-    "manageFrontendQueues": "Manage frontend prototype queues for connector sync, parsing, chunking, embedding, and re-indexing.",
-    "reindexBase": "Re-index Base",
-    "start": "Start",
-    "complete": "Complete",
-    "retry": "Retry",
-    "progress": "Progress",
-    "indexedRatio": "Indexed Ratio",
-    "avgScore": "Avg Score",
-    "evalPass": "Eval Pass",
-    "failedJobs": "Failed Jobs",
-    "qualitySignals": "Quality Signals",
-    "indexCoverage": "Index Coverage",
-    "citationConfidence": "Citation Confidence",
-    "evaluationPassRate": "Evaluation Pass Rate",
-    "jobHealth": "Job Health",
-    "topQueries": "Top Queries",
-    "governanceSettings": "Governance Settings",
-    "prototypeAccessSafety": "Prototype access, safety, and answer policy controls.",
-    "aclMode": "ACL Mode",
-    "private": "Private",
-    "team": "Team",
-    "workspace": "Workspace",
-    "piiRedaction": "PII redaction",
-    "maskSensitiveValues": "Mask sensitive values before content enters retrieval context.",
-    "documentLevelAccess": "Document-level access checks",
-    "applyMetadataSecurity": "Apply metadata security filters before ranking.",
-    "apiScopePreview": "API Scope Preview",
-    "configureConnector": "Configure {{kind}}",
-    "syncMode": "Sync Mode",
-    "manual": "Manual",
-    "hourly": "Hourly",
-    "daily": "Daily",
-    "weekly": "Weekly",
-    "includeExcludeFilters": "Include / Exclude Filters",
-    "createsLocalPrototype": "This creates a local prototype job. The UI flow is complete even before persistence is connected.",
-    "createJob": "Create Job",
     "cancel": "Cancel",
-    "knowledgeCapabilityMap": "Knowledge Capability Map",
     "moduleStatusAtGlance": "Module status at a glance. Open the dedicated section for each workflow.",
     "productSurfaceRag": "Product surface for a full RAG knowledge platform. Prototype items are fully represented in the frontend flow and ready for later persistence.",
-    "retrievalSettings": "Retrieval Settings",
-    "tuneRetrievalBehavior": "Tune the retrieval behavior directly in the frontend prototype.",
-    "retrievalDefaults": "Retrieval Defaults",
-    "chunkSize": "Chunk Size",
-    "overlap": "Overlap",
-    "rerankResults": "Rerank results",
-    "reorderCandidates": "Reorder candidates after hybrid retrieval.",
-    "queryRewrite": "Query rewrite",
-    "expandShortQueries": "Expand short queries before search.",
-    "requireCitations": "Require citations",
-    "treatUncitedAnswers": "Treat uncited answers as low confidence.",
-    "keywordWeight": "Keyword Weight",
-    "vectorWeight": "Vector Weight",
-    "rerankWeight": "Rerank Weight",
-    "createKnowledgeBase": "Create Knowledge Base",
-    "knowledgeBaseCreated": "Knowledge base created",
     "name": "Name",
-    "description": "Description",
-    "metadataJson": "Metadata JSON",
     "create": "Create",
     "creating": "Creating...",
-    "addDocumentTitle": "Add Document",
-    "parsePreview": "Parse Preview",
-    "parsing": "Parsing...",
-    "previewParse": "Preview Parse",
     "parsePreviewReady": "Parse preview ready",
     "parseFailed": "Parse failed",
-    "indexDocument": "Index Document",
-    "indexing": "Indexing...",
     "documentIndexedWith": "Document indexed with {{count}} chunk{{count === 1 ? '' : 's'}}",
-    "documentIngestFailed": "Document ingest failed",
-    "sourceType": "Source Type",
-    "sourceUri": "Source URI",
-    "content": "Content",
-    "chunkOverlap": "Chunk Overlap"
+    "manageInsideBase": "Manage documents, retrieval testing, indexing jobs, and settings inside the selected knowledge base.",
+    "loading": "Loading knowledge",
+    "manageBases": "Manage Bases",
+    "currentBase": "Current Base",
+    "runSearch": "Run Search",
+    "addDocumentBeforeTest": "Add at least one document before testing retrieval quality.",
+    "testSearch": "Test Search",
+    "openSettings": "Open knowledge settings",
+    "documentsHint": "Add content, filter indexed documents, and select one item to inspect chunks or metadata.",
+    "searchDocuments": "Search documents",
+    "openDetails": "Open details",
+    "expectedCitation": "Expected citation",
+    "askAgainstBase": "Ask a question against {{name}}, then inspect citations, chunks, and score breakdowns.",
+    "sections": {
+      "documents": "Documents",
+      "playground": "Search Test",
+      "evaluation": "Evaluation",
+      "jobs": "Jobs",
+      "analytics": "Analytics",
+      "settings": "Settings",
+      "ingest": "Ingest"
+    },
+    "sourceLabels": {
+      "all": "All sources",
+      "text": "Text",
+      "markdown": "Markdown",
+      "json": "Structured",
+      "html": "HTML",
+      "pdf": "PDF"
+    },
+    "statusLabels": {
+      "all": "All statuses",
+      "indexed": "Indexed",
+      "draft": "Draft",
+      "failed": "Failed",
+      "archived": "Archived"
+    },
+    "fullRagSurface": "Product surface for a full RAG knowledge platform. Prototype items are fully represented in the frontend flow and ready for later persistence.",
+    "modelSelection": "Model Selection",
+    "retrievalMode": "Retrieval Mode",
+    "hybridRetrieval": "Hybrid keyword + vector",
+    "vectorOnly": "Vector only",
+    "keywordOnly": "Keyword only",
+    "embeddingModel": "Embedding Model",
+    "rerankModel": "Rerank Model",
+    "noConfiguredModels": "No configured models found. Add models in the Models page, then select them here.",
+    "candidatePool": "Candidate Pool",
+    "minimumScore": "Minimum Score",
+    "autoSelect": "Auto select",
+    "scoreDetails": "Score details",
+    "noExtraScoringSignals": "No extra scoring signals available.",
+    "documentIndexedWithChunks": "Document indexed with {{count}} chunks",
+    "targetBase": "Target base",
+    "noBaseSelected": "No base selected",
+    "documentTargetHint": "This document will be parsed, chunked, and indexed into the active base. Change the scope bar before opening this dialog if the target is wrong.",
+    "failedToLoadBases": "Failed to load knowledge bases",
+    "openBase": "Open Base",
+    "notSelected": "Not selected",
+    "propertiesTab": "Properties",
+    "noProperties": "No properties",
+    "noPropertiesDescription": "This item does not have user-facing properties yet.",
+    "queryStats": "{{count}} searches / {{rate}}% success",
+    "jobTypes": {
+      "sitemapSync": "Sitemap Sync",
+      "reindex": "Re-index",
+      "pdfBatch": "PDF Batch"
+    },
+    "jobTargets": {
+      "productDocs": "Product Docs",
+      "policyPack": "Policy Pack"
+    },
+    "evalSamples": {
+      "downloadInvoices": "Where can customers download invoices?",
+      "billingInvoiceFaq": "Billing and Invoice FAQ",
+      "annualPlanRefunds": "How do refunds affect annual plans?",
+      "refundPolicy": "Refund policy"
+    },
+    "topQuery": {
+      "downloadInvoice": "download invoice",
+      "refundPolicy": "refund policy",
+      "billingContact": "billing contact",
+      "planUpgrade": "plan upgrade"
+    },
+    "propertyLabels": {
+      "category": "Category",
+      "audience": "Audience",
+      "locale": "Locale",
+      "revision": "Revision",
+      "tags": "Tags",
+      "source": "Source",
+      "owner": "Owner",
+      "policy": "Policy",
+      "domain": "Domain",
+      "confidence": "Confidence"
+    }
   },
   "teams": {
     "title": "Teams",
-    "description": "Manage multi-agent teams — create, configure members and policies, and run collaborative tasks.",
+    "description": "Manage multi-agent teams: configure members, policies, and collaborative runs.",
     "newTeam": "New Team",
     "searchPlaceholder": "Search teams...",
-    "searchByNameCodeType": "Search by name, code, type, or description",
+    "searchByNameType": "Search by name, id, type, or description",
     "teamsShown": "of {{count}} teams shown",
     "allStatuses": "All statuses",
     "filterByStatus": "Filter by status",
     "sortTeams": "Sort teams",
     "newestFirst": "Newest first",
     "noMatchingTeams": "No matching teams",
-    "adjustFiltersTeam": "Adjust search or filters to find a matching team.",
+    "adjustFiltersTeam": "Adjust search or filters to find a matching team definition.",
     "noTeams": "No teams",
     "createTeamStart": "Create a team to start coordinating multiple specialized agents.",
     "selectTeam": "Select a team to view details.",
-    "selectTeamInspect": "Select a team to inspect collaboration, versions, and runs.",
+    "selectTeamInspect": "Select a team to inspect collaboration, configs, and runs.",
     "teamDetails": "Team Details",
     "teamDirectory": "Team Directory",
     "teamCockpit": "Team Cockpit",
-    "copyCode": "Copy Code",
-    "teamCodeCopied": "Team code copied",
-    "newVersion": "New Version",
-    "newVersionBtn": "New Version",
+    "copyId": "Copy ID",
+    "teamIdCopied": "Team id copied",
     "run": "Run",
     "runs": "Runs",
-    "versions": "Versions",
     "members": "Members",
     "failedRuns": "Failed Runs",
     "member": "Member",
@@ -705,8 +917,8 @@
     "coordinationTab": "Coordination",
     "mode": "Mode",
     "objective": "Objective",
-    "describeVersion": "Describe what this team should coordinate and optimize for.",
     "policy": "Policy",
+    "agent": "Agent",
     "maxRounds": "Max Rounds",
     "maxRoundsField": "Max rounds",
     "handoff": "Handoff",
@@ -736,11 +948,6 @@
     "teamCreated": "Team created",
     "failedCreateTeam": "Failed to create team",
     "createTeam": "Create Team",
-    "createTeamVersion": "Create Team Version",
-    "createTeamVersionBefore": "Create a team version before starting a collaborative run.",
-    "createTeamVersionInspect": "Create a team version to inspect member policy and coordination settings.",
-    "teamVersionCreated": "Team version created",
-    "failedCreateTeamVersion": "Failed to create team version",
     "startRun": "Start Run",
     "starting": "Starting...",
     "startRunPreview": "Start a run to preview the newest result for this team.",
@@ -750,48 +957,35 @@
     "runInput": "Run input",
     "runConsole": "Run Console",
     "input": "Input",
-    "allStatuses": "All statuses",
     "allTypes": "All types",
     "allRunStatuses": "All run statuses",
-    "filterByStatus": "Filter by status",
     "filterByType": "Filter by type",
     "filterRunsByStatus": "Filter runs by status",
-    "sortTeams": "Sort teams",
-    "newestFirst": "Newest first",
     "listView": "List view",
     "gridView": "Grid view",
     "structuredInput": "Structured input payload",
     "noOutputYet": "No output yet",
     "noRunRecords": "No run records found.",
     "noRuns": "No runs",
-    "noVersion": "No version",
-    "noVersionSelected": "No version selected",
-    "noRunnableVersion": "No runnable version",
     "noObjectiveProvided": "No objective provided.",
     "noPolicy": "No policy",
     "notPublished": "Not published",
     "notSet": "Not set",
-    "versionId": "Version ID",
     "latest": "Latest",
     "latestResult": "Latest Result",
     "none": "None",
     "lastRun": "Last Run",
     "currentObjective": "Current Objective",
-    "createVersionDefine": "Create a version to define how this team collaborates.",
-    "createVersionDefineObjective": "Create a version to define objective, coordination mode, members, and execution policy.",
-    "createVersionBeforeDefining": "Create a team version before defining member roles and collaboration shape.",
-    "createVersionBeforeRun": "Create a team version before starting a run",
-    "createVersionBeforeTeamRun": "Create a version before this team can run work.",
-    "createNewVersionMember": "Create a new version with member rows to define who participates and what each role is responsible for.",
-    "adjustFiltersTeam": "Adjust search or filters to find a matching team definition.",
     "noMembersConfigured": "No members configured",
+    "noTeamConfiguration": "No team configuration yet.",
     "noDescription": "No description",
     "owner": "Owner",
     "unassigned": "Unassigned",
     "preparingConsole": "Preparing console",
     "taskTemplates": "Task templates",
     "template": "Template {{index}}",
-    "searchByNameCodeType": "Search by name, code, type, or description"
+    "createTeamDefineObjective": "Create a team, choose its members, and define how they collaborate.",
+    "describeObjective": "Describe the task objective this team should work toward."
   },
   "settings": {
     "title": "Settings",
@@ -831,8 +1025,6 @@
     "apiKey": "API Key",
     "models": "Models",
     "defaultModel": "Default Model",
-    "enabled": "Enabled",
-    "disabled": "Disabled",
     "noProviders": "No model providers configured",
     "noProvidersDescription": "Add a model provider to enable LLM connections for your agents.",
     "testConnection": "Test Connection",
@@ -878,6 +1070,7 @@
     "totalProviders": "Total Providers",
     "activeProviders": "Active Providers",
     "totalModels": "Total Models",
+    "defaultModels": "Default Models",
     "providers": "Providers",
     "searchProviders": "Search providers...",
     "discoveredModels": "Discovered Models",
@@ -928,6 +1121,119 @@
     "failedToCreate": "Failed to create",
     "failedToUpdate": "Failed to update",
     "failedToDelete": "Failed to delete",
-    "failedToSave": "Failed to save"
+    "failedToSave": "Failed to save",
+    "unableToLoadData": "Unable to load data",
+    "checkGatewayConnection": "Check the gateway connection and credentials.",
+    "somethingBroke": "Something broke"
+  },
+  "models": {
+    "title": "Models",
+    "description": "Manage configured model endpoints.",
+    "defaultTestPrompt": "Say OK in one short sentence.",
+    "failedToLoad": "Failed to load models",
+    "modelCreated": "Model created",
+    "modelSaved": "Model saved",
+    "failedToSave": "Failed to save model",
+    "modelDeleted": "Model deleted",
+    "failedToDelete": "Failed to delete model",
+    "testFailed": "Model test failed",
+    "loading": "Loading models",
+    "newModel": "New Model",
+    "configured": "Configured",
+    "withApiKey": "With API Key",
+    "configuredModels": "Configured Models",
+    "shown": "{{shown}} of {{total}} shown",
+    "listManagement": "List management",
+    "searchPlaceholder": "Search name, model, provider...",
+    "allProviders": "All providers",
+    "model": "Model",
+    "provider": "Provider",
+    "capabilities": "Capabilities",
+    "noMatching": "No matching models",
+    "noMatchingDesc": "Adjust filters or create a new configured model.",
+    "createModel": "Create Model",
+    "editModel": "Edit Model",
+    "testModel": "Test Model",
+    "prompt": "Prompt",
+    "testing": "Testing",
+    "runTest": "Run Test",
+    "testNamed": "Test {{name}}",
+    "editNamed": "Edit {{name}}",
+    "deleteNamed": "Delete {{name}}",
+    "modelName": "Model Name",
+    "baseUrl": "Base URL",
+    "apiKey": "API Key",
+    "keepSecretPlaceholder": "Leave blank to keep stored secret",
+    "contextWindow": "Context Window",
+    "maxOutputTokens": "Max Output Tokens",
+    "temperature": "Temperature",
+    "timeoutSeconds": "Timeout Seconds",
+    "failedToCreate": "Failed to create model",
+    "newModelDesc": "Add a configured model endpoint.",
+    "quickCreateHint": "Add the model ID first. You can tune limits, temperature, and description later.",
+    "optionalApiKey": "Optional for local providers",
+    "modelNamePreview": "Model ID",
+    "baseUrlNeeded": "Base URL is required for custom providers.",
+    "showAdvanced": "Show advanced options",
+    "hideAdvanced": "Hide advanced options",
+    "localChatPlaceholder": "Local Chat",
+    "modelTestCompleted": "Model test completed",
+    "providerOpenaiCompatible": "OpenAI compatible",
+    "providerOpenai": "OpenAI",
+    "providerOllama": "Ollama",
+    "providerAnthropic": "Anthropic",
+    "providerDeepSeek": "DeepSeek",
+    "providerCustom": "Custom",
+    "capability": {
+      "chat": "Chat",
+      "tools": "Tools",
+      "reasoning": "Reasoning",
+      "embedding": "Embedding",
+      "image": "Image",
+      "audio": "Audio",
+      "video": "Video",
+      "rerank": "Rerank",
+      "moderation": "Moderation"
+    }
+  },
+  "memories": {
+    "title": "Memory",
+    "description": "Manage agent, user, session, and team memories. Inspect recall behavior without reading raw JSON.",
+    "failedToLoad": "Failed to load memories",
+    "loading": "Loading memories",
+    "store": "Memory Store",
+    "shown": "{{shown}} shown / {{total}} total",
+    "noMemoriesFound": "No memories found",
+    "noMemoriesFoundDesc": "Adjust filters or wait for agents and sessions to accumulate memory.",
+    "detail": "Memory Detail",
+    "searchPlaceholder": "Search content, scope, source, or owner",
+    "scope": "Scope",
+    "allScopes": "All scopes",
+    "allTypes": "All types",
+    "scopeGlobal": "Global",
+    "scopeUser": "User",
+    "scopeSession": "Session",
+    "scopeAgent": "Agent",
+    "scopeTeam": "Team",
+    "expired": "Expired",
+    "scopeId": "Scope ID",
+    "importance": "Importance",
+    "selectMemory": "Select a memory",
+    "selectMemoryDesc": "Details, scope, access history, and retrieval metadata appear here.",
+    "content": "Content",
+    "ownerAgent": "Owner Agent",
+    "user": "User",
+    "session": "Session",
+    "source": "Source",
+    "embedding": "Embedding",
+    "notGenerated": "Not generated",
+    "vectorSize": "Vector Size",
+    "lastAccessed": "Last Accessed",
+    "expires": "Expires",
+    "never": "Never",
+    "metadata": "Metadata",
+    "noMetadata": "No metadata provided.",
+    "structuredContent": "Structured Content",
+    "noStructuredContent": "No structured content."
   }
 }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 550 - 338
web/src/locales/zh.json


+ 523 - 188
web/src/pages/agents/AgentListPage.tsx

@@ -1,111 +1,114 @@
 import * as React from "react";
 import { useTranslation } from "react-i18next";
 import {
-  Activity,
-  Archive,
   Bot,
-  CheckCircle2,
-  Copy,
-  FileCode2,
   Play,
   RefreshCw,
   Search,
-  SlidersHorizontal,
+  Sparkles,
+  Trash2,
 } from "lucide-react";
-import { listAgentRuns, listAgentVersions } from "@/api";
+import { createAgentConfig, deleteAgent, listAgentConfigs, listAgentRuns, listModelProviders, listSkills, updateAgent } from "@/api";
 import { ApiErrorState } from "@/components/shared/ApiErrorState";
+import { ConfirmDialog } from "@/components/shared/ConfirmDialog";
 import { EmptyState } from "@/components/shared/EmptyState";
-import { EntityListItem } from "@/components/shared/EntityListItem";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
-import { MetricCard } from "@/components/shared/MetricCard";
 import { PageHeader } from "@/components/shared/PageHeader";
 import { SearchInput } from "@/components/shared/SearchInput";
-import { StatusBadge } from "@/components/shared/StatusBadge";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Dialog } from "@/components/ui/dialog";
+import { Input, Textarea } from "@/components/ui/input";
 import { Select } from "@/components/ui/select";
-import { Tabs } from "@/components/ui/tabs";
 import { toast } from "@/components/ui/toaster";
+import { demoText } from "@/lib/demo-text";
 import { useAgentList } from "@/hooks";
-import { copyToClipboard } from "@/lib/utils";
-import type { AgentRun, AgentRunStatus, AgentStatus, AgentVersion } from "@/types";
+import type { AgentConfig, AgentDefinition, AgentRun, JSONObject, ModelProvider, SkillDefinition } from "@/types";
 import { AgentOverview } from "./components/AgentOverview";
 import { AgentRuns } from "./components/AgentRuns";
-import { AgentVersions } from "./components/AgentVersions";
 import { CreateAgentDialog } from "./components/CreateAgentDialog";
 
-type StatusFilter = "all" | AgentStatus;
-type RunStatusFilter = "all" | AgentRunStatus;
-type SortMode = "recent" | "name" | "status";
-
 export function AgentListPage() {
   const { t } = useTranslation();
   const [search, setSearch] = React.useState("");
-  const [statusFilter, setStatusFilter] = React.useState<StatusFilter>("all");
-  const [typeFilter, setTypeFilter] = React.useState("all");
-  const [sortMode, setSortMode] = React.useState<SortMode>("recent");
-  const [runStatusFilter, setRunStatusFilter] = React.useState<RunStatusFilter>("all");
   const [selectedAgentId, setSelectedAgentId] = React.useState<string>();
-  const [activeTab, setActiveTab] = React.useState("overview");
-  const [versions, setVersions] = React.useState<AgentVersion[]>([]);
+  const [configs, setConfigs] = React.useState<AgentConfig[]>([]);
   const [runs, setRuns] = React.useState<AgentRun[]>([]);
+  const [skills, setSkills] = React.useState<SkillDefinition[]>([]);
   const [relatedLoading, setRelatedLoading] = React.useState(true);
+  const [editOpen, setEditOpen] = React.useState(false);
+  const [deleteOpen, setDeleteOpen] = React.useState(false);
+  const [runsOpen, setRunsOpen] = React.useState(false);
   const agents = useAgentList();
 
   const agentList = agents.data ?? [];
   const selectedAgent = agentList.find((agent) => agent.id === selectedAgentId) ?? agentList[0];
-  const agentTypes = React.useMemo(() => Array.from(new Set(agentList.map((a) => a.agent_type))).sort(), [agentList]);
-  const versionCounts = React.useMemo(() => countBy(versions, (v) => v.agent_id), [versions]);
-  const selectedVersions = versions.filter((v) => v.agent_id === selectedAgent?.id);
-  const selectedRuns = runs.filter((r) => r.agent_id === selectedAgent?.id);
-  const latestVersion = [...selectedVersions].sort((a, b) => b.version_no - a.version_no)[0];
-  const failedRuns = runs.filter((r) => r.status === "failed").length;
-  const activeAgents = agentList.filter((a) => a.status === "active").length;
-  const draftAgents = agentList.filter((a) => a.status === "draft").length;
-  const archivedAgents = agentList.filter((a) => a.status === "archived").length;
-
-  const filtered = agentList
-    .filter((agent) => {
-      const text = `${agent.name} ${agent.code} ${agent.agent_type} ${agent.description ?? ""}`.toLowerCase();
-      const matchesSearch = text.includes(search.toLowerCase());
-      const matchesStatus = statusFilter === "all" || agent.status === statusFilter;
-      const matchesType = typeFilter === "all" || agent.agent_type === typeFilter;
-      return matchesSearch && matchesStatus && matchesType;
-    })
-    .sort((first, second) => {
-      if (sortMode === "name") return first.name.localeCompare(second.name);
-      if (sortMode === "status") return first.status.localeCompare(second.status) || first.name.localeCompare(second.name);
-      return new Date(second.created_time).getTime() - new Date(first.created_time).getTime();
+  const selectedConfigs = configs.filter((config) => config.agent_id === selectedAgent?.id);
+  const selectedRuns = runs
+    .filter((r) => r.agent_id === selectedAgent?.id)
+    .sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime());
+  const activeConfig = [...selectedConfigs].sort((a, b) => b.version_no - a.version_no)[0];
+  const configByAgent = React.useMemo(() => {
+    const grouped = new Map<string, AgentConfig[]>();
+    configs.forEach((config) => {
+      grouped.set(config.agent_id, [...(grouped.get(config.agent_id) ?? []), config]);
+    });
+    const result = new Map<string, AgentConfig>();
+    grouped.forEach((items, agentId) => {
+      const config = [...items].sort((a, b) => b.version_no - a.version_no)[0];
+      if (config) result.set(agentId, config);
     });
+    return result;
+  }, [configs]);
 
-  const hasFilters = search.length > 0 || statusFilter !== "all" || typeFilter !== "all" || sortMode !== "recent";
+  const filtered = agentList.filter((agent) => {
+    const text = `${agent.name} ${agent.description ?? ""}`.toLowerCase();
+    return text.includes(search.toLowerCase());
+  });
 
-  const loadRelated = React.useCallback(async () => {
+  const loadSkills = React.useCallback(async () => {
+    try {
+      setSkills(await listSkills());
+    } catch {
+      setSkills([]);
+    }
+  }, []);
+
+  const loadRelated = React.useCallback(async (agentId?: string) => {
+    if (!agentId) {
+      setConfigs([]);
+      setRuns([]);
+      setRelatedLoading(false);
+      return;
+    }
     setRelatedLoading(true);
     try {
-      const [versionData, runData] = await Promise.all([listAgentVersions(), listAgentRuns()]);
-      setVersions(versionData);
-      setRuns(runData);
+      const [configData, runData] = await Promise.all([listAgentConfigs(agentId), listAgentRuns(agentId)]);
+      setConfigs((current) => [...current.filter((config) => config.agent_id !== agentId), ...configData]);
+      setRuns((current) => [...current.filter((run) => run.agent_id !== agentId), ...runData]);
+    } catch {
+      toast.error(t("errors.failedToLoad"));
     } finally {
       setRelatedLoading(false);
     }
-  }, []);
+  }, [t]);
 
-  React.useEffect(() => { void loadRelated(); }, [loadRelated]);
+  React.useEffect(() => { void loadSkills(); }, [loadSkills]);
+  React.useEffect(() => { void loadRelated(selectedAgent?.id); }, [loadRelated, selectedAgent?.id]);
   React.useEffect(() => { if (!selectedAgentId && agentList[0]) setSelectedAgentId(agentList[0].id); }, [agentList, selectedAgentId]);
 
-  function clearFilters() {
-    setSearch("");
-    setStatusFilter("all");
-    setTypeFilter("all");
-    setSortMode("recent");
-  }
-
-  async function copyAgentCode() {
+  async function handleDelete() {
     if (!selectedAgent) return;
-    await copyToClipboard(selectedAgent.code);
-    toast.success(t("agents.codeCopied"));
+    try {
+      await deleteAgent(selectedAgent.id);
+      toast.success(t("agents.agentDeleted"));
+      setSelectedAgentId(undefined);
+      setDeleteOpen(false);
+      void agents.refetch();
+    } catch {
+      toast.error(t("agents.failedToDeleteAgent"));
+    }
   }
 
   if (agents.loading) return <LoadingSpinner label={t("common.loading")} />;
@@ -118,7 +121,7 @@ export function AgentListPage() {
         description={t("agents.description")}
         actions={
           <>
-            <Button variant="outline" onClick={() => { void agents.refetch(); void loadRelated(); }}>
+            <Button variant="outline" onClick={() => { void agents.refetch(); void loadSkills(); void loadRelated(selectedAgent?.id); }}>
               <RefreshCw className="h-4 w-4" /> {t("common.refresh")}
             </Button>
             <CreateAgentDialog onCreated={() => void agents.refetch()} />
@@ -126,82 +129,29 @@ export function AgentListPage() {
         }
       />
 
-      {/* Metric cards */}
-      <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
-        <MetricCard label={t("agents.title")} value={agentList.length} icon={Bot} />
-        <MetricCard label={t("common.active")} value={activeAgents} icon={CheckCircle2} />
-        <MetricCard label={t("common.draft")} value={draftAgents} icon={FileCode2} />
-        <MetricCard label={t("common.archived")} value={archivedAgents} icon={Archive} />
-        <MetricCard label={t("common.versions")} value={versions.length} icon={Copy} />
-        <MetricCard label={t("agents.failedRuns")} value={failedRuns} icon={Activity} />
-      </div>
-
-      <div className="grid gap-6 xl:grid-cols-[440px_1fr]">
-        {/* Left panel: agent directory */}
-        <Card>
-          <CardHeader>
+      <div className="grid gap-6 xl:grid-cols-[380px_minmax(0,1fr)]">
+        <Card className="overflow-hidden">
+          <CardHeader className="border-b border-border">
             <div className="flex items-start justify-between gap-3">
               <div>
                 <CardTitle>{t("agents.agentDirectory")}</CardTitle>
-                <CardDescription>
-                  {t("agents.agentsShown", { count: agentList.length })} {filtered.length}
-                </CardDescription>
+                <CardDescription>{t("agents.shown", { shown: filtered.length, total: agentList.length })}</CardDescription>
               </div>
-              <SlidersHorizontal className="mt-1 h-4 w-4 text-muted-foreground" />
+              <Badge className="border-primary/20 bg-primary/10 text-primary">{agentList.length}</Badge>
             </div>
           </CardHeader>
-          <CardContent className="space-y-4">
-            <SearchInput value={search} onChange={setSearch} placeholder={t("agents.searchByNameCodeType")} />
-            <div className="grid gap-3 sm:grid-cols-2">
-              <Select
-                aria-label={t("common.filterByStatus")}
-                value={statusFilter}
-                onChange={(event) => setStatusFilter(event.target.value as StatusFilter)}
-                options={[
-                  { value: "all", label: t("agents.allStatuses") },
-                  { value: "active", label: t("common.active") },
-                  { value: "draft", label: t("common.draft") },
-                  { value: "archived", label: t("common.archived") },
-                ]}
-              />
-              <Select
-                aria-label={t("common.filterByAgentType")}
-                value={typeFilter}
-                onChange={(event) => setTypeFilter(event.target.value)}
-                options={[{ value: "all", label: t("agents.allTypes") }, ...agentTypes.map((type) => ({ value: type, label: type }))]}
-              />
-              <Select
-                aria-label={t("common.sortAgents")}
-                value={sortMode}
-                onChange={(event) => setSortMode(event.target.value as SortMode)}
-                options={[
-                  { value: "recent", label: t("agents.newestFirst") },
-                  { value: "name", label: t("common.name") },
-                  { value: "status", label: t("common.status") },
-                ]}
-              />
-            </div>
-            {hasFilters ? (
-              <Button type="button" variant="ghost" size="sm" onClick={clearFilters}>
-                {t("common.clearFilters")}
-              </Button>
-            ) : null}
-
+          <CardContent className="space-y-3 pt-4">
+            <SearchInput className="sm:w-full" value={search} onChange={setSearch} placeholder={t("agents.searchPlaceholder")} />
             {filtered.length ? (
               <div className="space-y-2">
                 {filtered.map((agent) => (
-                  <EntityListItem
+                  <AgentDirectoryRow
                     key={agent.id}
                     active={agent.id === selectedAgent?.id}
-                    title={agent.name}
-                    subtitle={`${agent.code} · ${agent.agent_type}`}
-                    meta={
-                      <div className="flex items-center gap-2">
-                        <Badge className="border-border bg-muted/60 text-muted-foreground">{versionCounts.get(agent.id) ?? 0}v</Badge>
-                        <StatusBadge status={agent.status} />
-                      </div>
-                    }
-                    onClick={() => { setSelectedAgentId(agent.id); setActiveTab("overview"); }}
+                    agent={agent}
+                    config={configByAgent.get(agent.id)}
+                    runCount={runs.filter((item) => item.agent_id === agent.id).length}
+                    onClick={() => setSelectedAgentId(agent.id)}
                   />
                 ))}
               </div>
@@ -211,69 +161,43 @@ export function AgentListPage() {
           </CardContent>
         </Card>
 
-        {/* Right panel: agent details */}
-        <Card>
-          <CardHeader>
+        <Card className="overflow-hidden">
+          <CardHeader className="border-b border-border">
             <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
               <div className="min-w-0">
                 <div className="flex flex-wrap items-center gap-2">
-                  <CardTitle className="truncate text-lg">{selectedAgent?.name ?? t("agents.agentDetails")}</CardTitle>
-                  {selectedAgent ? <StatusBadge status={selectedAgent.status} /> : null}
+                  <Bot className="h-5 w-5 text-primary" />
+                  <CardTitle className="truncate">{selectedAgent ? demoText(selectedAgent.name, t) : t("agents.agentDetails")}</CardTitle>
                 </div>
-                <CardDescription className="mt-1">
-                  {selectedAgent ? `${selectedAgent.agent_type} · ${selectedAgent.code}` : t("agents.selectAgent")}
+                <CardDescription className="mt-2">
+                  {selectedAgent ? t("agents.manageDescription") : t("agents.selectAgent")}
                 </CardDescription>
               </div>
-              <div className="flex flex-wrap items-center gap-2">
-                <Button variant="outline" size="sm" disabled={!selectedAgent} onClick={() => void copyAgentCode()}>
-                  <Copy className="h-4 w-4" /> {t("agents.copyCode")}
-                </Button>
-                {selectedAgent && latestVersion ? (
-                  <Button size="sm" onClick={() => setActiveTab("runs")}>
-                    <Play className="h-4 w-4" /> {t("agents.testConsole")}
+              {selectedAgent && (
+                <div className="flex shrink-0 items-center gap-2">
+                  {activeConfig && (
+                    <Button size="sm" variant="outline" onClick={() => setRunsOpen(true)}>
+                      <Play className="h-3 w-3 mr-1" /> {t("agents.test")}
+                    </Button>
+                  )}
+                  <Button variant="outline" size="sm" onClick={() => setEditOpen(true)}>
+                    {t("common.edit")}
                   </Button>
-                ) : null}
-              </div>
+                  <Button variant="ghost" size="sm" onClick={() => setDeleteOpen(true)}>
+                    <Trash2 className="h-3 w-3 text-destructive" />
+                  </Button>
+                </div>
+              )}
             </div>
           </CardHeader>
-          <CardContent>
+          <CardContent className="pt-4">
             {selectedAgent ? (
-              <Tabs
-                value={activeTab}
-                onChange={setActiveTab}
-                tabs={[
-                  {
-                    value: "overview",
-                    label: t("common.overview"),
-                    content: (
-                      <AgentOverview
-                        agent={selectedAgent}
-                        latestVersion={latestVersion}
-                        versionCount={selectedVersions.length}
-                        runCount={selectedRuns.length}
-                        failedRunCount={selectedRuns.filter((r) => r.status === "failed").length}
-                      />
-                    ),
-                  },
-                  {
-                    value: "runs",
-                    label: t("common.runs"),
-                    content: (
-                      <AgentRuns
-                        agentId={selectedAgent.id}
-                        runs={selectedRuns}
-                        loading={relatedLoading}
-                        statusFilter={runStatusFilter}
-                        onStatusFilterChange={setRunStatusFilter}
-                      />
-                    ),
-                  },
-                  {
-                    value: "versions",
-                    label: t("common.versions"),
-                    content: <AgentVersions versions={selectedVersions} loading={relatedLoading} />,
-                  },
-                ]}
+              <AgentOverview
+                agent={selectedAgent}
+                activeConfig={activeConfig}
+                skills={skills}
+                runCount={selectedRuns.length}
+                failedRunCount={selectedRuns.filter((r) => r.status === "failed").length}
               />
             ) : (
               <EmptyState icon={Bot} title={t("agents.noAgents")} description={t("agents.createAgentStart")} />
@@ -281,15 +205,426 @@ export function AgentListPage() {
           </CardContent>
         </Card>
       </div>
+
+      {selectedAgent && (
+        <EditAgentDialog
+          agent={selectedAgent}
+          activeConfig={activeConfig}
+          open={editOpen}
+          onOpenChange={setEditOpen}
+          onSaved={() => { void agents.refetch(); void loadRelated(selectedAgent.id); }}
+        />
+      )}
+      {selectedAgent && (
+        <Dialog open={runsOpen} onOpenChange={setRunsOpen} title={t("agents.testAgent")} className="max-w-5xl">
+          <div className="space-y-4">
+            <p className="text-sm text-muted-foreground">
+              {t("agents.testAgentDescription", { name: demoText(selectedAgent.name, t) })}
+            </p>
+            <AgentRuns agentId={selectedAgent.id} runs={selectedRuns} loading={relatedLoading} />
+          </div>
+        </Dialog>
+      )}
+      <ConfirmDialog
+        open={deleteOpen}
+        onOpenChange={setDeleteOpen}
+        title={t("agents.deleteAgent")}
+        description={t("agents.deleteConfirm", { name: demoText(selectedAgent?.name, t) })}
+        onConfirm={handleDelete}
+      />
     </div>
   );
 }
 
-function countBy<T>(items: T[], getKey: (item: T) => string) {
-  const counts = new Map<string, number>();
-  for (const item of items) {
-    const key = getKey(item);
-    counts.set(key, (counts.get(key) ?? 0) + 1);
+function AgentDirectoryRow({
+  agent,
+  config,
+  active,
+  runCount,
+  onClick,
+}: {
+  agent: AgentDefinition;
+  config?: AgentConfig;
+  active: boolean;
+  runCount: number;
+  onClick: () => void;
+}) {
+  const { t } = useTranslation();
+  const model = config ? formatAgentModel(config, t) : t("agents.noModelSelected");
+  const skillCount = config?.skill_refs_json.length ?? 0;
+  return (
+    <button
+      type="button"
+      onClick={onClick}
+      className={[
+        "grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-md border p-3 text-left transition",
+        active ? "border-primary/45 bg-primary/10" : "border-border bg-muted/30 hover:bg-muted/55",
+      ].join(" ")}
+    >
+      <span className="min-w-0">
+        <span className="block truncate text-sm font-medium">{demoText(agent.name, t)}</span>
+        <span className="mt-1 block truncate text-xs text-muted-foreground">{model}</span>
+        <span className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
+          <span>{t("agents.runsBadge", { count: runCount })}</span>
+          <span className="text-border">|</span>
+          <span>{t("agents.skillsBadge", { count: skillCount })}</span>
+        </span>
+      </span>
+    </button>
+  );
+}
+
+function formatAgentModel(config: AgentConfig, t: ReturnType<typeof useTranslation>["t"]) {
+  const value = config.model_config_json.model;
+  return typeof value === "string" && value ? value : t("agents.noModelSelected");
+}
+
+function EditAgentDialog({
+  agent,
+  activeConfig,
+  open,
+  onOpenChange,
+  onSaved,
+}: {
+  agent: AgentDefinition;
+  activeConfig?: AgentConfig;
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  onSaved: () => void;
+}) {
+  const { t } = useTranslation();
+  const [name, setName] = React.useState(agent.name);
+  const [systemPrompt, setSystemPrompt] = React.useState(activeConfig?.system_prompt ?? "");
+  const [modelProviders, setModelProviders] = React.useState<ModelProvider[]>([]);
+  const [selectedProviderId, setSelectedProviderId] = React.useState("");
+  const [selectedModel, setSelectedModel] = React.useState("");
+  const [availableSkills, setAvailableSkills] = React.useState<SkillDefinition[]>([]);
+  const [selectedSkillIds, setSelectedSkillIds] = React.useState<string[]>([]);
+  const [skillsLoading, setSkillsLoading] = React.useState(false);
+  const [skillsError, setSkillsError] = React.useState<string | null>(null);
+  const [memoryScope, setMemoryScope] = React.useState("session");
+  const [temperature, setTemperature] = React.useState("0.7");
+  const [maxTokens, setMaxTokens] = React.useState("4096");
+  const [timeoutSeconds, setTimeoutSeconds] = React.useState("60");
+  const [retryAttempts, setRetryAttempts] = React.useState("2");
+  const [retryBackoffMs, setRetryBackoffMs] = React.useState("800");
+  const [toolCallLimit, setToolCallLimit] = React.useState("8");
+  const [contextWindow, setContextWindow] = React.useState("");
+  const [outputFormat, setOutputFormat] = React.useState("text");
+  const [humanApprovalPolicy, setHumanApprovalPolicy] = React.useState("never");
+  const [submitting, setSubmitting] = React.useState(false);
+
+  const currentProvider = modelProviders.find((provider) => provider.id === selectedProviderId);
+  const modelOptions = React.useMemo(() => {
+    return modelProviders.flatMap((provider) =>
+      provider.models
+        .filter((model) => model.model_type === "chat" || model.model_type === "reasoning")
+        .map((model) => ({
+          value: `${provider.id}:${model.model_id}`,
+          label: `${model.display_name} - ${provider.name}`,
+        }))
+    );
+  }, [modelProviders]);
+
+  React.useEffect(() => {
+    if (!open) return;
+    setName(agent.name);
+    hydrateFromConfig(activeConfig);
+    void listModelProviders().then((providers) => {
+      setModelProviders(providers);
+      hydrateModelSelection(providers, activeConfig);
+    });
+    setSkillsLoading(true);
+    setSkillsError(null);
+    void listSkills()
+      .then((skills) => setAvailableSkills(Array.isArray(skills) ? skills : []))
+      .catch((err) => {
+        setAvailableSkills([]);
+        setSkillsError(err instanceof Error ? err.message : t("agents.failedToLoadSkills"));
+      })
+      .finally(() => setSkillsLoading(false));
+  }, [open, agent, activeConfig]);
+
+  function hydrateFromConfig(config?: AgentConfig) {
+    const modelConfig = config?.model_config_json ?? {};
+    const memoryPolicy = config?.memory_policy_json ?? {};
+    const runtimePolicy = config?.runtime_policy_json ?? {};
+    const retryPolicy = getRecord(runtimePolicy, "retry_policy") ?? {};
+
+    setSystemPrompt(config?.system_prompt ?? "");
+    setSelectedSkillIds((config?.skill_refs_json ?? []).flatMap((item) => {
+      const skillId = getString(item, "skill_id");
+      return skillId ? [skillId] : [];
+    }));
+    setMemoryScope(getString(memoryPolicy, "memory_scope") ?? "session");
+    setTemperature(getConfigString(modelConfig, "temperature", "0.7"));
+    setMaxTokens(getConfigString(modelConfig, "max_tokens", "4096"));
+    setTimeoutSeconds(getConfigString(modelConfig, "timeout_seconds", "60"));
+    setContextWindow(getConfigString(modelConfig, "context_window", ""));
+    setOutputFormat(getString(modelConfig, "output_format") ?? "text");
+    setRetryAttempts(getConfigString(retryPolicy, "max_attempts", "2"));
+    setRetryBackoffMs(getConfigString(retryPolicy, "backoff_ms", "800"));
+    setToolCallLimit(getConfigString(runtimePolicy, "tool_call_limit", "8"));
+    setHumanApprovalPolicy(getString(runtimePolicy, "human_approval_policy") ?? "never");
+  }
+
+  function hydrateModelSelection(providers: ModelProvider[], config?: AgentConfig) {
+    const modelConfig = config?.model_config_json ?? {};
+    const providerType = getString(modelConfig, "provider");
+    const modelId = getString(modelConfig, "model");
+    const matchedProvider = providers.find((provider) =>
+      provider.provider_type === providerType && provider.models.some((model) => model.model_id === modelId)
+    );
+    if (matchedProvider && modelId) {
+      setSelectedProviderId(matchedProvider.id);
+      setSelectedModel(`${matchedProvider.id}:${modelId}`);
+      return;
+    }
+    const firstProvider = providers[0];
+    const firstModel = firstProvider?.models.find((model) => model.model_type === "chat" || model.model_type === "reasoning");
+    setSelectedProviderId(firstProvider?.id ?? "");
+    setSelectedModel(firstProvider && firstModel ? `${firstProvider.id}:${firstModel.model_id}` : "");
+  }
+
+  function toggleSkill(skillId: string) {
+    setSelectedSkillIds((current) =>
+      current.includes(skillId) ? current.filter((id) => id !== skillId) : [...current, skillId]
+    );
+  }
+
+  function selectModel(value: string) {
+    const [providerId] = value.split(":");
+    setSelectedProviderId(providerId ?? "");
+    setSelectedModel(value);
   }
-  return counts;
+
+  async function submit(event: React.FormEvent) {
+    event.preventDefault();
+    setSubmitting(true);
+    try {
+      await updateAgent(agent.id, { name: name.trim() });
+      await createAgentConfig({
+        agent_id: agent.id,
+        role: "assistant",
+        system_prompt: systemPrompt,
+        model_config_json: {
+          provider: currentProvider?.provider_type ?? getString(activeConfig?.model_config_json ?? {}, "provider") ?? "openai",
+          model: selectedModel.split(":").slice(1).join(":") || selectedModel,
+          temperature: parseOptionalFloat(temperature) ?? 0.7,
+          max_tokens: parseOptionalInteger(maxTokens) ?? 4096,
+          timeout_seconds: parseOptionalInteger(timeoutSeconds) ?? 60,
+          context_window: parseOptionalInteger(contextWindow),
+          output_format: outputFormat,
+        },
+        memory_policy_json: {
+          memory_scope: memoryScope,
+        },
+        runtime_policy_json: {
+          retry_policy: {
+            max_attempts: parseOptionalInteger(retryAttempts) ?? 2,
+            backoff_ms: parseOptionalInteger(retryBackoffMs) ?? 800,
+          },
+          tool_call_limit: parseOptionalInteger(toolCallLimit) ?? 8,
+          human_approval_policy: humanApprovalPolicy,
+        },
+        tool_refs_json: activeConfig?.tool_refs_json ?? [],
+        skill_refs_json: selectedSkillIds.map((skillId) => ({ skill_id: skillId })),
+        status: "draft",
+      });
+      toast.success(t("agents.agentUpdated"));
+      onOpenChange(false);
+      onSaved();
+    } catch {
+      toast.error(t("agents.failedToUpdateAgent"));
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange} title={t("agents.editAgent")} className="max-w-5xl">
+      <form className="space-y-5" onSubmit={submit}>
+        <div className="grid gap-5 lg:grid-cols-2">
+          <section className="space-y-4">
+            <div className="rounded-lg border border-border bg-muted/15 p-4">
+              <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.basicAgent")}</p>
+              <div className="mt-4 space-y-4">
+                <Field label={t("common.name")} required>
+                  <Input required value={name} onChange={(event) => setName(event.target.value)} placeholder={t("agents.namePlaceholder")} />
+                </Field>
+                <Field label={t("agents.systemPrompt")}>
+                  <Textarea value={systemPrompt} onChange={(event) => setSystemPrompt(event.target.value)} placeholder={t("agents.systemPromptPlaceholder")} rows={8} />
+                </Field>
+              </div>
+            </div>
+
+            <div className="rounded-lg border border-border bg-muted/10 p-4">
+              <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.skills")}</p>
+              <div className="mt-4">
+                {skillsLoading ? (
+                  <div className="rounded-md border border-dashed border-border bg-muted/20 p-3 text-sm text-muted-foreground">
+                    {t("agents.loadingSkills")}
+                  </div>
+                ) : skillsError ? (
+                  <div className="rounded-md border border-dashed border-red-500/30 bg-red-500/5 p-3 text-sm text-red-500">
+                    {skillsError}
+                  </div>
+                ) : availableSkills.length > 0 ? (
+                  <div className="flex max-h-44 flex-wrap gap-2 overflow-auto pr-1">
+                    {availableSkills.map((skill) => (
+                      <button
+                        key={skill.id}
+                        type="button"
+                        onClick={() => toggleSkill(skill.id)}
+                        className={`flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition ${
+                          selectedSkillIds.includes(skill.id)
+                            ? "border-primary bg-primary/15 text-primary"
+                            : "border-border bg-muted/30 text-muted-foreground hover:bg-muted/60"
+                        }`}
+                      >
+                        <Sparkles className="h-3 w-3" />
+                        {skill.name}
+                      </button>
+                    ))}
+                  </div>
+                ) : (
+                  <div className="rounded-md border border-dashed border-border bg-muted/20 p-3 text-sm text-muted-foreground">
+                    {t("agents.noSkillsYet")}
+                  </div>
+                )}
+              </div>
+            </div>
+          </section>
+
+          <aside className="space-y-4 rounded-lg border border-border bg-surface p-4 lg:sticky lg:top-24 lg:max-h-[calc(100dvh-12rem)] lg:overflow-auto">
+            <div>
+              <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.modelAndMemory")}</p>
+              <p className="mt-1 text-sm text-muted-foreground">{t("agents.modelAndMemoryHint")}</p>
+            </div>
+
+            <div className="space-y-4">
+              <Field label={t("agents.model")}>
+                <Select value={selectedModel} onChange={(event) => selectModel(event.target.value)} options={modelOptions} />
+              </Field>
+              <div className="grid gap-3 sm:grid-cols-2">
+                <Field label={t("agents.temperature")}>
+                  <Input value={temperature} onChange={(event) => setTemperature(event.target.value)} inputMode="decimal" />
+                </Field>
+                <Field label={t("agents.maxTokens")}>
+                  <Input value={maxTokens} onChange={(event) => setMaxTokens(event.target.value)} inputMode="numeric" />
+                </Field>
+                <Field label={t("agents.timeoutSeconds")}>
+                  <Input value={timeoutSeconds} onChange={(event) => setTimeoutSeconds(event.target.value)} inputMode="numeric" />
+                </Field>
+                <Field label={t("agents.contextWindow")}>
+                  <Input value={contextWindow} onChange={(event) => setContextWindow(event.target.value)} inputMode="numeric" placeholder={t("agents.auto")} />
+                </Field>
+              </div>
+            </div>
+
+            <div className="h-px bg-border" />
+
+            <div className="space-y-4">
+              <Field label={t("agents.memoryScope")}>
+                <Select
+                  value={memoryScope}
+                  onChange={(event) => setMemoryScope(event.target.value)}
+                  options={[
+                    { value: "session", label: t("agents.memoryScopeSession") },
+                    { value: "persistent", label: t("agents.memoryScopePersistent") },
+                  ]}
+                />
+              </Field>
+            </div>
+          </aside>
+        </div>
+
+        <div className="rounded-lg border border-border bg-muted/10 p-4">
+          <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.executionPolicy")}</p>
+          <div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
+            <Field label={t("agents.retryAttempts")}>
+              <Input value={retryAttempts} onChange={(event) => setRetryAttempts(event.target.value)} inputMode="numeric" />
+            </Field>
+            <Field label={t("agents.retryBackoffMs")}>
+              <Input value={retryBackoffMs} onChange={(event) => setRetryBackoffMs(event.target.value)} inputMode="numeric" />
+            </Field>
+            <Field label={t("agents.toolCallLimit")}>
+              <Input value={toolCallLimit} onChange={(event) => setToolCallLimit(event.target.value)} inputMode="numeric" />
+            </Field>
+            <Field label={t("agents.outputFormat")}>
+              <Select
+                value={outputFormat}
+                onChange={(event) => setOutputFormat(event.target.value)}
+                options={[
+                  { value: "text", label: t("agents.outputText") },
+                  { value: "json", label: "JSON" },
+                  { value: "markdown", label: "Markdown" },
+                ]}
+              />
+            </Field>
+            <Field label={t("agents.humanApproval")}>
+              <Select
+                value={humanApprovalPolicy}
+                onChange={(event) => setHumanApprovalPolicy(event.target.value)}
+                options={[
+                  { value: "never", label: t("agents.approvalNever") },
+                  { value: "sensitive_actions", label: t("agents.approvalSensitiveActions") },
+                  { value: "before_final", label: t("agents.approvalBeforeFinal") },
+                  { value: "always", label: t("agents.approvalAlways") },
+                ]}
+              />
+            </Field>
+          </div>
+        </div>
+
+        <div className="flex justify-end gap-2 border-t border-border pt-4">
+          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
+            {t("common.cancel")}
+          </Button>
+          <Button disabled={submitting || !name.trim()}>{submitting ? t("common.saving") : t("common.save")}</Button>
+        </div>
+      </form>
+    </Dialog>
+  );
 }
+
+function Field({ label, children, required }: { label: string; children: React.ReactNode; required?: boolean }) {
+  return (
+    <label className="block space-y-2 text-sm">
+      <span className="text-muted-foreground">
+        {label}
+        {required && <span className="text-red-500 ml-0.5">*</span>}
+      </span>
+      {children}
+    </label>
+  );
+}
+
+function parseOptionalInteger(value: string) {
+  if (!value.trim()) return null;
+  const parsed = Number.parseInt(value, 10);
+  return Number.isFinite(parsed) ? parsed : null;
+}
+
+function parseOptionalFloat(value: string) {
+  if (!value.trim()) return null;
+  const parsed = Number.parseFloat(value);
+  return Number.isFinite(parsed) ? parsed : null;
+}
+
+function getConfigString(value: JSONObject, key: string, fallback: string) {
+  const item = value[key];
+  if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") return String(item);
+  return fallback;
+}
+
+function getString(value: JSONObject, key: string) {
+  const item = value[key];
+  return typeof item === "string" ? item : undefined;
+}
+
+function getRecord(value: JSONObject, key: string): JSONObject | undefined {
+  const item = value[key];
+  return item && typeof item === "object" && !Array.isArray(item) ? item as JSONObject : undefined;
+}
+

+ 0 - 30
web/src/pages/agents/components/AgentCard.tsx

@@ -1,30 +0,0 @@
-import { Bot, Eye } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { StatusBadge } from "@/components/shared/StatusBadge";
-import type { AgentDefinition } from "@/types";
-
-export function AgentCard({ agent, onOpen }: { agent: AgentDefinition; onOpen: () => void }) {
-  return (
-    <Card>
-      <CardHeader>
-        <div className="flex items-start justify-between">
-          <div className="grid h-10 w-10 place-items-center rounded-md bg-primary/15 text-primary">
-            <Bot className="h-5 w-5" />
-          </div>
-          <StatusBadge status={agent.status} />
-        </div>
-        <CardTitle className="truncate">{agent.name}</CardTitle>
-      </CardHeader>
-      <CardContent className="space-y-4">
-        <div className="space-y-2 text-sm text-muted-foreground">
-          <p>{agent.agent_type}</p>
-          <p className="line-clamp-2">{agent.description ?? "No description"}</p>
-        </div>
-        <Button variant="outline" className="w-full" onClick={onOpen}>
-          <Eye className="h-4 w-4" /> Details
-        </Button>
-      </CardContent>
-    </Card>
-  );
-}

+ 0 - 61
web/src/pages/agents/components/AgentDetailSheet.tsx

@@ -1,61 +0,0 @@
-import * as React from "react";
-import { Sheet } from "@/components/ui/sheet";
-import { Tabs } from "@/components/ui/tabs";
-import { JsonViewer } from "@/components/shared/JsonViewer";
-import { StatusBadge } from "@/components/shared/StatusBadge";
-import { useAgentRuns, useAgentVersions } from "@/hooks";
-import type { AgentDefinition } from "@/types";
-
-export function AgentDetailSheet({
-  agent,
-  open,
-  onOpenChange,
-}: {
-  agent?: AgentDefinition;
-  open: boolean;
-  onOpenChange: (open: boolean) => void;
-}) {
-  const [tab, setTab] = React.useState("details");
-  const versions = useAgentVersions(open ? agent?.id : undefined);
-  const runs = useAgentRuns(open ? agent?.id : undefined);
-  if (!agent) return null;
-  return (
-    <Sheet open={open} onOpenChange={onOpenChange} title={agent.name} description={agent.agent_type} className="max-w-2xl">
-      <Tabs
-        value={tab}
-        onChange={setTab}
-        tabs={[
-          { value: "details", label: "Details", content: <JsonViewer value={agent} collapsed={false} /> },
-          {
-            value: "versions",
-            label: "Versions",
-            content: (
-              <div className="space-y-2">
-                {(versions.data ?? []).map((version) => (
-                  <div key={version.id} className="flex items-center justify-between rounded-md border border-border p-3 text-sm">
-                    <span>v{version.version_no}</span>
-                    <StatusBadge status={version.status} />
-                  </div>
-                ))}
-              </div>
-            ),
-          },
-          {
-            value: "runs",
-            label: "Runs",
-            content: (
-              <div className="space-y-2">
-                {(runs.data ?? []).map((run) => (
-                  <div key={run.id} className="flex items-center justify-between rounded-md border border-border p-3 text-sm">
-                    <span className="font-mono text-xs">{run.id}</span>
-                    <StatusBadge status={run.status} />
-                  </div>
-                ))}
-              </div>
-            ),
-          },
-        ]}
-      />
-    </Sheet>
-  );
-}

+ 188 - 55
web/src/pages/agents/components/AgentOverview.tsx

@@ -1,93 +1,226 @@
 import { useTranslation } from "react-i18next";
-import { TerminalSquare } from "lucide-react";
-import { EmptyState } from "@/components/shared/EmptyState";
-import { JsonViewer } from "@/components/shared/JsonViewer";
+import { Brain, CheckCircle2, Cpu, History, Settings2, Sparkles } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { demoText } from "@/lib/demo-text";
 import { formatDateTime } from "@/lib/utils";
-import type { AgentDefinition, AgentVersion } from "@/types";
+import type { AgentConfig, AgentDefinition, JSONObject, SkillDefinition } from "@/types";
 
 export function AgentOverview({
   agent,
-  latestVersion,
-  versionCount,
+  activeConfig,
+  skills,
   runCount,
   failedRunCount,
 }: {
   agent: AgentDefinition;
-  latestVersion?: AgentVersion;
-  versionCount: number;
+  activeConfig?: AgentConfig;
+  skills: SkillDefinition[];
   runCount: number;
   failedRunCount: number;
 }) {
   const { t } = useTranslation();
+  const modelConfig = activeConfig?.model_config_json ?? {};
+  const memoryPolicy = activeConfig?.memory_policy_json ?? {};
+  const runtimePolicy = activeConfig?.runtime_policy_json ?? {};
+  const retryPolicy = getRecord(runtimePolicy.retry_policy);
+  const selectedSkills = getSelectedSkills(activeConfig?.skill_refs_json ?? [], skills);
+  const modelName = formatValue(modelConfig.model) || t("agents.noModelSelected");
+  const provider = formatProvider(formatValue(modelConfig.provider), t) || t("agents.providerNotSet");
+  const memoryScope = formatValue(memoryPolicy.memory_scope) || "session";
+  const health = runCount ? Math.round(((runCount - failedRunCount) / runCount) * 100) : 100;
+
   return (
     <div className="space-y-6">
-      {/* Summary tiles */}
-      <div className="grid gap-4 md:grid-cols-4">
-        <SummaryTile label={t("common.versions")} value={versionCount} />
-        <SummaryTile label={t("common.runs")} value={runCount} />
-        <SummaryTile label={t("agents.failures")} value={failedRunCount} />
-        <SummaryTile label={t("agents.latest")} value={latestVersion ? `v${latestVersion.version_no}` : t("agents.none")} />
-      </div>
+      {activeConfig ? (
+        <>
+          <section className="rounded-xl border border-border bg-muted/15 p-5">
+            <div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
+              <div>
+                <div className="flex flex-wrap items-center gap-2">
+                  <Badge className="border-primary/20 bg-primary/10 text-primary">{t("tools.configured")}</Badge>
+                  <span className="text-xs text-muted-foreground">{t("common.created")} {formatDateTime(agent.created_time)}</span>
+                </div>
+                <h2 className="mt-4 text-2xl font-semibold tracking-tight">{modelName}</h2>
+                <p className="mt-1 text-sm text-muted-foreground">{provider}</p>
+              </div>
+              <div className="grid gap-3 sm:grid-cols-2 xl:min-w-[520px] xl:grid-cols-4">
+                <SignalTile icon={CheckCircle2} label={t("agents.health")} value={`${health}%`} />
+                <SignalTile icon={History} label={t("common.runs")} value={runCount} />
+                <SignalTile icon={Sparkles} label={t("skills.title")} value={selectedSkills.length} />
+                <SignalTile icon={Brain} label={t("agents.memorySection")} value={readableLabel(memoryScope, t)} />
+              </div>
+            </div>
+          </section>
 
-      {/* Agent details */}
-      <div className="grid gap-4 text-sm md:grid-cols-2">
-        <Detail label={t("common.code")} value={agent.code} mono />
-        <Detail label={t("common.type")} value={agent.agent_type} />
-        <Detail label={t("agents.owner")} value={agent.owner_user_id ?? t("agents.unassigned")} mono />
-        <Detail label={t("common.created")} value={formatDateTime(agent.created_time)} />
-        <div className="md:col-span-2">
-          <p className="text-muted-foreground">{t("common.description")}</p>
-          <p className="mt-1 leading-6">{agent.description ?? t("agents.noDescription")}</p>
-        </div>
-      </div>
+          <div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_420px]">
+            <section className="rounded-xl border border-border bg-muted/15 p-5">
+              <SectionHeader icon={Sparkles} title={t("skills.title")} meta={t("agents.selectedCount", { count: selectedSkills.length })} />
+              {selectedSkills.length ? (
+                <div className="mt-4 grid gap-3 md:grid-cols-2">
+                  {selectedSkills.map((skill) => (
+                    <div key={skill.id} className="rounded-lg border border-border bg-surface-elevated p-4">
+                      <div className="flex items-start justify-between gap-3">
+                        <div className="min-w-0">
+                          <p className="truncate text-sm font-semibold">{demoText(skill.name, t)}</p>
+                          <p className="mt-1 text-xs text-muted-foreground">{readableLabel(skill.skill_type, t)}</p>
+                        </div>
+                        <Badge className="border-primary/20 bg-primary/10 text-primary">{t("skills.title")}</Badge>
+                      </div>
+                      <p className="mt-3 line-clamp-2 text-sm leading-6 text-muted-foreground">
+                        {demoText(skill.description, t) || t("agents.noDescription")}
+                      </p>
+                    </div>
+                  ))}
+                </div>
+              ) : (
+                <EmptyInline title={t("agents.noSkillsSelected")} description={t("agents.attachSkillsHint")} />
+              )}
+            </section>
 
-      {/* Config section (merged from Config tab) */}
-      {latestVersion ? (
-        <div className="space-y-4">
-          <section className="space-y-2">
-            <div className="flex items-center gap-2">
-              <TerminalSquare className="h-4 w-4 text-muted-foreground" />
-              <h3 className="text-sm font-semibold">{t("agents.systemPrompt")}</h3>
+            <section className="rounded-xl border border-border bg-muted/15 p-5">
+              <SectionHeader icon={Cpu} title={t("agents.runtimeConfiguration")} />
+              <div className="mt-4 space-y-5">
+                <ConfigGroup
+                  title={t("agents.model")}
+                  items={[
+                    [t("agents.temperature"), formatValue(modelConfig.temperature) || "0.7"],
+                    [t("agents.maxTokens"), formatValue(modelConfig.max_tokens) || "4096"],
+                    [t("agents.context"), formatValue(modelConfig.context_window) || t("agents.auto")],
+                    [t("agents.output"), readableLabel(formatValue(modelConfig.output_format) || "text", t)],
+                  ]}
+                />
+                <ConfigGroup
+                  title={t("agents.execution")}
+                  items={[
+                    [t("agents.retries"), formatValue(retryPolicy.max_attempts) || "2"],
+                    [t("agents.backoff"), retryPolicy.backoff_ms ? `${formatValue(retryPolicy.backoff_ms)}ms` : t("agents.auto")],
+                    [t("agents.toolLimit"), formatValue(runtimePolicy.tool_call_limit) || "8"],
+                    [t("agents.review"), readableLabel(formatValue(runtimePolicy.human_approval_policy) || "never", t)],
+                  ]}
+                />
+              </div>
+            </section>
+          </div>
+        </>
+      ) : (
+        <div className="space-y-5">
+          <section className="rounded-xl border border-border bg-muted/15 p-5">
+            <div className="flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between">
+              <div>
+                <div className="flex flex-wrap items-center gap-2">
+                  <Badge className="border-amber-500/25 bg-amber-500/10 text-amber-700 dark:text-amber-200">{t("agents.needsSetup")}</Badge>
+                  <span className="text-xs text-muted-foreground">{t("common.created")} {formatDateTime(agent.created_time)}</span>
+                </div>
+                <h2 className="mt-4 text-2xl font-semibold tracking-tight">{t("agents.noModelSelected")}</h2>
+                <p className="mt-1 text-sm text-muted-foreground">{t("agents.setupHint")}</p>
+              </div>
+              <div className="grid gap-3 sm:grid-cols-2 xl:min-w-[520px] xl:grid-cols-4">
+                <SignalTile icon={CheckCircle2} label={t("agents.health")} value={`${health}%`} />
+                <SignalTile icon={History} label={t("common.runs")} value={runCount} />
+                <SignalTile icon={Sparkles} label={t("skills.title")} value={0} />
+                <SignalTile icon={Brain} label={t("agents.memorySection")} value={t("agents.notSet")} />
+              </div>
+            </div>
+          </section>
+
+          <section className="rounded-xl border border-border bg-muted/15 p-5">
+            <SectionHeader icon={Settings2} title={t("agents.setupChecklist")} />
+            <div className="mt-4 grid gap-3 md:grid-cols-3">
+              <ChecklistItem title={t("agents.chooseModel")} description={t("agents.chooseModelHint")} />
+              <ChecklistItem title={t("agents.attachSkills")} description={t("agents.attachSkillsShortHint")} />
+              <ChecklistItem title={t("agents.tuneRuntime")} description={t("agents.tuneRuntimeHint")} />
             </div>
-            <div className="rounded-md border border-border bg-muted/30 p-4 text-sm leading-6">{latestVersion.system_prompt}</div>
           </section>
-          <div className="grid gap-4 lg:grid-cols-2">
-            <ConfigBlock title={t("agents.modelConfig")} value={latestVersion.model_config_json} />
-            <ConfigBlock title={t("agents.memoryPolicy")} value={latestVersion.memory_policy_json} />
-            <ConfigBlock title={t("agents.toolReferences")} value={latestVersion.tool_refs_json} />
-            <ConfigBlock title={t("agents.skillReferences")} value={latestVersion.skill_refs_json} />
-          </div>
         </div>
-      ) : (
-        <EmptyState icon={TerminalSquare} title={t("agents.noConfig")} description={t("agents.publishOrDraft")} />
       )}
     </div>
   );
 }
 
-function SummaryTile({ label, value }: { label: string; value: string | number }) {
+function SignalTile({ icon: Icon, label, value }: { icon: typeof CheckCircle2; label: string; value: string | number }) {
+  return (
+    <div className="rounded-lg border border-border bg-surface-elevated p-3">
+      <div className="flex items-center gap-2 text-xs text-muted-foreground">
+        <Icon className="h-3.5 w-3.5 text-primary" />
+        {label}
+      </div>
+      <p className="mt-2 truncate text-lg font-semibold tabular-nums">{value}</p>
+    </div>
+  );
+}
+
+function SectionHeader({ icon: Icon, title, meta }: { icon: typeof Cpu; title: string; meta?: string }) {
   return (
-    <div className="rounded-md border border-border bg-muted/30 p-3">
-      <p className="text-xs text-muted-foreground">{label}</p>
-      <p className="mt-1 text-xl font-semibold tabular-nums">{value}</p>
+    <div className="flex flex-wrap items-center justify-between gap-3">
+      <div className="flex items-center gap-2">
+        <span className="grid h-8 w-8 place-items-center rounded-lg bg-primary/10 text-primary">
+          <Icon className="h-4 w-4" />
+        </span>
+        <h3 className="text-sm font-semibold">{title}</h3>
+      </div>
+      {meta ? <span className="text-xs text-muted-foreground">{meta}</span> : null}
+    </div>
+  );
+}
+
+function ConfigGroup({ title, items }: { title: string; items: Array<[string, string]> }) {
+  return (
+    <div>
+      <h4 className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">{title}</h4>
+      <div className="mt-3 space-y-2">
+        {items.map(([label, value]) => (
+          <div key={label} className="grid grid-cols-[112px_minmax(0,1fr)] gap-3 text-sm">
+            <span className="text-muted-foreground">{label}</span>
+            <span className="truncate">{value || "-"}</span>
+          </div>
+        ))}
+      </div>
     </div>
   );
 }
 
-function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
+function EmptyInline({ title, description }: { title: string; description: string }) {
   return (
-    <div className="min-w-0">
-      <p className="text-muted-foreground">{label}</p>
-      <p className={mono ? "mt-1 truncate font-mono text-xs" : "mt-1 truncate"}>{value}</p>
+    <div className="mt-4 rounded-lg border border-dashed border-border bg-surface-elevated p-4">
+      <p className="text-sm font-medium">{title}</p>
+      <p className="mt-1 text-sm leading-6 text-muted-foreground">{description}</p>
     </div>
   );
 }
 
-function ConfigBlock({ title, value }: { title: string; value: unknown }) {
+function ChecklistItem({ title, description }: { title: string; description: string }) {
   return (
-    <section className="min-w-0 rounded-md border border-border bg-muted/30 p-4">
-      <h3 className="mb-3 text-sm font-semibold">{title}</h3>
-      <JsonViewer value={value} />
-    </section>
+    <div className="rounded-lg border border-border bg-surface-elevated p-4">
+      <p className="text-sm font-semibold">{title}</p>
+      <p className="mt-2 text-sm leading-6 text-muted-foreground">{description}</p>
+    </div>
   );
 }
+
+function getRecord(value: unknown): Record<string, unknown> {
+  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
+}
+
+function formatValue(value: unknown): string {
+  if (typeof value === "string") return value;
+  if (typeof value === "number" || typeof value === "boolean") return String(value);
+  return "";
+}
+
+function readableLabel(value: string, t: ReturnType<typeof useTranslation>["t"]) {
+  const fallback = value.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
+  return t(`agents.valueLabels.${value}`, fallback);
+}
+
+function formatProvider(value: string, t: ReturnType<typeof useTranslation>["t"]) {
+  if (!value) return "";
+  return t(`models.provider${value.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`, readableLabel(value, t));
+}
+
+function getSelectedSkills(refs: JSONObject[], skills: SkillDefinition[]) {
+  const selectedIds = new Set(refs.flatMap((ref) => {
+    const value = ref.skill_id;
+    return typeof value === "string" ? [value] : [];
+  }));
+  return skills.filter((skill) => selectedIds.has(skill.id));
+}

+ 3 - 9
web/src/pages/agents/components/AgentRuns.tsx

@@ -16,18 +16,15 @@ export function AgentRuns({
   agentId,
   runs,
   loading,
-  statusFilter,
-  onStatusFilterChange,
 }: {
   agentId: string;
   runs: AgentRun[];
   loading: boolean;
-  statusFilter: RunStatusFilter;
-  onStatusFilterChange: (status: RunStatusFilter) => void;
 }) {
   const { t } = useTranslation();
   const [testInput, setTestInput] = React.useState("");
   const [testResult, setTestResult] = React.useState<AgentRun | undefined>();
+  const [statusFilter, setStatusFilter] = React.useState<RunStatusFilter>("all");
 
   function simulateRun() {
     const timestamp = new Date().toISOString();
@@ -46,7 +43,6 @@ export function AgentRuns({
       lease_expire_time: null,
       started_time: timestamp,
       finished_time: timestamp,
-      error_code: null,
       error_message: null,
       created_time: timestamp,
     });
@@ -58,7 +54,6 @@ export function AgentRuns({
 
   return (
     <div className="space-y-6">
-      {/* Inline test console */}
       <div className="grid gap-4 lg:grid-cols-[1fr_360px]">
         <div className="space-y-3">
           <label className="block space-y-2 text-sm">
@@ -82,15 +77,14 @@ export function AgentRuns({
         </div>
       </div>
 
-      {/* Run history */}
       <div className="space-y-3">
         <div className="flex items-center justify-between gap-3">
-          <h3 className="text-sm font-semibold">{t("common.runs")}</h3>
+          <h3 className="text-sm font-semibold">{t("common.runs")} ({filteredRuns.length})</h3>
           <Select
             className="w-48"
             aria-label={t("common.filterRunsByStatus")}
             value={statusFilter}
-            onChange={(event) => onStatusFilterChange(event.target.value as RunStatusFilter)}
+            onChange={(event) => setStatusFilter(event.target.value as RunStatusFilter)}
             options={[
               { value: "all", label: t("common.all") },
               { value: "queued", label: t("common.queued") },

+ 0 - 54
web/src/pages/agents/components/AgentVersions.tsx

@@ -1,54 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { FileCode2 } from "lucide-react";
-import { EmptyState } from "@/components/shared/EmptyState";
-import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
-import { StatusBadge } from "@/components/shared/StatusBadge";
-import { formatDateTime } from "@/lib/utils";
-import type { AgentVersion } from "@/types";
-
-export function AgentVersions({ versions, loading }: { versions: AgentVersion[]; loading: boolean }) {
-  const { t } = useTranslation();
-  if (loading) return <LoadingSpinner label={t("common.loading")} />;
-  if (!versions.length) return <EmptyState icon={FileCode2} title={t("agents.noVersions")} description={t("agents.createVersionDefine")} />;
-
-  const sorted = [...versions].sort((a, b) => b.version_no - a.version_no);
-
-  return (
-    <div className="space-y-3">
-      {sorted.map((version) => (
-        <div key={version.id} className="rounded-md border border-border bg-muted/30 p-4">
-          <div className="flex flex-wrap items-start justify-between gap-3">
-            <div>
-              <div className="flex flex-wrap items-center gap-2">
-                <p className="font-medium">v{version.version_no}</p>
-                <StatusBadge status={version.status} />
-              </div>
-              <p className="mt-1 text-sm text-muted-foreground">{version.goal ?? t("agents.noDescription")}</p>
-            </div>
-            <p className="text-xs text-muted-foreground">{formatDateTime(version.created_time)}</p>
-          </div>
-          <div className="mt-4 grid gap-3 text-sm md:grid-cols-3">
-            <div className="min-w-0">
-              <p className="text-muted-foreground">{t("agents.role")}</p>
-              <p className="mt-1">{version.role}</p>
-            </div>
-            <div className="min-w-0">
-              <p className="text-muted-foreground">{t("agents.model")}</p>
-              <p className="mt-1 truncate font-mono text-xs">{stringifyConfigValue(version.model_config_json.model) ?? t("agents.noDescription")}</p>
-            </div>
-            <div className="min-w-0">
-              <p className="text-muted-foreground">{t("agents.published")}</p>
-              <p className="mt-1">{version.published_time ? formatDateTime(version.published_time) : t("agents.notPublished")}</p>
-            </div>
-          </div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-function stringifyConfigValue(value: unknown) {
-  if (typeof value === "string") return value;
-  if (typeof value === "number" || typeof value === "boolean") return String(value);
-  return undefined;
-}

+ 227 - 209
web/src/pages/agents/components/CreateAgentDialog.tsx

@@ -1,105 +1,106 @@
-import * as React from "react";
+import * as React from "react";
 import { useTranslation } from "react-i18next";
-import { Bot, Plus, Settings2, TerminalSquare, Wrench, Brain } from "lucide-react";
-import { createAgent, createAgentVersion } from "@/api";
-import { listModelProviders } from "@/api/model-providers";
-import { listTools } from "@/api";
+import { Plus, Sparkles } from "lucide-react";
+import { createAgent, createAgentConfig, listModelProviders, listSkills } from "@/api";
 import { Button } from "@/components/ui/button";
 import { Dialog } from "@/components/ui/dialog";
 import { Input, Textarea } from "@/components/ui/input";
 import { Select } from "@/components/ui/select";
 import { toast } from "@/components/ui/toaster";
-import { slugifyName } from "@/lib/utils";
 import { useAuthStore } from "@/stores/auth";
-import type { ModelProvider, ToolDefinition } from "@/types";
+import type { ModelProvider, SkillDefinition } from "@/types";
 
 export function CreateAgentDialog({ onCreated }: { onCreated: () => void }) {
   const { t } = useTranslation();
   const [open, setOpen] = React.useState(false);
   const userId = useAuthStore((state) => state.userId);
-  const [codeTouched, setCodeTouched] = React.useState(false);
   const [submitting, setSubmitting] = React.useState(false);
 
-  // Basic info
-  const [form, setForm] = React.useState({ name: "", code: "", description: "", agent_type: "assistant" });
-
-  // Version config
-  const [goal, setGoal] = React.useState("");
+  const [name, setName] = React.useState("");
   const [systemPrompt, setSystemPrompt] = React.useState("");
 
-  // Model config
   const [modelProviders, setModelProviders] = React.useState<ModelProvider[]>([]);
   const [selectedProviderId, setSelectedProviderId] = React.useState("");
   const [selectedModel, setSelectedModel] = React.useState("");
-  const [temperature, setTemperature] = React.useState("0.7");
-  const [maxTokens, setMaxTokens] = React.useState("4096");
 
-  // Tools
-  const [availableTools, setAvailableTools] = React.useState<ToolDefinition[]>([]);
-  const [selectedToolCodes, setSelectedToolCodes] = React.useState<string[]>([]);
-
-  // Memory
-  const [memoryEnabled, setMemoryEnabled] = React.useState(true);
+  const [availableSkills, setAvailableSkills] = React.useState<SkillDefinition[]>([]);
+  const [selectedSkillIds, setSelectedSkillIds] = React.useState<string[]>([]);
+  const [skillsLoading, setSkillsLoading] = React.useState(false);
+  const [skillsError, setSkillsError] = React.useState<string | null>(null);
   const [memoryScope, setMemoryScope] = React.useState("session");
+  const [temperature, setTemperature] = React.useState("0.7");
+  const [maxTokens, setMaxTokens] = React.useState("4096");
+  const [timeoutSeconds, setTimeoutSeconds] = React.useState("60");
+  const [retryAttempts, setRetryAttempts] = React.useState("2");
+  const [retryBackoffMs, setRetryBackoffMs] = React.useState("800");
+  const [toolCallLimit, setToolCallLimit] = React.useState("8");
+  const [contextWindow, setContextWindow] = React.useState("");
+  const [outputFormat, setOutputFormat] = React.useState("text");
+  const [humanApprovalPolicy, setHumanApprovalPolicy] = React.useState("never");
 
-  const agentTypes = [
-    { value: "assistant", label: t("agents.typeAssistant") },
-    { value: "planner", label: t("agents.typePlanner") },
-    { value: "executor", label: t("agents.typeExecutor") },
-    { value: "research", label: t("agents.typeResearch") },
-    { value: "tool_user", label: t("agents.typeToolUser") },
-  ];
+  const currentProvider = modelProviders.find((p) => p.id === selectedProviderId);
+  const modelOptions = React.useMemo(() => {
+    return modelProviders.flatMap((provider) =>
+      provider.models
+        .filter((model) => model.model_type === "chat" || model.model_type === "reasoning")
+        .map((model) => ({
+          value: `${provider.id}:${model.model_id}`,
+          label: `${model.display_name} - ${provider.name}`,
+        }))
+    );
+  }, [modelProviders]);
 
-  // Load providers and tools when dialog opens
   React.useEffect(() => {
     if (!open) return;
     void listModelProviders().then((providers) => {
-      const active = providers.filter((p) => p.status === "active");
-      setModelProviders(active);
-      if (active[0]) {
-        setSelectedProviderId(active[0].id);
-        const chatModels = active[0].models.filter((m) => m.enabled && (m.model_type === "chat" || m.model_type === "reasoning"));
-        if (chatModels[0]) setSelectedModel(chatModels[0].model_id);
+      setModelProviders(providers);
+      if (providers[0]) {
+        const chatModels = providers[0].models.filter((m) => m.model_type === "chat" || m.model_type === "reasoning");
+        if (chatModels[0]) {
+          setSelectedProviderId(providers[0].id);
+          setSelectedModel(`${providers[0].id}:${chatModels[0].model_id}`);
+        }
       }
     });
-    void listTools().then(setAvailableTools).catch(() => {});
+    setSkillsLoading(true);
+    setSkillsError(null);
+    void listSkills()
+      .then((skills) => setAvailableSkills(Array.isArray(skills) ? skills : []))
+      .catch((err) => {
+        setAvailableSkills([]);
+        setSkillsError(err instanceof Error ? err.message : t("agents.failedToLoadSkills"));
+      })
+      .finally(() => setSkillsLoading(false));
   }, [open]);
 
-  // Available models for selected provider
-  const currentProvider = modelProviders.find((p) => p.id === selectedProviderId);
-  const modelOptions = React.useMemo(() => {
-    if (!currentProvider) return [];
-    return currentProvider.models
-      .filter((m) => m.enabled && (m.model_type === "chat" || m.model_type === "reasoning"))
-      .map((m) => ({ value: m.model_id, label: m.display_name }));
-  }, [currentProvider]);
-
-  function updateName(name: string) {
-    setForm((current) => ({
-      ...current,
-      name,
-      code: codeTouched ? current.code : slugifyName(name, "agent"),
-    }));
-  }
-
-  function toggleTool(code: string) {
-    setSelectedToolCodes((current) =>
-      current.includes(code) ? current.filter((c) => c !== code) : [...current, code],
-    );
-  }
-
   function reset() {
-    setForm({ name: "", code: "", description: "", agent_type: "assistant" });
-    setGoal("");
+    setName("");
     setSystemPrompt("");
     setSelectedProviderId("");
     setSelectedModel("");
+    setSelectedSkillIds([]);
+    setMemoryScope("session");
     setTemperature("0.7");
     setMaxTokens("4096");
-    setSelectedToolCodes([]);
-    setMemoryEnabled(true);
-    setMemoryScope("session");
-    setCodeTouched(false);
+    setTimeoutSeconds("60");
+    setRetryAttempts("2");
+    setRetryBackoffMs("800");
+    setToolCallLimit("8");
+    setContextWindow("");
+    setOutputFormat("text");
+    setHumanApprovalPolicy("never");
+  }
+
+  function toggleSkill(skillId: string) {
+    setSelectedSkillIds((current) =>
+      current.includes(skillId) ? current.filter((id) => id !== skillId) : [...current, skillId]
+    );
+  }
+
+  function selectModel(value: string) {
+    const [providerId] = value.split(":");
+    setSelectedProviderId(providerId ?? "");
+    setSelectedModel(value);
   }
 
   async function submit(event: React.FormEvent) {
@@ -108,29 +109,36 @@ export function CreateAgentDialog({ onCreated }: { onCreated: () => void }) {
     try {
       const agent = await createAgent({
         owner_user_id: userId,
-        name: form.name.trim(),
-        code: form.code.trim(),
-        description: form.description.trim() || undefined,
-        agent_type: form.agent_type,
+        name: name.trim(),
+        agent_type: "assistant",
       });
 
-      await createAgentVersion({
+      await createAgentConfig({
         agent_id: agent.id,
-        role: form.agent_type,
-        goal: goal.trim() || null,
+        role: "assistant",
         system_prompt: systemPrompt,
         model_config_json: {
           provider: currentProvider?.provider_type ?? "openai",
-          model: selectedModel,
-          temperature: Number(temperature),
-          max_tokens: Number(maxTokens),
+          model: selectedModel.split(":").slice(1).join(":") || selectedModel,
+          temperature: parseOptionalFloat(temperature) ?? 0.7,
+          max_tokens: parseOptionalInteger(maxTokens) ?? 4096,
+          timeout_seconds: parseOptionalInteger(timeoutSeconds) ?? 60,
+          context_window: parseOptionalInteger(contextWindow),
+          output_format: outputFormat,
         },
         memory_policy_json: {
-          enabled: memoryEnabled,
           memory_scope: memoryScope,
         },
-        tool_refs_json: selectedToolCodes.map((code) => ({ tool_code: code })),
-        skill_refs_json: [],
+        runtime_policy_json: {
+          retry_policy: {
+            max_attempts: parseOptionalInteger(retryAttempts) ?? 2,
+            backoff_ms: parseOptionalInteger(retryBackoffMs) ?? 800,
+          },
+          tool_call_limit: parseOptionalInteger(toolCallLimit) ?? 8,
+          human_approval_policy: humanApprovalPolicy,
+        },
+        tool_refs_json: [],
+        skill_refs_json: selectedSkillIds.map((skillId) => ({ skill_id: skillId })),
         status: "draft",
       });
 
@@ -150,122 +158,93 @@ export function CreateAgentDialog({ onCreated }: { onCreated: () => void }) {
       <Button onClick={() => setOpen(true)}>
         <Plus className="h-4 w-4" /> {t("agents.newAgent")}
       </Button>
-      <Dialog open={open} onOpenChange={(value) => { if (!value) reset(); setOpen(value); }} title={t("agents.create")} className="max-w-2xl">
-        <form className="space-y-6" onSubmit={submit}>
-          {/* Section: Basic Info */}
-          <section className="space-y-4">
-            <SectionHeader icon={<Bot className="h-4 w-4" />} title={t("agents.basicInfo")} description={t("agents.basicInfoDesc")} />
-            <div className="grid gap-4 sm:grid-cols-2">
-              <Field label={t("common.name")}>
-                <Input required value={form.name} onChange={(e) => updateName(e.target.value)} placeholder={t("agents.namePlaceholder")} />
-              </Field>
-              <Field label={t("common.code")}>
-                <Input
-                  required
-                  value={form.code}
-                  onChange={(e) => { setCodeTouched(true); setForm({ ...form, code: e.target.value }); }}
-                  placeholder={t("agents.codePlaceholder")}
-                />
-              </Field>
-            </div>
-            <Field label={t("common.type")}>
-              <Select value={form.agent_type} onChange={(e) => setForm({ ...form, agent_type: e.target.value })} options={agentTypes} />
-            </Field>
-            <Field label={t("common.description")}>
-              <Textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder={t("agents.descriptionPlaceholder")} />
-            </Field>
-            <Field label={t("agents.goal")}>
-              <Input value={goal} onChange={(e) => setGoal(e.target.value)} placeholder={t("agents.goalPlaceholder")} />
-            </Field>
-          </section>
-
-          {/* Section: System Prompt */}
-          <section className="space-y-4">
-            <SectionHeader icon={<TerminalSquare className="h-4 w-4" />} title={t("agents.systemPrompt")} />
-            <Textarea
-              className="min-h-32"
-              value={systemPrompt}
-              onChange={(e) => setSystemPrompt(e.target.value)}
-              placeholder={t("agents.systemPromptPlaceholder")}
-            />
-          </section>
+      <Dialog open={open} onOpenChange={(value) => { if (!value) reset(); setOpen(value); }} title={t("agents.create")} className="max-w-5xl">
+        <form className="space-y-5" onSubmit={submit}>
+          <div className="grid gap-5 lg:grid-cols-2">
+            <section className="space-y-4">
+              <div className="rounded-lg border border-border bg-muted/15 p-4">
+                <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.basicAgent")}</p>
+                <div className="mt-4 space-y-4">
+                  <Field label={t("common.name")} required>
+                    <Input required value={name} onChange={(e) => setName(e.target.value)} placeholder={t("agents.namePlaceholder")} />
+                  </Field>
+                  <Field label={t("agents.systemPrompt")}>
+                    <Textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} placeholder={t("agents.systemPromptPlaceholder")} rows={8} />
+                  </Field>
+                </div>
+              </div>
 
-          {/* Section: Model Settings */}
-          <section className="space-y-4">
-            <SectionHeader icon={<Settings2 className="h-4 w-4" />} title={t("agents.modelSettings")} description={t("agents.modelSettingsDesc")} />
-            <div className="grid gap-4 sm:grid-cols-2">
-              <Field label={t("agents.provider")}>
-                <Select
-                  value={selectedProviderId}
-                  onChange={(e) => {
-                    setSelectedProviderId(e.target.value);
-                    const provider = modelProviders.find((p) => p.id === e.target.value);
-                    const chatModels = provider?.models.filter((m) => m.enabled && (m.model_type === "chat" || m.model_type === "reasoning")) ?? [];
-                    setSelectedModel(chatModels[0]?.model_id ?? "");
-                  }}
-                  options={modelProviders.map((p) => ({ value: p.id, label: p.name }))}
-                />
-              </Field>
-              <Field label={t("agents.model")}>
-                <Select value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} options={modelOptions} />
-              </Field>
-            </div>
-            <div className="grid gap-4 sm:grid-cols-2">
-              <Field label={`${t("agents.temperature")} (${temperature})`}>
-                <input
-                  type="range"
-                  min="0"
-                  max="2"
-                  step="0.1"
-                  value={temperature}
-                  onChange={(e) => setTemperature(e.target.value)}
-                  className="w-full accent-primary"
-                />
-              </Field>
-              <Field label={t("agents.maxTokens")}>
-                <Input type="number" min={1} max={128000} value={maxTokens} onChange={(e) => setMaxTokens(e.target.value)} />
-              </Field>
-            </div>
-          </section>
+              <div className="rounded-lg border border-border bg-muted/10 p-4">
+                <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.skills")}</p>
 
-          {/* Section: Tools */}
-          {availableTools.length > 0 ? (
-            <section className="space-y-4">
-              <SectionHeader icon={<Wrench className="h-4 w-4" />} title={t("agents.toolsSection")} description={t("agents.toolsSectionDesc")} />
-              <div className="flex flex-wrap gap-2">
-                {availableTools.map((tool) => (
-                  <button
-                    key={tool.id}
-                    type="button"
-                    onClick={() => toggleTool(tool.code)}
-                    className={`rounded-md border px-3 py-1.5 text-sm transition ${
-                      selectedToolCodes.includes(tool.code)
-                        ? "border-primary bg-primary/15 text-primary"
-                        : "border-border bg-muted/30 text-muted-foreground hover:bg-muted/60"
-                    }`}
-                  >
-                    {tool.name}
-                  </button>
-                ))}
+                <div className="mt-4">
+                  <div>
+                    {skillsLoading ? (
+                      <div className="rounded-md border border-dashed border-border bg-muted/20 p-3 text-sm text-muted-foreground">
+                        {t("agents.loadingSkills")}
+                      </div>
+                    ) : skillsError ? (
+                      <div className="rounded-md border border-dashed border-red-500/30 bg-red-500/5 p-3 text-sm text-red-500">
+                        {skillsError}
+                      </div>
+                    ) : availableSkills.length > 0 ? (
+                      <div className="flex max-h-44 flex-wrap gap-2 overflow-auto pr-1">
+                        {availableSkills.map((skill) => (
+                          <button
+                            key={skill.id}
+                            type="button"
+                            onClick={() => toggleSkill(skill.id)}
+                            className={`flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition ${
+                              selectedSkillIds.includes(skill.id)
+                                ? "border-primary bg-primary/15 text-primary"
+                                : "border-border bg-muted/30 text-muted-foreground hover:bg-muted/60"
+                            }`}
+                          >
+                            <Sparkles className="h-3 w-3" />
+                            {skill.name}
+                          </button>
+                        ))}
+                      </div>
+                    ) : (
+                      <div className="rounded-md border border-dashed border-border bg-muted/20 p-3 text-sm text-muted-foreground">
+                        {t("agents.noSkillsYet")}
+                      </div>
+                    )}
+                  </div>
+                </div>
               </div>
+
             </section>
-          ) : null}
 
-          {/* Section: Memory */}
-          <section className="space-y-4">
-            <SectionHeader icon={<Brain className="h-4 w-4" />} title={t("agents.memorySection")} description={t("agents.memorySectionDesc")} />
-            <div className="grid gap-4 sm:grid-cols-2">
-              <Field label={t("agents.memoryEnabled")}>
-                <Select
-                  value={memoryEnabled ? "true" : "false"}
-                  onChange={(e) => setMemoryEnabled(e.target.value === "true")}
-                  options={[
-                    { value: "true", label: t("common.yes") },
-                    { value: "false", label: t("common.no") },
-                  ]}
-                />
-              </Field>
-              {memoryEnabled ? (
+            <aside className="space-y-4 rounded-lg border border-border bg-surface p-4 lg:sticky lg:top-24 lg:max-h-[calc(100dvh-12rem)] lg:overflow-auto">
+              <div>
+                <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.modelAndMemory")}</p>
+                <p className="mt-1 text-sm text-muted-foreground">{t("agents.modelAndMemoryHint")}</p>
+              </div>
+
+              <div className="space-y-4">
+                <Field label={t("agents.model")}>
+                  <Select value={selectedModel} onChange={(e) => selectModel(e.target.value)} options={modelOptions} />
+                </Field>
+                <div className="grid gap-3 sm:grid-cols-2">
+                  <Field label={t("agents.temperature")}>
+                    <Input value={temperature} onChange={(event) => setTemperature(event.target.value)} inputMode="decimal" />
+                  </Field>
+                  <Field label={t("agents.maxTokens")}>
+                    <Input value={maxTokens} onChange={(event) => setMaxTokens(event.target.value)} inputMode="numeric" />
+                  </Field>
+                  <Field label={t("agents.timeoutSeconds")}>
+                    <Input value={timeoutSeconds} onChange={(event) => setTimeoutSeconds(event.target.value)} inputMode="numeric" />
+                  </Field>
+                  <Field label={t("agents.contextWindow")}>
+                    <Input value={contextWindow} onChange={(event) => setContextWindow(event.target.value)} inputMode="numeric" placeholder={t("agents.auto")} />
+                  </Field>
+                </div>
+              </div>
+
+              <div className="h-px bg-border" />
+
+              <div className="space-y-4">
                 <Field label={t("agents.memoryScope")}>
                   <Select
                     value={memoryScope}
@@ -273,19 +252,56 @@ export function CreateAgentDialog({ onCreated }: { onCreated: () => void }) {
                     options={[
                       { value: "session", label: t("agents.memoryScopeSession") },
                       { value: "persistent", label: t("agents.memoryScopePersistent") },
-                      { value: "none", label: t("agents.memoryScopeNone") },
                     ]}
                   />
                 </Field>
-              ) : null}
+              </div>
+            </aside>
+          </div>
+
+          <div className="rounded-lg border border-border bg-muted/10 p-4">
+            <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">{t("agents.executionPolicy")}</p>
+            <div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
+              <Field label={t("agents.retryAttempts")}>
+                <Input value={retryAttempts} onChange={(event) => setRetryAttempts(event.target.value)} inputMode="numeric" />
+              </Field>
+              <Field label={t("agents.retryBackoffMs")}>
+                <Input value={retryBackoffMs} onChange={(event) => setRetryBackoffMs(event.target.value)} inputMode="numeric" />
+              </Field>
+              <Field label={t("agents.toolCallLimit")}>
+                <Input value={toolCallLimit} onChange={(event) => setToolCallLimit(event.target.value)} inputMode="numeric" />
+              </Field>
+              <Field label={t("agents.outputFormat")}>
+                <Select
+                  value={outputFormat}
+                  onChange={(event) => setOutputFormat(event.target.value)}
+                  options={[
+                    { value: "text", label: t("agents.outputText") },
+                    { value: "json", label: "JSON" },
+                    { value: "markdown", label: "Markdown" },
+                  ]}
+                />
+              </Field>
+              <Field label={t("agents.humanApproval")}>
+                <Select
+                  value={humanApprovalPolicy}
+                  onChange={(event) => setHumanApprovalPolicy(event.target.value)}
+                  options={[
+                    { value: "never", label: t("agents.approvalNever") },
+                    { value: "sensitive_actions", label: t("agents.approvalSensitiveActions") },
+                    { value: "before_final", label: t("agents.approvalBeforeFinal") },
+                    { value: "always", label: t("agents.approvalAlways") },
+                  ]}
+                />
+              </Field>
             </div>
-          </section>
+          </div>
 
-          <div className="flex flex-wrap justify-end gap-2">
+          <div className="flex justify-end gap-2 border-t border-border pt-4">
             <Button type="button" variant="ghost" onClick={() => { reset(); setOpen(false); }}>
               {t("common.cancel")}
             </Button>
-            <Button disabled={submitting || !form.name.trim() || !form.code.trim()}>
+            <Button disabled={submitting || !name.trim()}>
               {submitting ? t("common.creating") : t("agents.create")}
             </Button>
           </div>
@@ -295,25 +311,27 @@ export function CreateAgentDialog({ onCreated }: { onCreated: () => void }) {
   );
 }
 
-function Field({ label, children }: { label: string; children: React.ReactNode }) {
+function Field({ label, children, required }: { label: string; children: React.ReactNode; required?: boolean }) {
   return (
     <label className="block space-y-2 text-sm">
-      <span className="text-muted-foreground">{label}</span>
+      <span className="text-muted-foreground">
+        {label}
+        {required && <span className="text-red-500 ml-0.5">*</span>}
+      </span>
       {children}
     </label>
   );
 }
 
-function SectionHeader({ icon, title, description }: { icon: React.ReactNode; title: string; description?: string }) {
-  return (
-    <div className="flex items-start gap-3 rounded-md border border-border bg-muted/30 p-3">
-      <div className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-primary/15 text-primary">
-        {icon}
-      </div>
-      <div>
-        <h3 className="text-sm font-semibold">{title}</h3>
-        {description ? <p className="mt-0.5 text-xs leading-5 text-muted-foreground">{description}</p> : null}
-      </div>
-    </div>
-  );
+function parseOptionalInteger(value: string) {
+  if (!value.trim()) return null;
+  const parsed = Number.parseInt(value, 10);
+  return Number.isFinite(parsed) ? parsed : null;
+}
+
+function parseOptionalFloat(value: string) {
+  if (!value.trim()) return null;
+  const parsed = Number.parseFloat(value);
+  return Number.isFinite(parsed) ? parsed : null;
 }
+

+ 142 - 5
web/src/pages/dashboard/DashboardPage.tsx

@@ -1,26 +1,32 @@
 import * as React from "react";
 import { useTranslation } from "react-i18next";
+import { RefreshCw } from "lucide-react";
 import { ApiErrorState } from "@/components/shared/ApiErrorState";
 import { PageHeader } from "@/components/shared/PageHeader";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
+import { Button } from "@/components/ui/button";
 import { useInterval } from "@/hooks";
 import { getServicesHealth, listAgents, listRuns, listSessions } from "@/api";
 import { StatsCards } from "./components/StatsCards";
 import { ExecutionTrendChart } from "./components/ExecutionTrendChart";
 import { RecentRunsTable } from "./components/RecentRunsTable";
 import { ServiceHealthList } from "./components/ServiceHealthList";
+import { RunMixPanel } from "./components/RunMixPanel";
+import { ActivityFeed } from "./components/ActivityFeed";
 import type { AgentDefinition, DownstreamServiceHealth, Session, WorkflowRun } from "@/types";
 
 export function DashboardPage() {
   const { t } = useTranslation();
   const [loading, setLoading] = React.useState(true);
+  const [refreshing, setRefreshing] = React.useState(false);
   const [agents, setAgents] = React.useState<AgentDefinition[]>([]);
   const [sessions, setSessions] = React.useState<Session[]>([]);
   const [runs, setRuns] = React.useState<WorkflowRun[]>([]);
   const [services, setServices] = React.useState<DownstreamServiceHealth[]>([]);
   const [error, setError] = React.useState<string>();
 
-  const load = React.useCallback(async () => {
+  const load = React.useCallback(async (showRefreshing = false) => {
+    if (showRefreshing) setRefreshing(true);
     setError(undefined);
     const [agentData, sessionData, runData, healthData] = await Promise.allSettled([
       listAgents(),
@@ -36,6 +42,7 @@ export function DashboardPage() {
       setError(t("dashboard.allRequestsFailed"));
     }
     setLoading(false);
+    if (showRefreshing) setRefreshing(false);
   }, [t]);
 
   React.useEffect(() => {
@@ -46,12 +53,36 @@ export function DashboardPage() {
   const today = new Date().toDateString();
   const runsToday = runs.filter((run) => new Date(run.created_time).toDateString() === today).length;
   const activeSessions = sessions.filter((session) => session.session_status === "active").length;
+  const activeAgents = agents.filter((agent) => agent.status === "active").length;
+  const completedRuns = runs.filter((run) => run.status === "completed").length;
+  const failedRuns = runs.filter((run) => run.status === "failed").length;
+  const liveRuns = runs.filter((run) => ["queued", "running", "pending"].includes(run.status)).length;
+  const successRate = completedRuns + failedRuns > 0 ? Math.round((completedRuns / (completedRuns + failedRuns)) * 100) : 0;
+  const healthyServices = services.filter((service) => service.status === "ok").length;
+  const totalNodes = runs.reduce((sum, run) => sum + run.current_node_count, 0);
+  const averageNodes = runs.length > 0 ? Math.round((totalNodes / runs.length) * 10) / 10 : 0;
+  const latestRun = [...runs].sort(
+    (a, b) => new Date(b.started_time ?? b.created_time).getTime() - new Date(a.started_time ?? a.created_time).getTime()
+  )[0];
+  const statusBreakdown = [
+    { label: t("common.completed"), value: completedRuns, tone: "success" as const },
+    { label: t("common.running"), value: liveRuns, tone: "primary" as const },
+    { label: t("common.failed"), value: failedRuns, tone: "destructive" as const },
+  ];
+  const runTypeBreakdown = Object.entries(
+    runs.reduce<Record<string, number>>((acc, run) => {
+      acc[run.run_type] = (acc[run.run_type] ?? 0) + 1;
+      return acc;
+    }, {})
+  )
+    .map(([label, value]) => ({ label: t(`dashboard.runTypes.${label}`, humanizeCode(label)), value }))
+    .sort((a, b) => b.value - a.value);
   const trend = Array.from({ length: 7 }, (_, index) => {
     const day = new Date();
     day.setDate(day.getDate() - (6 - index));
     const dayRuns = runs.filter((run) => new Date(run.created_time).toDateString() === day.toDateString());
     return {
-      label: day.toLocaleDateString(undefined, { weekday: "short" }),
+      label: `${String(day.getMonth() + 1).padStart(2, "0")}-${String(day.getDate()).padStart(2, "0")}`,
       successful: dayRuns.filter((run) => run.status === "completed").length,
       failed: dayRuns.filter((run) => run.status === "failed").length,
     };
@@ -62,13 +93,119 @@ export function DashboardPage() {
 
   return (
     <div className="space-y-6">
-      <PageHeader title={t("dashboard.title")} description={t("dashboard.description")} />
-      <StatsCards agents={agents.length} runsToday={runsToday} activeSessions={activeSessions} />
-      <div className="grid gap-6 xl:grid-cols-[1fr_360px]">
+      <PageHeader
+        title={t("dashboard.title")}
+        description={t("dashboard.description")}
+        actions={
+          <Button variant="outline" onClick={() => void load(true)} disabled={refreshing}>
+            <RefreshCw className={refreshing ? "h-4 w-4 motion-safe:animate-spin" : "h-4 w-4"} />
+            {t("common.refresh")}
+          </Button>
+        }
+      />
+
+      <section className="overflow-hidden rounded-md border border-border bg-surface-elevated shadow-glow">
+        <div className="grid gap-0 lg:grid-cols-[minmax(0,1fr)_380px]">
+          <div className="relative min-h-[240px] border-b border-border p-5 sm:p-6 lg:border-b-0 lg:border-r">
+            <div className="relative flex h-full flex-col justify-between gap-8">
+              <div className="max-w-2xl">
+                <div className="mb-4 inline-flex items-center gap-2 rounded-md border border-primary/20 bg-primary/10 px-3 py-2 text-xs font-medium text-primary">
+                  <span className="h-2 w-2 rounded-full bg-emerald-400 shadow-[0_0_12px_rgba(16,185,129,0.85)]" />
+                  {t("dashboard.liveWorkspace", "Live workspace")}
+                </div>
+                <h2 className="text-2xl font-semibold tracking-normal sm:text-3xl">
+                  {t("dashboard.commandCenter", "Agent operations command center")}
+                </h2>
+                <p className="mt-3 max-w-xl text-sm leading-6 text-muted-foreground">
+                  {t(
+                    "dashboard.commandCenterDescription",
+                    "Track execution health, workflow throughput, and service readiness from one operational surface."
+                  )}
+                </p>
+              </div>
+              <div className="grid gap-3 sm:grid-cols-3">
+                {statusBreakdown.map((item) => (
+                  <div key={item.label} className="rounded-md border border-border bg-surface-elevated/80 p-4">
+                    <p className="text-xs font-medium text-muted-foreground">{item.label}</p>
+                    <p className="mt-2 text-2xl font-semibold tabular-nums">{item.value}</p>
+                    <div
+                      className={
+                        item.tone === "success"
+                          ? "mt-3 h-1.5 rounded-full bg-emerald-500"
+                          : item.tone === "destructive"
+                            ? "mt-3 h-1.5 rounded-full bg-red-500"
+                            : "mt-3 h-1.5 rounded-full bg-primary"
+                      }
+                    />
+                  </div>
+                ))}
+              </div>
+            </div>
+          </div>
+
+          <div className="grid content-between gap-4 p-5 sm:p-6">
+            <div>
+              <p className="text-xs font-medium uppercase text-muted-foreground">
+                {t("dashboard.readiness", "Readiness")}
+              </p>
+              <div className="mt-4 grid grid-cols-2 gap-3">
+                <div className="rounded-md border border-border bg-muted/30 p-4">
+                  <p className="text-xs text-muted-foreground">{t("dashboard.successRate", "Success rate")}</p>
+                  <p className="mt-2 text-2xl font-semibold tabular-nums">{successRate}%</p>
+                </div>
+                <div className="rounded-md border border-border bg-muted/30 p-4">
+                  <p className="text-xs text-muted-foreground">{t("dashboard.serviceUptime", "Services ok")}</p>
+                  <p className="mt-2 text-2xl font-semibold tabular-nums">
+                    {healthyServices}/{services.length || 0}
+                  </p>
+                </div>
+              </div>
+            </div>
+            <div className="rounded-md border border-border bg-muted/30 p-4">
+              <p className="text-xs text-muted-foreground">{t("dashboard.latestRun", "Latest run")}</p>
+              <div className="mt-3 flex items-center justify-between gap-3">
+                <div className="min-w-0">
+                  <p className="truncate font-mono text-sm">{latestRun?.id ?? t("common.noData")}</p>
+                  <p className="mt-1 text-xs text-muted-foreground">
+                    {latestRun ? t(`dashboard.runTypes.${latestRun.run_type}`, humanizeCode(latestRun.run_type)) : "-"}
+                  </p>
+                </div>
+                <div className="text-right text-sm font-semibold tabular-nums">
+                  {latestRun ? latestRun.current_node_count : 0}
+                  <span className="ml-1 text-xs font-normal text-muted-foreground">{t("dashboard.nodes", "Nodes")}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </section>
+
+      <StatsCards
+        agents={agents.length}
+        activeAgents={activeAgents}
+        runsToday={runsToday}
+        activeSessions={activeSessions}
+        successRate={successRate}
+        averageNodes={averageNodes}
+      />
+
+      <div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_380px]">
         <ExecutionTrendChart data={trend} />
+        <ActivityFeed runs={runs} />
+      </div>
+
+      <div className="grid gap-6 xl:grid-cols-[minmax(0,0.92fr)_minmax(360px,0.58fr)]">
+        <RunMixPanel runTypes={runTypeBreakdown} totalRuns={runs.length} liveRuns={liveRuns} failedRuns={failedRuns} />
         <ServiceHealthList services={services} />
       </div>
+
       <RecentRunsTable runs={runs} />
     </div>
   );
 }
+
+function humanizeCode(value: string) {
+  return value
+    .replace(/_/g, " ")
+    .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}

+ 64 - 0
web/src/pages/dashboard/components/ActivityFeed.tsx

@@ -0,0 +1,64 @@
+import { useTranslation } from "react-i18next";
+import { Clock, GitBranch, Radio } from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { RelativeTime } from "@/components/shared/RelativeTime";
+import { StatusBadge } from "@/components/shared/StatusBadge";
+import { truncateMiddle } from "@/lib/utils";
+import type { WorkflowRun } from "@/types";
+
+export function ActivityFeed({ runs }: { runs: WorkflowRun[] }) {
+  const { t } = useTranslation();
+  const sortedRuns = [...runs]
+    .sort((a, b) => new Date(b.started_time ?? b.created_time).getTime() - new Date(a.started_time ?? a.created_time).getTime())
+    .slice(0, 5);
+
+  return (
+    <Card className="min-h-80">
+      <CardHeader className="flex flex-row items-start justify-between gap-4">
+        <div>
+          <CardTitle>{t("dashboard.activityFeed", "Activity feed")}</CardTitle>
+          <CardDescription>{t("dashboard.activityFeedDescription", "Newest workflow runs and their current execution state.")}</CardDescription>
+        </div>
+        <div className="grid h-10 w-10 place-items-center rounded-md bg-primary/10 text-primary">
+          <Radio className="h-5 w-5" />
+        </div>
+      </CardHeader>
+      <CardContent>
+        {sortedRuns.length === 0 ? (
+          <div className="grid min-h-40 place-items-center rounded-md border border-dashed border-border bg-muted/20 text-sm text-muted-foreground">
+            {t("dashboard.noRecentActivity", "No recent run activity")}
+          </div>
+        ) : (
+          <div className="space-y-3">
+            {sortedRuns.map((run) => (
+              <div key={run.id} className="rounded-md border border-border bg-muted/20 p-3">
+                <div className="flex items-start justify-between gap-3">
+                  <div className="min-w-0">
+                    <p className="truncate font-mono text-sm">{truncateMiddle(run.id, 22)}</p>
+                    <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
+                      <span className="inline-flex items-center gap-1">
+                        <GitBranch className="h-3.5 w-3.5" />
+                        {t(`dashboard.runTypes.${run.run_type}`, humanizeCode(run.run_type))}
+                      </span>
+                      <span className="inline-flex items-center gap-1">
+                        <Clock className="h-3.5 w-3.5" />
+                        <RelativeTime value={run.started_time ?? run.created_time} />
+                      </span>
+                    </div>
+                  </div>
+                  <StatusBadge status={run.status} />
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+      </CardContent>
+    </Card>
+  );
+}
+
+function humanizeCode(value: string) {
+  return value
+    .replace(/_/g, " ")
+    .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}

+ 22 - 18
web/src/pages/dashboard/components/ExecutionTrendChart.tsx

@@ -1,39 +1,43 @@
 import { useTranslation } from "react-i18next";
 import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 
 export function ExecutionTrendChart({ data }: { data: Array<{ label: string; successful: number; failed: number }> }) {
   const { t } = useTranslation();
   return (
     <Card className="min-h-80">
-      <CardHeader>
-        <CardTitle>{t("dashboard.executionTrend")}</CardTitle>
+      <CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
+        <div>
+          <CardTitle>{t("dashboard.executionTrend")}</CardTitle>
+          <CardDescription>{t("dashboard.executionTrendDescription", "Seven-day completed and failed workflow volume.")}</CardDescription>
+        </div>
+        <div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
+          <span className="inline-flex items-center gap-2">
+            <span className="h-2 w-2 rounded-full bg-emerald-500" />
+            {t("common.completed")}
+          </span>
+          <span className="inline-flex items-center gap-2">
+            <span className="h-2 w-2 rounded-full bg-red-500" />
+            {t("common.failed")}
+          </span>
+        </div>
       </CardHeader>
       <CardContent className="h-64">
         <ResponsiveContainer width="100%" height="100%">
-          <AreaChart data={data}>
-            <defs>
-              <linearGradient id="success" x1="0" x2="0" y1="0" y2="1">
-                <stop offset="5%" stopColor="#10B981" stopOpacity={0.45} />
-                <stop offset="95%" stopColor="#10B981" stopOpacity={0} />
-              </linearGradient>
-              <linearGradient id="failed" x1="0" x2="0" y1="0" y2="1">
-                <stop offset="5%" stopColor="#DC2626" stopOpacity={0.35} />
-                <stop offset="95%" stopColor="#DC2626" stopOpacity={0} />
-              </linearGradient>
-            </defs>
-            <CartesianGrid stroke="rgba(255,255,255,0.08)" vertical={false} />
+          <AreaChart data={data} margin={{ left: -18, right: 8, top: 8, bottom: 0 }}>
+            <CartesianGrid stroke="hsl(var(--border))" vertical={false} />
             <XAxis dataKey="label" stroke="#8A8F98" fontSize={12} tickLine={false} axisLine={false} />
-            <YAxis stroke="#8A8F98" fontSize={12} tickLine={false} axisLine={false} />
+            <YAxis allowDecimals={false} stroke="#8A8F98" fontSize={12} tickLine={false} axisLine={false} />
             <Tooltip
               contentStyle={{
                 background: "hsl(var(--surface-elevated))",
                 border: "1px solid hsl(var(--border))",
                 color: "hsl(var(--foreground))",
+                borderRadius: 8,
               }}
             />
-            <Area type="monotone" dataKey="successful" stroke="#10B981" fill="url(#success)" />
-            <Area type="monotone" dataKey="failed" stroke="#DC2626" fill="url(#failed)" />
+            <Area type="monotone" dataKey="successful" stroke="#10B981" strokeWidth={2} fill="#10B981" fillOpacity={0.12} />
+            <Area type="monotone" dataKey="failed" stroke="#DC2626" strokeWidth={2} fill="#DC2626" fillOpacity={0.1} />
           </AreaChart>
         </ResponsiveContainer>
       </CardContent>

+ 62 - 30
web/src/pages/dashboard/components/RecentRunsTable.tsx

@@ -1,47 +1,79 @@
 import { useTranslation } from "react-i18next";
+import { ArrowUpRight, History } from "lucide-react";
 import { StatusBadge } from "@/components/shared/StatusBadge";
 import { RelativeTime } from "@/components/shared/RelativeTime";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { truncateMiddle } from "@/lib/utils";
 import type { WorkflowRun } from "@/types";
 
 export function RecentRunsTable({ runs }: { runs: WorkflowRun[] }) {
   const { t } = useTranslation();
+  const sortedRuns = [...runs]
+    .sort((a, b) => new Date(b.started_time ?? b.created_time).getTime() - new Date(a.started_time ?? a.created_time).getTime())
+    .slice(0, 10);
+
   return (
     <Card>
-      <CardHeader>
-        <CardTitle>{t("dashboard.recentRuns")}</CardTitle>
+      <CardHeader className="flex flex-row items-start justify-between gap-4">
+        <div>
+          <CardTitle>{t("dashboard.recentRuns")}</CardTitle>
+          <CardDescription>{t("dashboard.recentRunsDescription", "Latest runtime records with trigger, priority, and workflow depth.")}</CardDescription>
+        </div>
+        <div className="grid h-10 w-10 place-items-center rounded-md bg-primary/10 text-primary">
+          <History className="h-5 w-5" />
+        </div>
       </CardHeader>
       <CardContent>
-        <div className="overflow-x-auto">
-          <table className="w-full text-left text-sm">
-            <thead className="text-xs text-muted-foreground">
-              <tr className="border-b border-border">
-                <th className="py-2">{t("agents.runId", "Run ID")}</th>
-                <th>{t("common.type", "Type")}</th>
-                <th>{t("common.status", "Status")}</th>
-                <th>{t("agents.started", "Started")}</th>
-                <th>{t("dashboard.nodes", "Nodes")}</th>
-              </tr>
-            </thead>
-            <tbody>
-              {runs.slice(0, 8).map((run) => (
-                <tr key={run.id} className="border-b border-border">
-                  <td className="py-3 font-mono text-xs">{truncateMiddle(run.id, 20)}</td>
-                  <td>{run.run_type}</td>
-                  <td>
-                    <StatusBadge status={run.status} />
-                  </td>
-                  <td className="text-muted-foreground">
-                    <RelativeTime value={run.started_time ?? run.created_time} />
-                  </td>
-                  <td>{run.current_node_count}</td>
+        {sortedRuns.length === 0 ? (
+          <div className="grid min-h-40 place-items-center rounded-md border border-dashed border-border bg-muted/20 text-sm text-muted-foreground">
+            {t("dashboard.noRuns", "No workflow runs yet")}
+          </div>
+        ) : (
+          <div className="overflow-x-auto">
+            <table className="w-full min-w-[760px] text-left text-sm">
+              <thead className="text-xs text-muted-foreground">
+                <tr className="border-b border-border">
+                  <th className="py-3 font-medium">{t("agents.runId", "Run ID")}</th>
+                  <th className="font-medium">{t("common.type", "Type")}</th>
+                  <th className="font-medium">{t("dashboard.trigger", "Trigger")}</th>
+                  <th className="font-medium">{t("common.status", "Status")}</th>
+                  <th className="font-medium">{t("agents.started", "Started")}</th>
+                  <th className="text-right font-medium">{t("dashboard.priority", "Priority")}</th>
+                  <th className="text-right font-medium">{t("dashboard.nodes", "Nodes")}</th>
                 </tr>
-              ))}
-            </tbody>
-          </table>
-        </div>
+              </thead>
+              <tbody>
+                {sortedRuns.map((run) => (
+                  <tr key={run.id} className="border-b border-border transition-colors hover:bg-muted/30">
+                    <td className="py-4">
+                      <div className="flex items-center gap-2">
+                        <span className="font-mono text-xs">{truncateMiddle(run.id, 22)}</span>
+                        <ArrowUpRight className="h-3.5 w-3.5 text-muted-foreground" />
+                      </div>
+                    </td>
+                    <td>{t(`dashboard.runTypes.${run.run_type}`, humanizeCode(run.run_type))}</td>
+                    <td className="text-muted-foreground">{t(`dashboard.triggers.${run.trigger_type}`, humanizeCode(run.trigger_type))}</td>
+                    <td>
+                      <StatusBadge status={run.status} />
+                    </td>
+                    <td className="text-muted-foreground">
+                      <RelativeTime value={run.started_time ?? run.created_time} />
+                    </td>
+                    <td className="text-right tabular-nums">{run.priority}</td>
+                    <td className="text-right tabular-nums">{run.current_node_count}</td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          </div>
+        )}
       </CardContent>
     </Card>
   );
 }
+
+function humanizeCode(value: string) {
+  return value
+    .replace(/_/g, " ")
+    .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}

+ 77 - 0
web/src/pages/dashboard/components/RunMixPanel.tsx

@@ -0,0 +1,77 @@
+import { useTranslation } from "react-i18next";
+import { AlertTriangle, Layers3, PlayCircle } from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+
+export function RunMixPanel({
+  runTypes,
+  totalRuns,
+  liveRuns,
+  failedRuns,
+}: {
+  runTypes: Array<{ label: string; value: number }>;
+  totalRuns: number;
+  liveRuns: number;
+  failedRuns: number;
+}) {
+  const { t } = useTranslation();
+  const maxValue = Math.max(...runTypes.map((item) => item.value), 1);
+
+  return (
+    <Card>
+      <CardHeader className="flex flex-row items-start justify-between gap-4">
+        <div>
+          <CardTitle>{t("dashboard.runMix", "Run mix")}</CardTitle>
+          <CardDescription>{t("dashboard.runMixDescription", "Workflow composition, live load, and failure pressure.")}</CardDescription>
+        </div>
+        <div className="grid h-10 w-10 place-items-center rounded-md bg-violet-500/10 text-violet-700 dark:text-violet-300">
+          <Layers3 className="h-5 w-5" />
+        </div>
+      </CardHeader>
+      <CardContent className="space-y-5">
+        <div className="grid gap-3 sm:grid-cols-3">
+          <div className="rounded-md border border-border bg-muted/25 p-4">
+            <p className="text-xs text-muted-foreground">{t("dashboard.totalRuns", "Total runs")}</p>
+            <p className="mt-2 text-2xl font-semibold tabular-nums">{totalRuns}</p>
+          </div>
+          <div className="rounded-md border border-border bg-muted/25 p-4">
+            <p className="flex items-center gap-1 text-xs text-muted-foreground">
+              <PlayCircle className="h-3.5 w-3.5" />
+              {t("dashboard.liveRuns", "Live runs")}
+            </p>
+            <p className="mt-2 text-2xl font-semibold tabular-nums">{liveRuns}</p>
+          </div>
+          <div className="rounded-md border border-border bg-muted/25 p-4">
+            <p className="flex items-center gap-1 text-xs text-muted-foreground">
+              <AlertTriangle className="h-3.5 w-3.5" />
+              {t("dashboard.failedRuns", "Failed runs")}
+            </p>
+            <p className="mt-2 text-2xl font-semibold tabular-nums">{failedRuns}</p>
+          </div>
+        </div>
+
+        {runTypes.length === 0 ? (
+          <div className="grid min-h-32 place-items-center rounded-md border border-dashed border-border bg-muted/20 text-sm text-muted-foreground">
+            {t("dashboard.noRunMix", "No run types yet")}
+          </div>
+        ) : (
+          <div className="space-y-4">
+            {runTypes.map((item) => {
+              const width = `${Math.max(8, Math.round((item.value / maxValue) * 100))}%`;
+              return (
+                <div key={item.label}>
+                  <div className="mb-2 flex items-center justify-between gap-3 text-sm">
+                    <span className="truncate font-medium">{item.label}</span>
+                    <span className="tabular-nums text-muted-foreground">{item.value}</span>
+                  </div>
+                  <div className="h-2 rounded-full bg-muted">
+                    <div className="h-2 rounded-full bg-primary" style={{ width }} />
+                  </div>
+                </div>
+              );
+            })}
+          </div>
+        )}
+      </CardContent>
+    </Card>
+  );
+}

+ 56 - 13
web/src/pages/dashboard/components/ServiceHealthList.tsx

@@ -1,24 +1,67 @@
 import { useTranslation } from "react-i18next";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { CheckCircle2, Server, XCircle } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import type { DownstreamServiceHealth } from "@/types";
 
 export function ServiceHealthList({ services }: { services: DownstreamServiceHealth[] }) {
   const { t } = useTranslation();
+  const healthy = services.filter((service) => service.status === "ok").length;
+  const healthPercent = services.length > 0 ? Math.round((healthy / services.length) * 100) : 0;
+
   return (
-    <Card>
-      <CardHeader>
-        <CardTitle>{t("dashboard.services")}</CardTitle>
+    <Card className="min-h-full">
+      <CardHeader className="flex flex-row items-start justify-between gap-4">
+        <div>
+          <CardTitle>{t("dashboard.services")}</CardTitle>
+          <CardDescription>{t("dashboard.serviceHealthDescription", "Gateway dependencies and downstream availability.")}</CardDescription>
+        </div>
+        <div className="grid h-10 w-10 place-items-center rounded-md bg-emerald-500/10 text-emerald-700 dark:text-emerald-300">
+          <Server className="h-5 w-5" />
+        </div>
       </CardHeader>
-      <CardContent className="space-y-3">
-        {services.map((service) => (
-          <div key={service.service} className="flex items-center justify-between gap-3 text-sm">
-            <div className="min-w-0">
-              <p className="truncate font-medium">{service.service}</p>
-              <p className="truncate text-xs text-muted-foreground">{service.url}</p>
-            </div>
-            <span className={service.status === "ok" ? "h-2.5 w-2.5 rounded-full bg-emerald-400" : "h-2.5 w-2.5 rounded-full bg-red-400"} />
+      <CardContent className="space-y-4">
+        <div className="rounded-md border border-border bg-muted/25 p-4">
+          <div className="flex items-center justify-between gap-4">
+            <p className="text-sm font-medium">{t("dashboard.serviceReadiness", "Service readiness")}</p>
+            <p className="text-sm font-semibold tabular-nums">{healthPercent}%</p>
+          </div>
+          <div className="mt-3 h-2 rounded-full bg-muted">
+            <div className="h-2 rounded-full bg-emerald-500" style={{ width: `${healthPercent}%` }} />
+          </div>
+          <p className="mt-2 text-xs text-muted-foreground">
+            {t("dashboard.servicesReady", "{{healthy}} of {{total}} services ready", { healthy, total: services.length })}
+          </p>
+        </div>
+
+        {services.length === 0 ? (
+          <div className="grid min-h-32 place-items-center rounded-md border border-dashed border-border bg-muted/20 text-sm text-muted-foreground">
+            {t("dashboard.noServices", "No downstream services reported")}
+          </div>
+        ) : (
+          <div className="space-y-3">
+            {services.map((service) => (
+              <div key={service.service} className="rounded-md border border-border bg-muted/20 p-3">
+                <div className="flex items-start justify-between gap-3 text-sm">
+                  <div className="min-w-0">
+                    <p className="truncate font-medium">{service.service}</p>
+                    <p className="mt-1 truncate text-xs text-muted-foreground">{service.url}</p>
+                  </div>
+                  <Badge
+                    className={
+                      service.status === "ok"
+                        ? "border-emerald-500/35 bg-emerald-500/10 text-emerald-800 dark:text-emerald-300"
+                        : "border-red-500/35 bg-red-500/10 text-red-800 dark:text-red-300"
+                    }
+                  >
+                    {service.status === "ok" ? <CheckCircle2 className="mr-1 h-3.5 w-3.5" /> : <XCircle className="mr-1 h-3.5 w-3.5" />}
+                    {t(`status.${service.status}`, service.status)}
+                  </Badge>
+                </div>
+              </div>
+            ))}
           </div>
-        ))}
+        )}
       </CardContent>
     </Card>
   );

+ 68 - 11
web/src/pages/dashboard/components/StatsCards.tsx

@@ -1,32 +1,89 @@
 import { useTranslation } from "react-i18next";
-import { Activity, Bot, MessageSquare } from "lucide-react";
+import { Activity, Bot, Gauge, MessageSquare, Network, Target } from "lucide-react";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import type { LucideIcon } from "lucide-react";
 
 export function StatsCards({
   agents,
+  activeAgents,
   runsToday,
   activeSessions,
+  successRate,
+  averageNodes,
 }: {
   agents: number;
+  activeAgents: number;
   runsToday: number;
   activeSessions: number;
+  successRate: number;
+  averageNodes: number;
 }) {
   const { t } = useTranslation();
-  const cards = [
-    { label: t("dashboard.agents"), value: agents, icon: Bot },
-    { label: t("dashboard.runsToday"), value: runsToday, icon: Activity },
-    { label: t("dashboard.activeSessions"), value: activeSessions, icon: MessageSquare },
+  const cards: Array<{
+    label: string;
+    value: string | number;
+    detail: string;
+    icon: LucideIcon;
+    accent: string;
+  }> = [
+    {
+      label: t("dashboard.agents"),
+      value: agents,
+      detail: t("dashboard.activeAgents", "{{count}} active", { count: activeAgents }),
+      icon: Bot,
+      accent: "bg-primary/10 text-primary",
+    },
+    {
+      label: t("dashboard.runsToday"),
+      value: runsToday,
+      detail: t("dashboard.executionVolume", "Execution volume today"),
+      icon: Activity,
+      accent: "bg-orange-500/10 text-orange-600 dark:text-orange-300",
+    },
+    {
+      label: t("dashboard.activeSessions"),
+      value: activeSessions,
+      detail: t("dashboard.liveConversations", "Live conversations"),
+      icon: MessageSquare,
+      accent: "bg-cyan-500/10 text-cyan-700 dark:text-cyan-300",
+    },
+    {
+      label: t("dashboard.successRate", "Success rate"),
+      value: `${successRate}%`,
+      detail: t("dashboard.completedVsFailed", "Completed vs failed runs"),
+      icon: Target,
+      accent: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
+    },
+    {
+      label: t("dashboard.avgNodes", "Avg nodes"),
+      value: averageNodes,
+      detail: t("dashboard.avgNodesDetail", "Workflow depth per run"),
+      icon: Network,
+      accent: "bg-violet-500/10 text-violet-700 dark:text-violet-300",
+    },
+    {
+      label: t("dashboard.healthScore", "Health score"),
+      value: successRate >= 90 ? "A" : successRate >= 70 ? "B" : successRate > 0 ? "C" : "-",
+      detail: t("dashboard.healthScoreDetail", "Derived from recent run quality"),
+      icon: Gauge,
+      accent: "bg-slate-500/10 text-slate-700 dark:text-slate-300",
+    },
   ];
   return (
-    <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
+    <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
       {cards.map((card) => (
-        <Card key={card.label}>
-          <CardHeader className="flex flex-row items-center justify-between pb-2">
-            <CardTitle className="text-sm text-muted-foreground">{card.label}</CardTitle>
-            <card.icon className="h-4 w-4 text-primary" />
+        <Card key={card.label} className="overflow-hidden">
+          <CardHeader className="flex flex-row items-start justify-between gap-3 pb-2">
+            <div className="min-w-0">
+              <CardTitle className="truncate text-sm text-muted-foreground">{card.label}</CardTitle>
+              <p className="mt-1 truncate text-xs text-muted-foreground">{card.detail}</p>
+            </div>
+            <div className={`grid h-10 w-10 shrink-0 place-items-center rounded-md ${card.accent}`}>
+              <card.icon className="h-5 w-5" />
+            </div>
           </CardHeader>
           <CardContent>
-            <div className="text-3xl font-semibold">{card.value}</div>
+            <div className="text-3xl font-semibold tabular-nums">{card.value}</div>
           </CardContent>
         </Card>
       ))}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 285 - 252
web/src/pages/knowledge/KnowledgePage.tsx


+ 354 - 0
web/src/pages/memories/MemoryPage.tsx

@@ -0,0 +1,354 @@
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import {
+  Brain,
+  Clock,
+  Filter,
+  RefreshCw,
+} from "lucide-react";
+import { listMemories } from "@/api";
+import { ApiErrorState } from "@/components/shared/ApiErrorState";
+import { EmptyState } from "@/components/shared/EmptyState";
+import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
+import { PageHeader } from "@/components/shared/PageHeader";
+import { SearchInput } from "@/components/shared/SearchInput";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Dialog } from "@/components/ui/dialog";
+import { formatDateTime } from "@/lib/utils";
+import type { JSONValue, MemoryItem, MemoryScopeType } from "@/types";
+
+const memoryTypes = ["fact", "preference", "summary", "task_state", "profile", "instruction"];
+
+export function MemoryPage() {
+  const { t } = useTranslation();
+  const [memories, setMemories] = React.useState<MemoryItem[]>([]);
+  const [selectedId, setSelectedId] = React.useState<string>();
+  const [searchText, setSearchText] = React.useState("");
+  const [scopeFilter, setScopeFilter] = React.useState<MemoryScopeType | "all">("all");
+  const [typeFilter, setTypeFilter] = React.useState("all");
+  const [loading, setLoading] = React.useState(true);
+  const [error, setError] = React.useState<string>();
+  const [detailOpen, setDetailOpen] = React.useState(false);
+
+  const selected = memories.find((item) => item.id === selectedId) ?? memories[0];
+  const typeOptions = Array.from(new Set([...memoryTypes, ...memories.map((item) => item.memory_type)])).sort();
+
+  const filtered = memories.filter((item) => {
+    const haystack = [
+      item.content_text,
+      item.memory_type,
+      item.scope_type,
+      item.scope_id,
+      item.owner_agent_id,
+      item.user_id,
+      item.session_id,
+      item.source_ref,
+    ].join(" ").toLowerCase();
+    return (
+      haystack.includes(searchText.toLowerCase()) &&
+      (scopeFilter === "all" || item.scope_type === scopeFilter) &&
+      (typeFilter === "all" || item.memory_type === typeFilter)
+    );
+  });
+
+  const load = React.useCallback(async () => {
+    setLoading(true);
+    setError(undefined);
+    try {
+      const results = await listMemories({ limit: 500 });
+      setMemories(results);
+      setSelectedId((current) => current ?? results[0]?.id);
+    } catch (err) {
+      setError(err instanceof Error ? err.message : t("memories.failedToLoad"));
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  React.useEffect(() => {
+    void load();
+  }, [load]);
+
+  if (loading) return <LoadingSpinner label={t("memories.loading")} />;
+  if (error) return <ApiErrorState message={error} onRetry={() => void load()} />;
+
+  return (
+    <div className="space-y-6">
+      <PageHeader
+        title={t("memories.title")}
+        description={t("memories.description")}
+        actions={
+          <>
+            <Button variant="outline" onClick={() => void load()}>
+              <RefreshCw className="h-4 w-4" /> {t("common.refresh")}
+            </Button>
+          </>
+        }
+      />
+
+      <Card>
+        <CardHeader>
+          <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
+            <div>
+              <CardTitle>{t("memories.store")}</CardTitle>
+              <CardDescription>{t("memories.shown", { shown: filtered.length, total: memories.length })}</CardDescription>
+            </div>
+          </div>
+        </CardHeader>
+        <CardContent className="space-y-4 pt-4">
+          <MemoryFilters
+            searchText={searchText}
+            scopeFilter={scopeFilter}
+            typeFilter={typeFilter}
+            typeOptions={typeOptions}
+            onSearch={setSearchText}
+            onScope={(value) => setScopeFilter(value as MemoryScopeType | "all")}
+            onType={setTypeFilter}
+          />
+
+          {filtered.length ? (
+            <div className="grid gap-3 xl:grid-cols-2">
+              {filtered.map((memory) => (
+                <MemoryListItem
+                  key={memory.id}
+                  memory={memory}
+                  selected={selected?.id === memory.id}
+                  onSelect={() => {
+                    setSelectedId(memory.id);
+                    setDetailOpen(true);
+                  }}
+                />
+              ))}
+            </div>
+          ) : (
+            <EmptyState icon={Brain} title={t("memories.noMemoriesFound")} description={t("memories.noMemoriesFoundDesc")} />
+          )}
+        </CardContent>
+      </Card>
+
+      <Dialog open={detailOpen} onOpenChange={setDetailOpen} title={t("memories.detail")} className="max-w-3xl">
+        <MemoryDetailPanel memory={selected} />
+      </Dialog>
+
+    </div>
+  );
+}
+
+function MemoryFilters({
+  searchText,
+  scopeFilter,
+  typeFilter,
+  typeOptions,
+  onSearch,
+  onScope,
+  onType,
+}: {
+  searchText: string;
+  scopeFilter: string;
+  typeFilter: string;
+  typeOptions: string[];
+  onSearch: (value: string) => void;
+  onScope: (value: string) => void;
+  onType: (value: string) => void;
+}) {
+  const { t } = useTranslation();
+  const scopeOptions: Array<{ value: MemoryScopeType; label: string }> = [
+    { value: "global", label: t("memories.scopeGlobal") },
+    { value: "user", label: t("memories.scopeUser") },
+    { value: "session", label: t("memories.scopeSession") },
+    { value: "agent", label: t("memories.scopeAgent") },
+    { value: "team", label: t("memories.scopeTeam") },
+  ];
+
+  return (
+    <div className="rounded-xl border border-border bg-muted/20 p-3">
+      <div className="grid gap-3 2xl:grid-cols-[minmax(280px,1fr)_auto] 2xl:items-center">
+        <SearchInput className="sm:w-full" value={searchText} onChange={onSearch} placeholder={t("memories.searchPlaceholder")} />
+        <div className="flex flex-wrap gap-2 2xl:justify-end">
+          <PillSelect
+            label={t("memories.scope")}
+            value={scopeFilter}
+            onChange={onScope}
+            options={[{ value: "all", label: t("memories.allScopes") }, ...scopeOptions]}
+          />
+          <PillSelect
+            label={t("common.type")}
+            value={typeFilter}
+            onChange={onType}
+            options={[{ value: "all", label: t("memories.allTypes") }, ...typeOptions.map((type) => ({ value: type, label: type }))]}
+          />
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function PillSelect({ label, value, options, onChange }: { label: string; value: string; options: Array<{ value: string; label: string }>; onChange: (value: string) => void }) {
+  return (
+    <label className="inline-flex h-10 items-center gap-2 rounded-full border border-border bg-surface-elevated px-3 text-sm shadow-sm">
+      <Filter className="h-3.5 w-3.5 text-muted-foreground" />
+      <span className="text-xs text-muted-foreground">{label}</span>
+      <select className="bg-transparent text-sm font-medium outline-none" value={value} onChange={(event) => onChange(event.target.value)}>
+        {options.map((option) => (
+          <option key={option.value} value={option.value}>
+            {option.label}
+          </option>
+        ))}
+      </select>
+    </label>
+  );
+}
+
+function MemoryListItem({
+  memory,
+  selected,
+  onSelect,
+}: {
+  memory: MemoryItem;
+  selected: boolean;
+  onSelect: () => void;
+}) {
+  const { t } = useTranslation();
+
+  return (
+    <div className={`rounded-xl border p-4 transition ${selected ? "border-primary/55 bg-primary/5 shadow-sm" : "border-border bg-surface-elevated hover:border-primary/30"}`}>
+      <button className="block w-full text-left" onClick={onSelect}>
+        <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
+          <div className="min-w-0 space-y-2">
+            <div className="flex flex-wrap items-center gap-2">
+              <Badge className="border-border bg-muted/40 text-muted-foreground">{memory.scope_type}</Badge>
+              <Badge className="border-border bg-muted/40 text-muted-foreground">{memory.memory_type}</Badge>
+              {isExpired(memory) ? <Badge className="border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-200">{t("memories.expired")}</Badge> : null}
+            </div>
+            <p className="line-clamp-2 text-sm font-medium leading-6">{memory.content_text}</p>
+            <div className="flex flex-wrap gap-3 text-xs text-muted-foreground">
+              <span>{t("memories.scopeId")}: {memory.scope_id}</span>
+              <span>{t("memories.importance")}: {memory.importance_score}</span>
+              <span>{t("common.created")}: {formatDateTime(memory.created_time)}</span>
+            </div>
+          </div>
+          <ImportanceMeter value={memory.importance_score} />
+        </div>
+      </button>
+    </div>
+  );
+}
+
+function MemoryDetailPanel({ memory }: { memory?: MemoryItem }) {
+  const { t } = useTranslation();
+
+  if (!memory) {
+    return (
+      <Card>
+        <CardContent>
+          <EmptyState icon={Brain} title={t("memories.selectMemory")} description={t("memories.selectMemoryDesc")} />
+        </CardContent>
+      </Card>
+    );
+  }
+  return (
+    <div className="space-y-4">
+      <div className="flex flex-wrap items-center gap-2">
+        <Badge className="border-border bg-muted/40 text-muted-foreground">{memory.scope_type}</Badge>
+        <Badge className="border-border bg-muted/40 text-muted-foreground">{memory.memory_type}</Badge>
+      </div>
+        <div className="rounded-xl border border-border bg-muted/20 p-4">
+          <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{t("memories.content")}</p>
+          <p className="mt-2 whitespace-pre-wrap text-sm leading-6">{memory.content_text}</p>
+        </div>
+        <div className="grid gap-3 sm:grid-cols-2">
+          <DetailTile label={t("memories.scopeId")} value={memory.scope_id} />
+          <DetailTile label={t("memories.importance")} value={`${memory.importance_score}/100`} />
+          <DetailTile label={t("memories.ownerAgent")} value={memory.owner_agent_id || t("common.notSet")} />
+          <DetailTile label={t("memories.user")} value={memory.user_id || t("common.notSet")} />
+          <DetailTile label={t("memories.session")} value={memory.session_id || t("common.notSet")} />
+          <DetailTile label={t("memories.source")} value={memory.source_ref || t("common.notSet")} />
+          <DetailTile label={t("memories.embedding")} value={memory.embedding_model || t("memories.notGenerated")} />
+          <DetailTile label={t("memories.vectorSize")} value={memory.embedding_json?.length ? String(memory.embedding_json.length) : "0"} />
+        </div>
+        <div className="grid gap-3 sm:grid-cols-3">
+          <TimelineTile icon={Clock} label={t("common.created")} value={formatDateTime(memory.created_time)} />
+          <TimelineTile icon={Clock} label={t("memories.lastAccessed")} value={memory.last_accessed_time ? formatDateTime(memory.last_accessed_time) : t("memories.never")} />
+          <TimelineTile icon={Clock} label={t("memories.expires")} value={memory.expires_time ? formatDateTime(memory.expires_time) : t("memories.never")} />
+        </div>
+        <KeyValuePanel title={t("memories.metadata")} data={memory.metadata_json} emptyText={t("memories.noMetadata")} />
+        {memory.content_json ? <KeyValuePanel title={t("memories.structuredContent")} data={memory.content_json} emptyText={t("memories.noStructuredContent")} /> : null}
+    </div>
+  );
+}
+
+function DetailTile({ label, value }: { label: string; value: string }) {
+  return (
+    <div className="min-w-0 rounded-lg border border-border bg-muted/20 p-3">
+      <p className="text-xs text-muted-foreground">{label}</p>
+      <p className="mt-1 truncate text-sm font-medium">{value}</p>
+    </div>
+  );
+}
+
+function TimelineTile({ icon: Icon, label, value }: { icon: typeof Clock; label: string; value: string }) {
+  return (
+    <div className="rounded-lg border border-border bg-muted/20 p-3">
+      <div className="flex items-center gap-2 text-xs text-muted-foreground">
+        <Icon className="h-3.5 w-3.5" />
+        {label}
+      </div>
+      <p className="mt-1 text-xs font-medium">{value}</p>
+    </div>
+  );
+}
+
+function KeyValuePanel({ title, data, emptyText }: { title: string; data: Record<string, JSONValue>; emptyText: string }) {
+  const entries = Object.entries(data);
+  return (
+    <div className="rounded-xl border border-border bg-muted/20 p-4">
+      <p className="text-sm font-semibold">{title}</p>
+      {entries.length ? (
+        <div className="mt-3 grid gap-2">
+          {entries.map(([key, value]) => (
+            <div key={key} className="grid gap-2 rounded-md border border-border bg-surface-elevated p-2 text-sm sm:grid-cols-[140px_1fr]">
+              <span className="font-mono text-xs text-muted-foreground">{key}</span>
+              <span className="break-words text-xs">{formatJsonValue(value)}</span>
+            </div>
+          ))}
+        </div>
+      ) : (
+        <p className="mt-2 text-sm text-muted-foreground">{emptyText}</p>
+      )}
+    </div>
+  );
+}
+
+function ImportanceMeter({ value }: { value: number }) {
+  const { t } = useTranslation();
+  const width = `${clamp(value, 0, 100)}%`;
+  return (
+    <div className="w-full max-w-36 shrink-0">
+      <div className="flex justify-between text-xs text-muted-foreground">
+        <span>{t("memories.importance")}</span>
+        <span>{value}</span>
+      </div>
+      <div className="mt-2 h-2 rounded-full bg-muted">
+        <div className="h-2 rounded-full bg-primary" style={{ width }} />
+      </div>
+    </div>
+  );
+}
+
+function isExpired(memory: MemoryItem) {
+  return Boolean(memory.expires_time && new Date(memory.expires_time).getTime() < Date.now());
+}
+
+function formatJsonValue(value: JSONValue): string {
+  if (value === null) return "null";
+  if (typeof value === "string") return value;
+  if (typeof value === "number" || typeof value === "boolean") return String(value);
+  if (Array.isArray(value)) return value.map(formatJsonValue).join(", ");
+  return Object.entries(value).map(([key, item]) => `${key}: ${formatJsonValue(item)}`).join(", ");
+}
+
+function clamp(value: number, min: number, max: number) {
+  return Math.min(max, Math.max(min, value));
+}

+ 7 - 25
web/src/pages/models/ModelProvidersPage.tsx

@@ -8,7 +8,6 @@ import {
   Trash2,
   Unplug,
   Wifi,
-  WifiOff,
 } from "lucide-react";
 import {
   createModelProvider,
@@ -23,7 +22,6 @@ import { EmptyState } from "@/components/shared/EmptyState";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
 import { MetricCard } from "@/components/shared/MetricCard";
 import { PageHeader } from "@/components/shared/PageHeader";
-import { StatusBadge } from "@/components/shared/StatusBadge";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Dialog } from "@/components/ui/dialog";
@@ -108,12 +106,6 @@ export function ModelProvidersPage() {
     void load();
   }
 
-  async function toggleProviderStatus(provider: ModelProvider) {
-    const next = provider.status === "active" ? "inactive" : "active";
-    await updateModelProvider(provider.id, { status: next });
-    void load();
-  }
-
   async function testConnection(provider: ModelProvider) {
     try {
       const result = await testModelProviderConnection(provider.id);
@@ -157,13 +149,13 @@ export function ModelProvidersPage() {
       <div className="grid gap-4 md:grid-cols-3">
         <MetricCard label={t("modelProviders.totalProviders")} value={providers.length} icon={Unplug} />
         <MetricCard
-          label={t("modelProviders.activeProviders")}
-          value={providers.filter((p) => p.status === "active").length}
+          label={t("modelProviders.totalModels")}
+          value={providers.reduce((acc, p) => acc + p.models.length, 0)}
           icon={Wifi}
         />
         <MetricCard
-          label={t("modelProviders.totalModels")}
-          value={providers.reduce((acc, p) => acc + p.models.filter((m) => m.enabled).length, 0)}
+          label={t("modelProviders.defaultModels")}
+          value={providers.filter((p) => p.default_model).length}
           icon={KeyRound}
         />
       </div>
@@ -194,14 +186,13 @@ export function ModelProvidersPage() {
                   <div className="min-w-0 flex-1 space-y-2">
                     <div className="flex items-center gap-2">
                       <span className="font-medium">{provider.name}</span>
-                      <StatusBadge status={provider.status} />
                       <span className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
                         {t(`modelProviders.${provider.provider_type}`)}
                       </span>
                     </div>
                     <p className="truncate text-xs text-muted-foreground">{provider.base_url}</p>
                     <div className="flex flex-wrap gap-1">
-                      {provider.models.filter((m) => m.enabled).map((m) => (
+                      {provider.models.map((m) => (
                         <span
                           key={m.model_id}
                           className="flex items-center gap-1 rounded bg-primary/10 px-1.5 py-0.5 text-xs text-primary"
@@ -211,7 +202,7 @@ export function ModelProvidersPage() {
                           <ModelTypeBadge type={m.model_type} compact />
                         </span>
                       ))}
-                      {provider.models.filter((m) => m.enabled).length === 0 && (
+                      {provider.models.length === 0 && (
                         <span className="text-xs text-muted-foreground">{t("modelProviders.noModels")}</span>
                       )}
                     </div>
@@ -220,14 +211,6 @@ export function ModelProvidersPage() {
                     <Button size="sm" variant="ghost" onClick={() => void testConnection(provider)} title={t("modelProviders.testConnection")}>
                       <Wifi className="h-4 w-4" />
                     </Button>
-                    <Button
-                      size="sm"
-                      variant="ghost"
-                      onClick={() => void toggleProviderStatus(provider)}
-                      title={t("modelProviders.toggleStatus")}
-                    >
-                      {provider.status === "active" ? <WifiOff className="h-4 w-4" /> : <Wifi className="h-4 w-4" />}
-                    </Button>
                     <Button
                       size="sm"
                       variant="ghost"
@@ -339,7 +322,7 @@ function ProviderDialog({
   }
 
   function addModelRow() {
-    setModels((prev) => [...prev, { model_id: "", display_name: "", model_type: "chat", enabled: true }]);
+    setModels((prev) => [...prev, { model_id: "", display_name: "", model_type: "chat" }]);
   }
 
   function removeModelRow(index: number) {
@@ -393,7 +376,6 @@ function ProviderDialog({
         model_id: m.model_id,
         display_name: m.display_name,
         model_type: m.model_type,
-        enabled: true,
       }));
     setModels((prev) => [...prev, ...newItems]);
     if (!defaultModel && newItems.length > 0) {

+ 453 - 206
web/src/pages/models/ModelsPage.tsx

@@ -1,13 +1,15 @@
 import * as React from "react";
+import { useTranslation } from "react-i18next";
+import type { TFunction } from "i18next";
 import {
-  Activity,
-  CheckCircle2,
-  Cpu,
+  BrainCircuit,
   FlaskConical,
+  KeyRound,
+  Pencil,
   Plus,
-  Power,
   RefreshCw,
   Save,
+  Search,
   Trash2,
 } from "lucide-react";
 import {
@@ -16,33 +18,39 @@ import {
   listModels,
   testModel,
   updateModel,
-  updateModelStatus,
 } from "@/api";
 import { ApiErrorState } from "@/components/shared/ApiErrorState";
 import { EmptyState } from "@/components/shared/EmptyState";
-import { EntityListItem } from "@/components/shared/EntityListItem";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
 import { MetricCard } from "@/components/shared/MetricCard";
 import { PageHeader } from "@/components/shared/PageHeader";
 import { SearchInput } from "@/components/shared/SearchInput";
-import { StatusBadge } from "@/components/shared/StatusBadge";
+import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Dialog } from "@/components/ui/dialog";
 import { Input, Textarea } from "@/components/ui/input";
 import { Select } from "@/components/ui/select";
 import { toast } from "@/components/ui/toaster";
+import { demoText } from "@/lib/demo-text";
 import { formatDateTime } from "@/lib/utils";
-import type { ModelCreateRequest, ModelDefinition, ModelStatus } from "@/types";
+import type { ModelCreateRequest, ModelDefinition } from "@/types";
+
+const providerPresets = [
+  { value: "openai_compatible", labelKey: "models.providerOpenaiCompatible", defaultLabel: "OpenAI compatible", baseUrl: "http://127.0.0.1:11434/v1" },
+  { value: "openai", labelKey: "models.providerOpenai", defaultLabel: "OpenAI", baseUrl: "https://api.openai.com/v1" },
+  { value: "ollama", labelKey: "models.providerOllama", defaultLabel: "Ollama", baseUrl: "http://127.0.0.1:11434/v1" },
+  { value: "anthropic", labelKey: "models.providerAnthropic", defaultLabel: "Anthropic", baseUrl: "https://api.anthropic.com" },
+  { value: "deepseek", labelKey: "models.providerDeepSeek", defaultLabel: "DeepSeek", baseUrl: "https://api.deepseek.com/v1" },
+  { value: "custom", labelKey: "models.providerCustom", defaultLabel: "Custom", baseUrl: "" },
+];
 
 type ModelFormState = {
-  code: string;
   name: string;
   provider_type: string;
   provider_base_url: string;
   provider_api_key: string;
   model_name: string;
-  status: ModelStatus;
   description: string;
   capabilities: string;
   context_window: string;
@@ -52,13 +60,11 @@ type ModelFormState = {
 };
 
 const emptyForm: ModelFormState = {
-  code: "",
   name: "",
   provider_type: "openai_compatible",
   provider_base_url: "http://127.0.0.1:11434/v1",
   provider_api_key: "",
   model_name: "",
-  status: "active",
   description: "",
   capabilities: "chat",
   context_window: "",
@@ -68,40 +74,42 @@ const emptyForm: ModelFormState = {
 };
 
 export function ModelsPage() {
+  const { t } = useTranslation();
   const [models, setModels] = React.useState<ModelDefinition[]>([]);
-  const [selectedId, setSelectedId] = React.useState<string>();
   const [search, setSearch] = React.useState("");
-  const [statusFilter, setStatusFilter] = React.useState("all");
+  const [providerFilter, setProviderFilter] = React.useState("all");
   const [loading, setLoading] = React.useState(true);
   const [saving, setSaving] = React.useState(false);
   const [testing, setTesting] = React.useState(false);
   const [error, setError] = React.useState<string>();
   const [createOpen, setCreateOpen] = React.useState(false);
+  const [editOpen, setEditOpen] = React.useState(false);
+  const [testOpen, setTestOpen] = React.useState(false);
+  const [activeModel, setActiveModel] = React.useState<ModelDefinition>();
   const [form, setForm] = React.useState<ModelFormState>(emptyForm);
-  const [testPrompt, setTestPrompt] = React.useState("Say OK in one short sentence.");
+  const [testPrompt, setTestPrompt] = React.useState(t("models.defaultTestPrompt"));
   const [testOutput, setTestOutput] = React.useState<string>();
 
-  const selected = models.find((model) => model.id === selectedId);
   const providers = Array.from(new Set(models.map((model) => model.provider_type))).sort();
-  const activeCount = models.filter((model) => model.status === "active").length;
-  const chatReadyCount = models.filter((model) => model.capabilities_json.includes("chat")).length;
+  const keyConfiguredCount = models.filter((model) => model.has_provider_api_key).length;
 
-  const filtered = models.filter((model) => {
-    const haystack = `${model.name} ${model.code} ${model.model_name} ${model.provider_type}`.toLowerCase();
-    const matchesSearch = haystack.includes(search.toLowerCase());
-    const matchesStatus = statusFilter === "all" || model.status === statusFilter;
-    return matchesSearch && matchesStatus;
-  });
+  const filtered = models
+    .filter((model) => {
+      const text = `${model.name} ${model.model_name} ${model.provider_type} ${model.description ?? ""}`.toLowerCase();
+      return (
+        text.includes(search.toLowerCase()) &&
+        (providerFilter === "all" || model.provider_type === providerFilter)
+      );
+    })
+    .sort((first, second) => new Date(second.updated_time).getTime() - new Date(first.updated_time).getTime());
 
   const load = React.useCallback(async () => {
     setLoading(true);
     setError(undefined);
     try {
-      const data = await listModels();
-      setModels(data);
-      setSelectedId((current) => current ?? data[0]?.id);
+      setModels(await listModels());
     } catch (err) {
-      setError(err instanceof Error ? err.message : "Failed to load models");
+      setError(err instanceof Error ? err.message : t("models.failedToLoad"));
     } finally {
       setLoading(false);
     }
@@ -111,232 +119,228 @@ export function ModelsPage() {
     void load();
   }, [load]);
 
-  React.useEffect(() => {
-    if (selected) setForm(fromModel(selected));
-  }, [selected]);
-
-  async function createFromDialog(payload: ModelCreateRequest) {
+  async function createFromDialog(formState: ModelFormState) {
+    const payload = toPayload(formState);
     const created = await createModel(payload);
     setModels((current) => [created, ...current]);
-    setSelectedId(created.id);
     setCreateOpen(false);
-    toast.success("Model created");
+    toast.success(t("models.modelCreated"));
   }
 
-  async function saveSelected() {
-    if (!selected) return;
+  function openEdit(model: ModelDefinition) {
+    setActiveModel(model);
+    setForm(fromModel(model));
+    setEditOpen(true);
+  }
+
+  function openTest(model: ModelDefinition) {
+    setActiveModel(model);
+    setTestPrompt(t("models.defaultTestPrompt"));
+    setTestOutput(undefined);
+    setTestOpen(true);
+  }
+
+  async function saveActiveModel() {
+    if (!activeModel) return;
     setSaving(true);
     try {
       const payload = toPayload(form);
       if (!form.provider_api_key.trim()) delete payload.provider_api_key;
-      const updated = await updateModel(selected.id, payload);
+      const updated = await updateModel(activeModel.id, payload);
       setModels((current) => current.map((model) => (model.id === updated.id ? updated : model)));
-      toast.success("Model saved");
+      setActiveModel(updated);
+      setEditOpen(false);
+      toast.success(t("models.modelSaved"));
     } catch (err) {
-      toast.error(err instanceof Error ? err.message : "Failed to save model");
+      toast.error(err instanceof Error ? err.message : t("models.failedToSave"));
     } finally {
       setSaving(false);
     }
   }
 
-  async function toggleSelected() {
-    if (!selected) return;
-    const nextStatus: ModelStatus = selected.status === "active" ? "disabled" : "active";
-    const updated = await updateModelStatus(selected.id, nextStatus);
-    setModels((current) => current.map((model) => (model.id === updated.id ? updated : model)));
-    toast.success(nextStatus === "active" ? "Model enabled" : "Model disabled");
-  }
-
-  async function deleteSelected() {
-    if (!selected) return;
-    await deleteModel(selected.id);
-    setModels((current) => current.filter((model) => model.id !== selected.id));
-    setSelectedId(models.find((model) => model.id !== selected.id)?.id);
-    setTestOutput(undefined);
-    toast.success("Model deleted");
+  async function removeModel(model: ModelDefinition) {
+    try {
+      await deleteModel(model.id);
+      setModels((current) => current.filter((item) => item.id !== model.id));
+      toast.success(t("models.modelDeleted"));
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : t("models.failedToDelete"));
+    }
   }
 
   async function runTest() {
-    if (!selected) return;
+    if (!activeModel) return;
     setTesting(true);
     setTestOutput(undefined);
     try {
-      const result = await testModel(selected.id, { prompt: testPrompt, max_tokens: 128 });
+      const result = await testModel(activeModel.id, { prompt: testPrompt, max_tokens: 128 });
       setTestOutput(result.response.content || JSON.stringify(result.response.raw_response_json, null, 2));
-      toast.success("Model test completed");
+      toast.success(t("models.modelTestCompleted"));
     } catch (err) {
-      setTestOutput(err instanceof Error ? err.message : "Model test failed");
-      toast.error("Model test failed");
+      setTestOutput(err instanceof Error ? err.message : t("models.testFailed"));
+      toast.error(t("models.testFailed"));
     } finally {
       setTesting(false);
     }
   }
 
-  if (loading) return <LoadingSpinner label="Loading models" />;
+  if (loading) return <LoadingSpinner label={t("models.loading")} />;
   if (error) return <ApiErrorState message={error} onRetry={() => void load()} />;
 
   return (
     <div className="space-y-6">
       <PageHeader
-        title="Models"
-        description="Manage model providers, serving names, defaults, and connectivity tests."
+        title={t("models.title")}
+        description={t("models.description")}
         actions={
           <>
             <Button variant="outline" onClick={() => void load()}>
-              <RefreshCw className="h-4 w-4" /> Refresh
+              <RefreshCw className="h-4 w-4" /> {t("common.refresh")}
             </Button>
             <Button onClick={() => setCreateOpen(true)}>
-              <Plus className="h-4 w-4" /> New Model
+              <Plus className="h-4 w-4" /> {t("models.newModel")}
             </Button>
           </>
         }
       />
 
-      <div className="grid gap-4 md:grid-cols-3">
-        <MetricCard label="Models" value={models.length} icon={Cpu} />
-        <MetricCard label="Active" value={activeCount} icon={CheckCircle2} />
-        <MetricCard label="Chat Ready" value={chatReadyCount} icon={Activity} />
+      <div className="grid gap-4 md:grid-cols-2">
+        <MetricCard label={t("models.configured")} value={models.length} icon={BrainCircuit} />
+        <MetricCard label={t("models.withApiKey")} value={`${keyConfiguredCount}/${models.length}`} icon={KeyRound} />
       </div>
 
-      <div className="grid gap-6 xl:grid-cols-[380px_1fr]">
-        <Card>
-          <CardHeader>
-            <CardTitle>Model Catalog</CardTitle>
-            <CardDescription>{filtered.length} of {models.length} shown</CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-4">
-            <SearchInput value={search} onChange={setSearch} placeholder="Search models" />
+      <Card>
+        <CardHeader>
+          <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
+            <div>
+              <CardTitle>{t("models.configuredModels")}</CardTitle>
+              <CardDescription>{t("models.shown", { shown: filtered.length, total: models.length })}</CardDescription>
+            </div>
+            <Badge className="w-fit border-border bg-muted/50 text-muted-foreground">{t("models.listManagement")}</Badge>
+          </div>
+        </CardHeader>
+        <CardContent className="space-y-4">
+          <div className="grid gap-3 lg:grid-cols-[1fr_220px]">
+            <SearchInput value={search} onChange={setSearch} placeholder={t("models.searchPlaceholder")} />
             <Select
-              value={statusFilter}
-              onChange={(event) => setStatusFilter(event.target.value)}
+              value={providerFilter}
+              onChange={(event) => setProviderFilter(event.target.value)}
               options={[
-                { value: "all", label: "All statuses" },
-                { value: "active", label: "Active" },
-                { value: "disabled", label: "Disabled" },
+                { value: "all", label: t("models.allProviders") },
+                ...providers.map((provider) => ({ value: provider, label: providerLabel(provider, t) })),
               ]}
             />
-            {filtered.length ? (
-              <div className="space-y-2">
+          </div>
+
+          {filtered.length ? (
+            <div className="overflow-hidden rounded-md border border-border">
+                <div className="hidden grid-cols-[1.2fr_1fr_150px_150px] gap-4 border-b border-border bg-muted/35 px-4 py-3 text-xs font-medium uppercase tracking-wide text-muted-foreground lg:grid">
+                  <span>{t("models.model")}</span>
+                  <span>{t("models.provider")}</span>
+                  <span>{t("models.capabilities")}</span>
+                  <span className="text-right">{t("common.actions")}</span>
+                </div>
+              <div className="divide-y divide-border">
                 {filtered.map((model) => (
-                  <EntityListItem
+                  <ModelRow
                     key={model.id}
-                    title={model.name}
-                    subtitle={`${model.code} - ${model.model_name}`}
-                    active={model.id === selectedId}
-                    onClick={() => {
-                      setSelectedId(model.id);
-                      setTestOutput(undefined);
-                    }}
-                    meta={<StatusBadge status={model.status} />}
+                    model={model}
+                    onEdit={() => openEdit(model)}
+                    onTest={() => openTest(model)}
+                    onDelete={() => void removeModel(model)}
                   />
                 ))}
               </div>
-            ) : (
-              <EmptyState icon={Cpu} title="No models" description="Create a model configuration to start routing chat completions." />
-            )}
-          </CardContent>
-        </Card>
-
-        {selected ? (
-          <div className="space-y-6">
-            <Card>
-              <CardHeader>
-                <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
-                  <div>
-                    <CardTitle>{selected.name}</CardTitle>
-                    <CardDescription>
-                      {selected.provider_type} / {selected.model_name}
-                    </CardDescription>
-                  </div>
-                  <div className="flex flex-wrap gap-2">
-                    <Button variant="outline" onClick={() => void toggleSelected()}>
-                      <Power className="h-4 w-4" /> {selected.status === "active" ? "Disable" : "Enable"}
-                    </Button>
-                    <Button variant="destructive" onClick={() => void deleteSelected()}>
-                      <Trash2 className="h-4 w-4" /> Delete
-                    </Button>
-                  </div>
-                </div>
-              </CardHeader>
-              <CardContent>
-                <ModelForm form={form} providers={providers} onChange={setForm} />
-                <div className="mt-5 flex justify-end">
-                  <Button onClick={() => void saveSelected()} disabled={saving}>
-                    <Save className="h-4 w-4" /> {saving ? "Saving" : "Save"}
-                  </Button>
-                </div>
-              </CardContent>
-            </Card>
-
-            <Card>
-              <CardHeader>
-                <CardTitle>Connectivity Test</CardTitle>
-                <CardDescription>Send a short prompt through this exact provider configuration.</CardDescription>
-              </CardHeader>
-              <CardContent className="space-y-4">
-                <Textarea value={testPrompt} onChange={(event) => setTestPrompt(event.target.value)} />
-                <div className="flex justify-end">
-                  <Button onClick={() => void runTest()} disabled={testing || selected.status !== "active"}>
-                    <FlaskConical className="h-4 w-4" /> {testing ? "Testing" : "Test Model"}
-                  </Button>
-                </div>
-                {testOutput ? (
-                  <pre className="max-h-72 overflow-auto rounded-md border border-border bg-muted/40 p-3 text-sm whitespace-pre-wrap">
-                    {testOutput}
-                  </pre>
-                ) : null}
-                <div className="grid gap-2 text-sm text-muted-foreground sm:grid-cols-2">
-                  <span>Updated {formatDateTime(selected.updated_time)}</span>
-                  <span>{selected.has_provider_api_key ? "API key configured" : "No API key configured"}</span>
-                </div>
-              </CardContent>
-            </Card>
-          </div>
-        ) : (
-          <EmptyState icon={Cpu} title="No model selected" description="Select or create a model to edit its configuration." />
-        )}
-      </div>
+            </div>
+          ) : (
+            <EmptyState
+              icon={Search}
+              title={t("models.noMatching")}
+              description={t("models.noMatchingDesc")}
+              actionLabel={t("models.createModel")}
+              onAction={() => setCreateOpen(true)}
+            />
+          )}
+        </CardContent>
+      </Card>
+
+      <CreateModelDialog open={createOpen} onOpenChange={setCreateOpen} onCreate={createFromDialog} providers={providers} />
 
-      <CreateModelDialog
-        open={createOpen}
-        onOpenChange={setCreateOpen}
-        onCreate={createFromDialog}
+      <EditModelDialog
+        open={editOpen}
+        onOpenChange={setEditOpen}
+        form={form}
         providers={providers}
+        activeModelName={demoText(activeModel?.name, t)}
+        saving={saving}
+        onChange={setForm}
+        onSave={saveActiveModel}
       />
+
+      <Dialog open={testOpen} onOpenChange={setTestOpen} title={t("models.testModel")} description={demoText(activeModel?.name, t)} className="max-w-2xl">
+        <div className="space-y-4">
+          <Field label={t("models.prompt")}>
+            <Textarea value={testPrompt} onChange={(event) => setTestPrompt(event.target.value)} />
+          </Field>
+          <Button className="w-full" onClick={() => void runTest()} disabled={testing}>
+            <FlaskConical className="h-4 w-4" /> {testing ? t("models.testing") : t("models.runTest")}
+          </Button>
+          {testOutput ? (
+            <pre className="max-h-72 overflow-auto whitespace-pre-wrap rounded-md bg-muted/40 p-3 text-xs leading-5 text-muted-foreground">
+              {testOutput}
+            </pre>
+          ) : null}
+        </div>
+      </Dialog>
     </div>
   );
 }
 
-function ModelForm({
-  form,
-  providers,
-  onChange,
+function ModelRow({
+  model,
+  onEdit,
+  onTest,
+  onDelete,
 }: {
-  form: ModelFormState;
-  providers: string[];
-  onChange: (form: ModelFormState) => void;
+  model: ModelDefinition;
+  onEdit: () => void;
+  onTest: () => void;
+  onDelete: () => void;
 }) {
-  const set = (key: keyof ModelFormState, value: string) => onChange({ ...form, [key]: value });
-  const providerOptions = Array.from(new Set(["openai_compatible", "openai", "ollama", ...providers])).map((value) => ({
-    value,
-    label: value,
-  }));
+  const { t } = useTranslation();
+
   return (
-    <div className="grid gap-4 md:grid-cols-2">
-      <Field label="Code"><Input value={form.code} onChange={(event) => set("code", event.target.value)} /></Field>
-      <Field label="Name"><Input value={form.name} onChange={(event) => set("name", event.target.value)} /></Field>
-      <Field label="Provider"><Select value={form.provider_type} onChange={(event) => set("provider_type", event.target.value)} options={providerOptions} /></Field>
-      <Field label="Provider Base URL"><Input value={form.provider_base_url} onChange={(event) => set("provider_base_url", event.target.value)} /></Field>
-      <Field label="Model Name"><Input value={form.model_name} onChange={(event) => set("model_name", event.target.value)} /></Field>
-      <Field label="API Key"><Input type="password" value={form.provider_api_key} onChange={(event) => set("provider_api_key", event.target.value)} placeholder="Leave blank to keep unset" /></Field>
-      <Field label="Capabilities"><Input value={form.capabilities} onChange={(event) => set("capabilities", event.target.value)} placeholder="chat, tools, vision" /></Field>
-      <Field label="Status"><Select value={form.status} onChange={(event) => set("status", event.target.value)} options={[{ value: "active", label: "Active" }, { value: "disabled", label: "Disabled" }]} /></Field>
-      <Field label="Context Window"><Input value={form.context_window} onChange={(event) => set("context_window", event.target.value)} inputMode="numeric" /></Field>
-      <Field label="Max Output Tokens"><Input value={form.max_output_tokens} onChange={(event) => set("max_output_tokens", event.target.value)} inputMode="numeric" /></Field>
-      <Field label="Default Temperature"><Input value={form.default_temperature} onChange={(event) => set("default_temperature", event.target.value)} inputMode="decimal" /></Field>
-      <Field label="Timeout Seconds"><Input value={form.timeout_seconds} onChange={(event) => set("timeout_seconds", event.target.value)} inputMode="decimal" /></Field>
-      <div className="md:col-span-2">
-        <Field label="Description"><Textarea value={form.description} onChange={(event) => set("description", event.target.value)} /></Field>
+    <div className="grid gap-3 px-4 py-4 lg:grid-cols-[1.2fr_1fr_150px_150px] lg:items-center">
+      <div className="min-w-0">
+        <div className="flex items-center gap-2">
+          <BrainCircuit className="h-4 w-4 text-primary" />
+          <span className="truncate text-sm font-medium">{demoText(model.name, t)}</span>
+        </div>
+        <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{model.model_name}</p>
+        <p className="mt-1 text-xs text-muted-foreground lg:hidden">{t("common.updated")} {formatDateTime(model.updated_time)}</p>
+      </div>
+      <div className="min-w-0 text-sm">
+        <p className="truncate">{providerLabel(model.provider_type, t)}</p>
+        <p className="mt-1 truncate text-xs text-muted-foreground">{model.provider_base_url}</p>
+      </div>
+      <div className="flex flex-wrap gap-1.5">
+        {model.capabilities_json.slice(0, 2).map((capability) => (
+          <Badge key={capability} className="border-primary/20 bg-primary/10 text-primary">{capabilityLabel(capability, t)}</Badge>
+        ))}
+        {model.capabilities_json.length > 2 ? (
+          <Badge className="border-border bg-muted text-muted-foreground">+{model.capabilities_json.length - 2}</Badge>
+        ) : null}
+      </div>
+      <div className="flex items-center justify-start gap-1.5 lg:justify-end">
+        <Button size="icon" variant="ghost" onClick={onTest} aria-label={t("models.testNamed", { name: demoText(model.name, t) })}>
+          <FlaskConical className="h-4 w-4" />
+        </Button>
+        <Button size="icon" variant="ghost" onClick={onEdit} aria-label={t("models.editNamed", { name: demoText(model.name, t) })}>
+          <Pencil className="h-4 w-4" />
+        </Button>
+        <Button size="icon" variant="ghost" className="text-destructive hover:text-destructive" onClick={onDelete} aria-label={t("models.deleteNamed", { name: demoText(model.name, t) })}>
+          <Trash2 className="h-4 w-4" />
+        </Button>
       </div>
     </div>
   );
@@ -350,34 +354,258 @@ function CreateModelDialog({
 }: {
   open: boolean;
   onOpenChange: (open: boolean) => void;
-  onCreate: (payload: ModelCreateRequest) => Promise<void>;
+  onCreate: (form: ModelFormState) => Promise<void>;
   providers: string[];
 }) {
+  const { t } = useTranslation();
   const [form, setForm] = React.useState<ModelFormState>(emptyForm);
   const [saving, setSaving] = React.useState(false);
+  const [showAdvanced, setShowAdvanced] = React.useState(false);
 
   React.useEffect(() => {
-    if (open) setForm(emptyForm);
+    if (open) {
+      setForm(emptyForm);
+      setShowAdvanced(false);
+    }
   }, [open]);
 
   async function submit() {
     setSaving(true);
     try {
-      await onCreate(toPayload(form) as ModelCreateRequest);
+      await onCreate(form);
     } catch (err) {
-      toast.error(err instanceof Error ? err.message : "Failed to create model");
+      toast.error(err instanceof Error ? err.message : t("models.failedToCreate"));
     } finally {
       setSaving(false);
     }
   }
 
+  function setField(key: keyof ModelFormState, value: string) {
+    setForm((current) => ({ ...current, [key]: value }));
+  }
+
+  function setProvider(value: string) {
+    const preset = providerPresets.find((item) => item.value === value);
+    setForm((current) => ({
+      ...current,
+      provider_type: value,
+      provider_base_url: preset?.baseUrl ?? current.provider_base_url,
+    }));
+  }
+
+  const canCreate = Boolean(form.model_name.trim() && form.provider_base_url.trim());
+  const providerOptions = buildProviderOptions(providers, t);
+
   return (
-    <Dialog open={open} onOpenChange={onOpenChange} title="New Model" description="Add an OpenAI-compatible model endpoint." className="max-w-4xl">
-      <ModelForm form={form} providers={providers} onChange={setForm} />
-      <div className="mt-5 flex justify-end">
-        <Button onClick={() => void submit()} disabled={saving}>
-          <Plus className="h-4 w-4" /> {saving ? "Creating" : "Create Model"}
-        </Button>
+    <Dialog
+      open={open}
+      onOpenChange={onOpenChange}
+      title={t("models.newModel")}
+      description={t("models.quickCreateHint")}
+      className="max-w-xl"
+    >
+      <div className="space-y-5">
+        <div className="grid gap-4 sm:grid-cols-2">
+          <Field label={t("models.provider")}>
+            <Select value={form.provider_type} onChange={(event) => setProvider(event.target.value)} options={providerOptions} />
+          </Field>
+          <Field label={t("models.modelName")}>
+            <Input
+              autoFocus
+              required
+              value={form.model_name}
+              onChange={(event) => setField("model_name", event.target.value)}
+              placeholder="gpt-4.1-mini"
+            />
+          </Field>
+        </div>
+
+        <Field label={t("models.apiKey")}>
+          <Input
+            type="password"
+            value={form.provider_api_key}
+            onChange={(event) => setField("provider_api_key", event.target.value)}
+            placeholder={t("models.optionalApiKey")}
+          />
+        </Field>
+
+        <div className="rounded-md border border-border bg-muted/25 p-3">
+          <div className="flex items-start gap-3">
+            <BrainCircuit className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
+            <div className="min-w-0 text-sm">
+              <p className="font-medium">{form.model_name.trim() || t("models.modelNamePreview")}</p>
+              <p className="mt-1 break-words text-xs text-muted-foreground">
+                {form.provider_base_url || t("models.baseUrlNeeded")}
+              </p>
+            </div>
+          </div>
+        </div>
+
+        <button
+          type="button"
+          className="text-sm font-medium text-primary hover:text-primary/80"
+          onClick={() => setShowAdvanced((value) => !value)}
+        >
+          {showAdvanced ? t("models.hideAdvanced") : t("models.showAdvanced")}
+        </button>
+
+        {showAdvanced ? (
+          <div className="grid gap-4 rounded-md border border-border p-4 sm:grid-cols-2">
+            <Field label={t("common.name")}>
+              <Input value={form.name} onChange={(event) => setField("name", event.target.value)} placeholder={form.model_name || t("models.localChatPlaceholder")} />
+            </Field>
+            <Field label={t("models.baseUrl")}>
+              <Input value={form.provider_base_url} onChange={(event) => setField("provider_base_url", event.target.value)} />
+            </Field>
+            <Field label={t("models.capabilities")}>
+              <Input value={form.capabilities} onChange={(event) => setField("capabilities", event.target.value)} placeholder="chat, tools" />
+            </Field>
+            <Field label={t("models.timeoutSeconds")}>
+              <Input value={form.timeout_seconds} onChange={(event) => setField("timeout_seconds", event.target.value)} inputMode="decimal" />
+            </Field>
+            <div className="sm:col-span-2">
+              <Field label={t("common.description")}>
+                <Textarea value={form.description} onChange={(event) => setField("description", event.target.value)} />
+              </Field>
+            </div>
+          </div>
+        ) : null}
+
+        <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
+          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>{t("common.cancel")}</Button>
+          <Button onClick={() => void submit()} disabled={saving || !canCreate}>
+            <Plus className="h-4 w-4" /> {saving ? t("common.creating") : t("models.createModel")}
+          </Button>
+        </div>
+      </div>
+    </Dialog>
+  );
+}
+
+function EditModelDialog({
+  open,
+  onOpenChange,
+  form,
+  providers,
+  activeModelName,
+  saving,
+  onChange,
+  onSave,
+}: {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  form: ModelFormState;
+  providers: string[];
+  activeModelName?: string;
+  saving: boolean;
+  onChange: (form: ModelFormState) => void;
+  onSave: () => void | Promise<void>;
+}) {
+  const { t } = useTranslation();
+  const [showAdvanced, setShowAdvanced] = React.useState(false);
+
+  React.useEffect(() => {
+    if (open) setShowAdvanced(false);
+  }, [open]);
+
+  function setField(key: keyof ModelFormState, value: string) {
+    onChange({ ...form, [key]: value });
+  }
+
+  function setProvider(value: string) {
+    const preset = providerPresets.find((item) => item.value === value);
+    onChange({
+      ...form,
+      provider_type: value,
+      provider_base_url: preset?.baseUrl ?? form.provider_base_url,
+    });
+  }
+
+  const providerOptions = buildProviderOptions(providers, t);
+  const canSave = Boolean(form.model_name.trim() && form.provider_base_url.trim());
+
+  return (
+    <Dialog
+      open={open}
+      onOpenChange={onOpenChange}
+      title={t("models.editModel")}
+      description={activeModelName}
+      className="max-w-xl"
+    >
+      <div className="space-y-5">
+        <div className="grid gap-4 sm:grid-cols-2">
+          <Field label={t("models.provider")}>
+            <Select value={form.provider_type} onChange={(event) => setProvider(event.target.value)} options={providerOptions} />
+          </Field>
+          <Field label={t("models.modelName")}>
+            <Input value={form.model_name} onChange={(event) => setField("model_name", event.target.value)} />
+          </Field>
+        </div>
+
+        <Field label={t("models.apiKey")}>
+          <Input
+            type="password"
+            value={form.provider_api_key}
+            onChange={(event) => setField("provider_api_key", event.target.value)}
+            placeholder={t("models.keepSecretPlaceholder")}
+          />
+        </Field>
+
+        <div className="rounded-md border border-border bg-muted/25 p-3">
+          <div className="flex items-start gap-3">
+            <BrainCircuit className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
+            <div className="min-w-0 text-sm">
+              <p className="font-medium">{form.name.trim() || form.model_name.trim() || t("models.modelNamePreview")}</p>
+              <p className="mt-1 break-words text-xs text-muted-foreground">{form.provider_base_url}</p>
+            </div>
+          </div>
+        </div>
+
+        <button
+          type="button"
+          className="text-sm font-medium text-primary hover:text-primary/80"
+          onClick={() => setShowAdvanced((value) => !value)}
+        >
+          {showAdvanced ? t("models.hideAdvanced") : t("models.showAdvanced")}
+        </button>
+
+        {showAdvanced ? (
+          <div className="grid gap-4 rounded-md border border-border p-4 sm:grid-cols-2">
+            <Field label={t("common.name")}>
+              <Input value={form.name} onChange={(event) => setField("name", event.target.value)} />
+            </Field>
+            <Field label={t("models.baseUrl")}>
+              <Input value={form.provider_base_url} onChange={(event) => setField("provider_base_url", event.target.value)} />
+            </Field>
+            <Field label={t("models.capabilities")}>
+              <Input value={form.capabilities} onChange={(event) => setField("capabilities", event.target.value)} />
+            </Field>
+            <Field label={t("models.timeoutSeconds")}>
+              <Input value={form.timeout_seconds} onChange={(event) => setField("timeout_seconds", event.target.value)} inputMode="decimal" />
+            </Field>
+            <Field label={t("models.contextWindow")}>
+              <Input value={form.context_window} onChange={(event) => setField("context_window", event.target.value)} inputMode="numeric" />
+            </Field>
+            <Field label={t("models.maxOutputTokens")}>
+              <Input value={form.max_output_tokens} onChange={(event) => setField("max_output_tokens", event.target.value)} inputMode="numeric" />
+            </Field>
+            <Field label={t("models.temperature")}>
+              <Input value={form.default_temperature} onChange={(event) => setField("default_temperature", event.target.value)} inputMode="decimal" />
+            </Field>
+            <div className="sm:col-span-2">
+              <Field label={t("common.description")}>
+                <Textarea value={form.description} onChange={(event) => setField("description", event.target.value)} />
+              </Field>
+            </div>
+          </div>
+        ) : null}
+
+        <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
+          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>{t("common.cancel")}</Button>
+          <Button onClick={() => void onSave()} disabled={saving || !canSave}>
+            <Save className="h-4 w-4" /> {saving ? t("common.saving") : t("common.save")}
+          </Button>
+        </div>
       </div>
     </Dialog>
   );
@@ -385,7 +613,7 @@ function CreateModelDialog({
 
 function Field({ label, children }: { label: string; children: React.ReactNode }) {
   return (
-    <label className="space-y-1.5 text-sm font-medium">
+    <label className="block space-y-2 text-sm font-medium">
       <span>{label}</span>
       {children}
     </label>
@@ -394,13 +622,11 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
 
 function fromModel(model: ModelDefinition): ModelFormState {
   return {
-    code: model.code,
     name: model.name,
     provider_type: model.provider_type,
     provider_base_url: model.provider_base_url,
     provider_api_key: "",
     model_name: model.model_name,
-    status: model.status,
     description: model.description ?? "",
     capabilities: model.capabilities_json.join(", "),
     context_window: String(model.context_window ?? ""),
@@ -411,14 +637,13 @@ function fromModel(model: ModelDefinition): ModelFormState {
 }
 
 function toPayload(form: ModelFormState): ModelCreateRequest {
+  const modelName = form.model_name.trim();
   return {
-    code: form.code.trim(),
-    name: form.name.trim(),
+    name: form.name.trim() || modelName || "New Model",
     provider_type: form.provider_type.trim() || "openai_compatible",
     provider_base_url: form.provider_base_url.trim(),
     provider_api_key: form.provider_api_key.trim() || null,
-    model_name: form.model_name.trim(),
-    status: form.status,
+    model_name: modelName,
     description: form.description.trim() || null,
     capabilities_json: form.capabilities.split(",").map((item) => item.trim()).filter(Boolean),
     context_window: parseOptionalInteger(form.context_window),
@@ -429,6 +654,28 @@ function toPayload(form: ModelFormState): ModelCreateRequest {
   };
 }
 
+function buildProviderOptions(providers: string[], t: TFunction) {
+  return Array.from(new Set([...providerPresets.map((preset) => preset.value), ...providers])).map((value) => ({
+    value,
+    label: providerLabel(value, t),
+  }));
+}
+
+function providerLabel(value: string, t: TFunction) {
+  const preset = providerPresets.find((item) => item.value === value);
+  return preset ? t(preset.labelKey, preset.defaultLabel) : humanizeCode(value);
+}
+
+function capabilityLabel(value: string, t: TFunction) {
+  return t(`models.capability.${value}`, humanizeCode(value));
+}
+
+function humanizeCode(value: string) {
+  return value
+    .replace(/_/g, " ")
+    .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}
+
 function parseOptionalInteger(value: string) {
   if (!value.trim()) return null;
   const parsed = Number.parseInt(value, 10);

+ 110 - 12
web/src/pages/sessions/SessionChatPage.tsx

@@ -1,12 +1,17 @@
 import * as React from "react";
 import { useTranslation } from "react-i18next";
-import { listMessages } from "@/api";
+import { Info, MessageSquarePlus } from "lucide-react";
+import { createMessage, listMessages, listRunRequests } from "@/api";
 import { ApiErrorState } from "@/components/shared/ApiErrorState";
-import { PageHeader } from "@/components/shared/PageHeader";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
+import { PageHeader } from "@/components/shared/PageHeader";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Dialog } from "@/components/ui/dialog";
 import { toast } from "@/components/ui/toaster";
 import { useApps, useSessionList } from "@/hooks";
-import type { Message, Session } from "@/types";
+import { formatDateTime } from "@/lib/utils";
+import type { Message, RunRequest, Session } from "@/types";
 import { ChatPanel } from "./components/ChatPanel";
 import { CreateSessionDialog } from "./components/CreateSessionDialog";
 import { SessionListPanel } from "./components/SessionListPanel";
@@ -18,21 +23,36 @@ export function SessionChatPage() {
   const [search, setSearch] = React.useState("");
   const [activeSessionId, setActiveSessionId] = React.useState<string>();
   const [messages, setMessages] = React.useState<Message[]>([]);
+  const [runRequests, setRunRequests] = React.useState<RunRequest[]>([]);
   const [createOpen, setCreateOpen] = React.useState(false);
+  const [contextOpen, setContextOpen] = React.useState(false);
 
   React.useEffect(() => {
     if (!activeSessionId && sessions.data?.[0]) setActiveSessionId(sessions.data[0].id);
   }, [activeSessionId, sessions.data]);
+
   React.useEffect(() => {
-    if (activeSessionId) void listMessages(activeSessionId).then(setMessages);
+    if (!activeSessionId) {
+      setMessages([]);
+      setRunRequests([]);
+      return;
+    }
+    void Promise.all([listMessages(activeSessionId), listRunRequests(activeSessionId)]).then(([nextMessages, nextRuns]) => {
+      setMessages(nextMessages);
+      setRunRequests(nextRuns);
+    });
   }, [activeSessionId]);
 
   const filtered = (sessions.data ?? []).filter((session) =>
-    `${session.title ?? ""} ${session.channel_type}`.toLowerCase().includes(search.toLowerCase()));
+    `${session.title ?? ""} ${session.channel_type} ${session.user_id}`.toLowerCase().includes(search.toLowerCase()));
+  const activeSession = (sessions.data ?? []).find((session) => session.id === activeSessionId);
 
-  async function send(_text: string) {
+  async function send(text: string) {
     if (!activeSessionId) return;
-    setMessages(await listMessages(activeSessionId));
+    await createMessage({ session_id: activeSessionId, role: "user", content_type: "text", content_text: text });
+    const [nextMessages, nextRuns] = await Promise.all([listMessages(activeSessionId), listRunRequests(activeSessionId)]);
+    setMessages(nextMessages);
+    setRunRequests(nextRuns);
     toast.success(t("sessions.messageSent"));
   }
 
@@ -42,18 +62,44 @@ export function SessionChatPage() {
   }
 
   return (
-    <div className="space-y-6">
-      <PageHeader title={t("sessions.title")} description={t("sessions.description")} />
-      <div className="flex overflow-hidden rounded-md border border-border">
+    <div className="min-w-0 space-y-4 overflow-hidden">
+      <PageHeader
+        title={t("sessions.title")}
+        description={t("sessions.description")}
+        actions={
+          <Button onClick={() => setCreateOpen(true)}>
+            <MessageSquarePlus className="h-4 w-4" />
+            {t("sessions.newSession")}
+          </Button>
+        }
+      />
+      <div className="grid min-w-0 gap-4 xl:grid-cols-[320px_minmax(0,1fr)]">
         <SessionListPanel
           sessions={filtered}
           activeSessionId={activeSessionId}
           search={search}
           onSearch={setSearch}
           onSelect={setActiveSessionId}
-          onCreate={() => setCreateOpen(true)}
         />
-        <ChatPanel messages={messages} active={Boolean(activeSessionId)} onSend={(text) => void send(text)} />
+        <Card className="min-w-0 overflow-hidden">
+          <CardHeader className="flex min-w-0 flex-col gap-3 border-b border-border p-4 sm:flex-row sm:items-center sm:justify-between">
+            <div className="min-w-0">
+              <CardTitle className="truncate">{activeSession?.title || t("sessions.untitledSession")}</CardTitle>
+              <p className="mt-1 truncate text-sm text-muted-foreground">
+                {activeSession
+                  ? `${messages.length} ${t("sessions.messages")} / ${t("sessions.lastActive")} ${formatDateTime(activeSession.last_active_time ?? activeSession.created_time)}`
+                  : t("sessions.chooseOrCreate")}
+              </p>
+            </div>
+            <Button className="shrink-0" variant="outline" onClick={() => setContextOpen(true)} disabled={!activeSession}>
+              <Info className="h-4 w-4" />
+              {t("sessions.context")}
+            </Button>
+          </CardHeader>
+          <CardContent className="p-0">
+            <ChatPanel messages={messages} active={Boolean(activeSessionId)} onSend={(text) => void send(text)} />
+          </CardContent>
+        </Card>
       </div>
       <CreateSessionDialog
         open={createOpen}
@@ -64,6 +110,58 @@ export function SessionChatPage() {
           void sessions.refetch();
         }}
       />
+      <Dialog
+        open={contextOpen}
+        onOpenChange={setContextOpen}
+        title={t("sessions.context")}
+        description={activeSession?.title || t("sessions.untitledSession")}
+      >
+        {activeSession ? (
+          <div className="min-w-0 space-y-4 overflow-hidden">
+            <div className="grid min-w-0 gap-3 sm:grid-cols-2">
+              <ContextItem label={t("sessions.application")} value={appName(apps.data ?? [], activeSession.app_id, t("sessions.unknownApplication"))} />
+              <ContextItem label={t("sessions.channelType")} value={formatChannel(activeSession.channel_type)} />
+              <ContextItem label={t("sessions.user")} value={activeSession.user_id} />
+              <ContextItem label={t("sessions.lastActive")} value={formatDateTime(activeSession.last_active_time ?? activeSession.created_time)} />
+              <ContextItem label={t("sessions.messages")} value={String(messages.length)} />
+              <ContextItem label={t("sessions.runActivity")} value={String(runRequests.length)} />
+            </div>
+            <div className="min-w-0 rounded-md border border-border bg-muted/20 p-4">
+              <div className="text-sm font-medium">{t("sessions.recentActivity")}</div>
+              <div className="mt-3 space-y-2">
+                {runRequests.slice(0, 4).map((request) => (
+                  <div key={request.id} className="flex min-w-0 items-center justify-between gap-3 rounded-md bg-surface-elevated px-3 py-2 text-sm">
+                    <span className="min-w-0 truncate text-foreground">{formatTrigger(request.trigger_type, t("sessions.manualRun"))}</span>
+                    <span className="shrink-0 text-xs text-muted-foreground">{formatDateTime(request.created_time)}</span>
+                  </div>
+                ))}
+                {!runRequests.length ? <p className="text-sm text-muted-foreground">{t("sessions.noRunActivity")}</p> : null}
+              </div>
+            </div>
+          </div>
+        ) : null}
+      </Dialog>
     </div>
   );
 }
+
+function ContextItem({ label, value }: { label: string; value: string }) {
+  return (
+    <div className="min-w-0 rounded-md border border-border bg-surface-base p-3">
+      <div className="truncate text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
+      <div className="mt-1 truncate text-sm font-medium text-foreground">{value}</div>
+    </div>
+  );
+}
+
+function appName(apps: Array<{ id: string; name: string }>, appId: string, fallback: string) {
+  return apps.find((app) => app.id === appId)?.name ?? fallback;
+}
+
+function formatChannel(value: string) {
+  return value.replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
+}
+
+function formatTrigger(value: string, fallback: string) {
+  return value ? formatChannel(value) : fallback;
+}

+ 2 - 2
web/src/pages/sessions/components/ChatInput.tsx

@@ -8,7 +8,7 @@ export function ChatInput({ disabled, onSend }: { disabled?: boolean; onSend: (t
   const [text, setText] = React.useState("");
   return (
     <form
-      className="flex items-end gap-2 border-t border-border p-4"
+      className="flex items-end gap-2 border-t border-border bg-surface-elevated p-3"
       onSubmit={(event) => {
         event.preventDefault();
         if (!text.trim()) return;
@@ -18,7 +18,7 @@ export function ChatInput({ disabled, onSend }: { disabled?: boolean; onSend: (t
     >
       <textarea
         aria-label={t("sessions.message")}
-        className="max-h-40 min-h-11 flex-1 resize-none rounded-md border border-border bg-muted/40 px-3 py-2 text-base outline-none focus:border-primary/70 sm:text-sm"
+        className="max-h-32 min-h-10 flex-1 resize-none rounded-md border border-border bg-muted/40 px-3 py-2 text-base outline-none transition placeholder:text-muted-foreground focus:border-primary/70 focus:ring-2 focus:ring-primary/20 sm:text-sm"
         value={text}
         disabled={disabled}
         onChange={(event) => setText(event.target.value)}

+ 2 - 2
web/src/pages/sessions/components/ChatPanel.tsx

@@ -16,8 +16,8 @@ export function ChatPanel({
 }) {
   const { t } = useTranslation();
   return (
-    <section className="flex min-h-[680px] flex-1 flex-col bg-surface-base">
-      <div className="flex-1 space-y-4 overflow-auto p-4">
+    <section className="flex min-h-[560px] min-w-0 flex-1 flex-col overflow-hidden bg-surface-base">
+      <div className="min-w-0 flex-1 space-y-3 overflow-auto p-3 sm:p-4">
         {active ? (
           messages.length ? (
             messages.map((message) => <MessageBubble key={message.id} message={message} />)

+ 21 - 8
web/src/pages/sessions/components/CreateSessionDialog.tsx

@@ -35,29 +35,42 @@ export function CreateSessionDialog({
     event.preventDefault();
     setSubmitting(true);
     try {
-      const session = await createSession({ user_id: userId, app_id: appId, title, channel_type: channelType });
+      const session = await createSession({ user_id: userId, app_id: appId, title: title.trim(), channel_type: channelType });
       toast.success(t("sessions.sessionCreated"));
       onCreated(session);
       onOpenChange(false);
+      setTitle("");
     } finally {
       setSubmitting(false);
     }
   }
 
   return (
-    <Dialog open={open} onOpenChange={onOpenChange} title={t("sessions.create")}>
+    <Dialog open={open} onOpenChange={onOpenChange} title={t("sessions.create")} description={t("sessions.createDescription")}>
       <form className="space-y-4" onSubmit={submit}>
         <label className="block space-y-2 text-sm">
-          <span className="text-muted-foreground">{t("tools.definition")}</span>
-          <Select value={appId} onChange={(event) => setAppId(event.target.value)} options={apps.map((app) => ({ value: app.id, label: app.name }))} />
+          <span className="font-medium text-foreground">{t("sessions.application")}</span>
+          <Select
+            value={appId}
+            onChange={(event) => setAppId(event.target.value)}
+            options={apps.map((app) => ({ value: app.id, label: app.name }))}
+          />
         </label>
         <label className="block space-y-2 text-sm">
-          <span className="text-muted-foreground">{t("common.name")}</span>
-          <Input value={title} onChange={(event) => setTitle(event.target.value)} />
+          <span className="font-medium text-foreground">{t("sessions.sessionName")}</span>
+          <Input value={title} onChange={(event) => setTitle(event.target.value)} placeholder={t("sessions.sessionNamePlaceholder")} />
         </label>
         <label className="block space-y-2 text-sm">
-          <span className="text-muted-foreground">{t("sessions.channelType")}</span>
-          <Input value={channelType} onChange={(event) => setChannelType(event.target.value)} />
+          <span className="font-medium text-foreground">{t("sessions.channelType")}</span>
+          <Select
+            value={channelType}
+            onChange={(event) => setChannelType(event.target.value)}
+            options={[
+              { value: "web", label: t("sessions.channelWeb") },
+              { value: "debug", label: t("sessions.channelDebug") },
+              { value: "api", label: t("sessions.channelApi") },
+            ]}
+          />
         </label>
         <div className="flex justify-end gap-2">
           <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>

+ 30 - 4
web/src/pages/sessions/components/MessageBubble.tsx

@@ -1,20 +1,46 @@
+import { useTranslation } from "react-i18next";
 import { cn } from "@/lib/utils";
 import type { Message } from "@/types";
 
 export function MessageBubble({ message }: { message: Message }) {
+  const { t } = useTranslation();
   const isUser = message.role === "user";
   const isSystem = message.role === "system";
+  const content = readableContent(message, t);
   return (
-    <div className={cn("flex", isUser ? "justify-end" : isSystem ? "justify-center" : "justify-start")}>
+    <div className={cn("flex min-w-0", isUser ? "justify-end" : isSystem ? "justify-center" : "justify-start")}>
       <div
         className={cn(
-          "max-w-[78%] rounded-md border px-4 py-3 text-sm",
+          "min-w-0 max-w-[82%] rounded-md border px-3 py-2 text-sm",
           isUser && "border-primary/30 bg-primary text-primary-foreground",
           !isUser && !isSystem && "border-border bg-surface-elevated",
-          isSystem && "border-border bg-muted/40 text-muted-foreground")}
+          isSystem && "max-w-[90%] border-border bg-muted/50 text-muted-foreground")}
       >
-        <p className="whitespace-pre-wrap">{message.content_text ?? JSON.stringify(message.content_json ?? {})}</p>
+        {!isSystem ? (
+          <div className={cn("mb-0.5 text-[11px] font-medium uppercase tracking-wide", isUser ? "text-primary-foreground/70" : "text-muted-foreground")}>
+            {isUser ? t("sessions.roleUser") : t("sessions.roleAssistant")}
+          </div>
+        ) : null}
+        <p className="whitespace-pre-wrap break-words leading-6 [overflow-wrap:anywhere]">{content}</p>
       </div>
     </div>
   );
 }
+
+function readableContent(message: Message, t: (key: string, options?: Record<string, unknown>) => string) {
+  const text = message.content_text?.trim();
+  if (text) return text;
+  const payload = message.content_json;
+  if (!payload) return t("sessions.noTextContent");
+  if (typeof payload === "string") return payload;
+  if (Array.isArray(payload)) return t("sessions.structuredItems", { count: payload.length });
+  if (typeof payload === "object") {
+    const record = payload as Record<string, unknown>;
+    for (const key of ["text", "message", "answer", "output", "result", "content"]) {
+      const value = record[key];
+      if (typeof value === "string" && value.trim()) return value;
+    }
+    return t("sessions.structuredMessage");
+  }
+  return String(payload);
+}

+ 38 - 25
web/src/pages/sessions/components/SessionListPanel.tsx

@@ -1,9 +1,8 @@
 import { useTranslation } from "react-i18next";
-import { MessageSquare, Plus } from "lucide-react";
-import { Button } from "@/components/ui/button";
+import { MessageSquare } from "lucide-react";
 import { SearchInput } from "@/components/shared/SearchInput";
-import { RelativeTime } from "@/components/shared/RelativeTime";
-import { cn } from "@/lib/utils";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { cn, formatDateTime } from "@/lib/utils";
 import type { Session } from "@/types";
 
 export function SessionListPanel({
@@ -12,46 +11,60 @@ export function SessionListPanel({
   search,
   onSearch,
   onSelect,
-  onCreate,
 }: {
   sessions: Session[];
   activeSessionId?: string;
   search: string;
   onSearch: (value: string) => void;
   onSelect: (sessionId: string) => void;
-  onCreate: () => void;
 }) {
   const { t } = useTranslation();
   return (
-    <aside className="w-full border-b border-border bg-surface-elevated p-4 md:w-[300px] md:border-b-0 md:border-r">
-      <div className="flex items-center justify-between gap-3">
-        <h2 className="text-sm font-semibold">{t("sessions.title")}</h2>
-        <Button variant="outline" size="icon" onClick={onCreate}>
-          <Plus className="h-4 w-4" />
-        </Button>
-      </div>
-      <div className="mt-4">
-        <SearchInput value={search} onChange={onSearch} placeholder={t("sessions.searchSessions")} />
-      </div>
-      <div className="mt-4 space-y-2">
+    <Card className="min-w-0 overflow-hidden">
+      <CardHeader className="border-b border-border p-4">
+        <div className="flex min-w-0 items-center justify-between gap-3">
+          <CardTitle className="truncate">{t("sessions.sessionList")}</CardTitle>
+          <span className="shrink-0 rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
+            {sessions.length}
+          </span>
+        </div>
+        <SearchInput
+          className="mt-2 w-full sm:w-full"
+          value={search}
+          onChange={onSearch}
+          placeholder={t("sessions.searchSessions")}
+        />
+      </CardHeader>
+      <CardContent className="max-h-[640px] min-w-0 space-y-1 overflow-auto p-2">
         {sessions.map((session) => (
           <button
             key={session.id}
             onClick={() => onSelect(session.id)}
             className={cn(
-              "flex w-full items-start gap-3 rounded-md border border-border p-3 text-left hover:bg-muted",
+              "group flex min-w-0 w-full items-start gap-2 rounded-md border border-transparent p-2 text-left transition hover:border-border hover:bg-muted/60",
               activeSessionId === session.id && "border-primary/40 bg-primary/10")}
           >
-            <MessageSquare className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
-            <span className="min-w-0">
-              <span className="block truncate text-sm font-medium">{session.title ?? t("sessions.untitledSession")}</span>
-              <span className="block text-xs text-muted-foreground">
-                <RelativeTime value={session.last_active_time ?? session.created_time} />
+            <span className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">
+              <MessageSquare className="h-4 w-4" />
+            </span>
+            <span className="min-w-0 flex-1">
+              <span className="block truncate text-sm font-medium">{session.title || t("sessions.untitledSession")}</span>
+              <span className="mt-0.5 block truncate text-xs text-muted-foreground">
+                {formatChannel(session.channel_type)} / {formatDateTime(session.last_active_time ?? session.created_time)}
               </span>
             </span>
           </button>
         ))}
-      </div>
-    </aside>
+        {!sessions.length ? (
+          <div className="rounded-md border border-dashed border-border p-4 text-center text-sm text-muted-foreground">
+            {t("sessions.emptyDescription")}
+          </div>
+        ) : null}
+      </CardContent>
+    </Card>
   );
 }
+
+function formatChannel(value: string) {
+  return value.replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
+}

+ 297 - 347
web/src/pages/skills/SkillsPage.tsx

@@ -1,22 +1,25 @@
 import * as React from "react";
 import { useTranslation } from "react-i18next";
 import {
-  Cpu,
   FileText,
   Link2,
+  Pencil,
   Plus,
+  Puzzle,
   Search,
   Trash2,
-  X,
+  Wrench,
 } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import { Dialog } from "@/components/ui/dialog";
 import { EmptyState } from "@/components/shared/EmptyState";
-import { Input } from "@/components/ui/input";
+import { MetricCard } from "@/components/shared/MetricCard";
 import { PageHeader } from "@/components/shared/PageHeader";
+import { SearchInput } from "@/components/shared/SearchInput";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Dialog } from "@/components/ui/dialog";
+import { Input, Textarea } from "@/components/ui/input";
 import { Select } from "@/components/ui/select";
-import { Textarea } from "@/components/ui/input";
 import { toast } from "@/components/ui/toaster";
 
 type Tool = {
@@ -35,6 +38,14 @@ type Skill = {
   status: "active" | "draft";
 };
 
+type SkillFormState = {
+  name: string;
+  description: string;
+  instruction: string;
+  category: string;
+  selectedToolIds: string[];
+};
+
 const mockTools: Tool[] = [
   { id: "tool_1", name: "search_knowledge", description: "Search knowledge base" },
   { id: "tool_2", name: "query_database", description: "Query database" },
@@ -65,423 +76,362 @@ const mockSkills: Skill[] = [
   },
 ];
 
+const emptyForm: SkillFormState = {
+  name: "",
+  description: "",
+  instruction: "",
+  category: "service",
+  selectedToolIds: [],
+};
+
 export function SkillsPage() {
   const { t } = useTranslation();
   const [skills, setSkills] = React.useState<Skill[]>(mockSkills);
   const [tools] = React.useState<Tool[]>(mockTools);
-  const [selectedSkill, setSelectedSkill] = React.useState<Skill | null>(null);
   const [search, setSearch] = React.useState("");
   const [categoryFilter, setCategoryFilter] = React.useState("all");
   const [createOpen, setCreateOpen] = React.useState(false);
+  const [editOpen, setEditOpen] = React.useState(false);
+  const [editingSkill, setEditingSkill] = React.useState<Skill>();
+  const [form, setForm] = React.useState<SkillFormState>(emptyForm);
+
+  const categories = Array.from(new Set(skills.map((skill) => skill.category))).sort();
+  const boundToolsCount = new Set(skills.flatMap((skill) => skill.tools.map((tool) => tool.id))).size;
+  const filtered = skills
+    .filter((skill) => {
+      const text = `${skill.name} ${skill.description} ${skill.category} ${skill.tools.map((tool) => tool.name).join(" ")}`.toLowerCase();
+      return text.includes(search.toLowerCase()) && (categoryFilter === "all" || skill.category === categoryFilter);
+    })
+    .sort((first, second) => first.name.localeCompare(second.name));
 
-  const filtered = skills.filter((s) => {
-    const matchSearch = s.name.toLowerCase().includes(search.toLowerCase()) ||
-                       s.description.toLowerCase().includes(search.toLowerCase());
-    const matchCat = categoryFilter === "all" || s.category === categoryFilter;
-    return matchSearch && matchCat;
-  });
+  function openCreate() {
+    setForm(emptyForm);
+    setCreateOpen(true);
+  }
+
+  function openEdit(skill: Skill) {
+    setEditingSkill(skill);
+    setForm(fromSkill(skill));
+    setEditOpen(true);
+  }
 
-  function handleCreate(skill: Skill) {
-    setSkills([skill, ...skills]);
-    setSelectedSkill(skill);
+  function createSkill() {
+    if (!form.name.trim()) return;
+    const selectedTools = tools.filter((tool) => form.selectedToolIds.includes(tool.id));
+    const skill: Skill = {
+      id: `skill_${Date.now()}`,
+      name: form.name.trim(),
+      description: form.description.trim(),
+      instruction: form.instruction.trim(),
+      category: form.category,
+      tools: selectedTools,
+      status: "draft",
+    };
+    setSkills((current) => [skill, ...current]);
     setCreateOpen(false);
+    toast.success(t("skills.created"));
   }
 
-  function handleUpdate(updated: Skill) {
-    setSkills((prev) => prev.map((s) => (s.id === updated.id ? updated : s)));
-    setSelectedSkill(updated);
+  function updateSkill() {
+    if (!editingSkill || !form.name.trim()) return;
+    const selectedTools = tools.filter((tool) => form.selectedToolIds.includes(tool.id));
+    const updated: Skill = {
+      ...editingSkill,
+      name: form.name.trim(),
+      description: form.description.trim(),
+      instruction: form.instruction.trim(),
+      category: form.category,
+      tools: selectedTools,
+    };
+    setSkills((current) => current.map((skill) => (skill.id === updated.id ? updated : skill)));
+    setEditingSkill(updated);
+    setEditOpen(false);
+    toast.success(t("skills.saved"));
   }
 
-  function handleDelete(id: string) {
-    setSkills((prev) => prev.filter((s) => s.id !== id));
-    if (selectedSkill?.id === id) setSelectedSkill(null);
+  function deleteSkill(id: string) {
+    setSkills((current) => current.filter((skill) => skill.id !== id));
     toast.success(t("skills.deleted"));
   }
 
   return (
-    <div className="flex h-full">
-      <div className="flex-1 flex flex-col overflow-hidden">
-        <PageHeader
-          title={t("skills.title")}
-          description={t("skills.description")}
-          actions={
-            <Button onClick={() => setCreateOpen(true)}>
-              <Plus className="h-4 w-4" /> {t("skills.new")}
-            </Button>
-          }
-        />
+    <div className="space-y-6">
+      <PageHeader
+        title={t("skills.title")}
+        description={t("skills.description")}
+        actions={
+          <Button onClick={openCreate}>
+            <Plus className="h-4 w-4" /> {t("skills.new")}
+          </Button>
+        }
+      />
+
+      <div className="grid gap-4 md:grid-cols-3">
+        <MetricCard label={t("skills.title")} value={skills.length} icon={Puzzle} />
+        <MetricCard label={t("skills.toolsCount")} value={boundToolsCount} icon={Wrench} />
+        <MetricCard label={t("skills.category")} value={categories.length} icon={FileText} />
+      </div>
 
-        <div className="flex items-center gap-4 border-b px-6 py-3">
-          <div className="relative flex-1 max-w-md">
-            <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
-            <Input
-              value={search}
-              onChange={(e) => setSearch(e.target.value)}
-              placeholder={t("skills.search")}
-              className="pl-9"
+      <Card>
+        <CardHeader>
+          <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
+            <div>
+              <CardTitle>{t("skills.title")}</CardTitle>
+              <CardDescription>{filtered.length} / {skills.length}</CardDescription>
+            </div>
+            <Badge className="w-fit border-border bg-muted/50 text-muted-foreground">{t("skills.selectTools")}</Badge>
+          </div>
+        </CardHeader>
+        <CardContent className="space-y-4">
+          <div className="grid gap-3 lg:grid-cols-[1fr_220px]">
+            <SearchInput value={search} onChange={setSearch} placeholder={t("skills.search")} />
+            <Select
+              value={categoryFilter}
+              onChange={(event) => setCategoryFilter(event.target.value)}
+              options={[
+                { value: "all", label: t("skills.allCategories") },
+                ...categories.map((category) => ({ value: category, label: categoryLabel(category, t) })),
+              ]}
             />
           </div>
-          <Select
-            value={categoryFilter}
-            onChange={(e) => setCategoryFilter(e.target.value)}
-            options={[
-              { value: "all", label: t("skills.allCategories") },
-              { value: "service", label: t("skills.catService") },
-              { value: "analytics", label: t("skills.catAnalytics") },
-              { value: "development", label: t("skills.catDevelopment") },
-              { value: "processing", label: t("skills.catProcessing") },
-            ]}
-            className="w-40"
-          />
-        </div>
 
-        <div className="flex-1 overflow-auto p-6">
-          {filtered.length === 0 ? (
+          {filtered.length ? (
+            <div className="overflow-hidden rounded-md border border-border">
+              <div className="hidden grid-cols-[1.1fr_1.4fr_150px_140px_110px] gap-4 border-b border-border bg-muted/35 px-4 py-3 text-xs font-medium uppercase tracking-wide text-muted-foreground lg:grid">
+                <span>{t("common.name")}</span>
+                <span>{t("skills.instruction")}</span>
+                <span>{t("skills.category")}</span>
+                <span>{t("skills.toolsCount")}</span>
+                <span className="text-right">{t("common.actions")}</span>
+              </div>
+              <div className="divide-y divide-border">
+                {filtered.map((skill) => (
+                  <SkillRow
+                    key={skill.id}
+                    skill={skill}
+                    t={t}
+                    onEdit={() => openEdit(skill)}
+                    onDelete={() => deleteSkill(skill.id)}
+                  />
+                ))}
+              </div>
+            </div>
+          ) : (
             <EmptyState
-              icon={Cpu}
+              icon={Search}
               title={t("skills.empty")}
               description={t("skills.emptyHint")}
               actionLabel={t("skills.new")}
-              onAction={() => setCreateOpen(true)}
+              onAction={openCreate}
             />
-          ) : (
-            <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
-              {filtered.map((skill) => (
-                <Card
-                  key={skill.id}
-                  className={[
-                    "cursor-pointer transition-all hover:shadow-md",
-                    selectedSkill?.id === skill.id ? "ring-2 ring-primary" : "",
-                  ].join(" ")}
-                  onClick={() => setSelectedSkill(skill)}
-                >
-                  <CardContent className="p-4">
-                    <div className="flex items-start justify-between">
-                      <div className="flex items-center gap-3">
-                        <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
-                          <Cpu className="h-5 w-5 text-primary" />
-                        </div>
-                        <div>
-                          <h3 className="font-semibold">{skill.name}</h3>
-                          <p className="text-sm text-muted-foreground">{skill.description}</p>
-                        </div>
-                      </div>
-                      <Button
-                        variant="ghost"
-                        size="icon"
-                        className="h-8 w-8 -mr-2 -mt-1"
-                        onClick={(e) => {
-                          e.stopPropagation();
-                          handleDelete(skill.id);
-                        }}
-                      >
-                        <Trash2 className="h-4 w-4 text-destructive" />
-                      </Button>
-                    </div>
-                    <div className="mt-3 flex flex-wrap gap-1">
-                      <span className={`rounded-full px-2 py-0.5 text-xs ${skill.status === "active" ? "bg-emerald-100 text-emerald-700" : "bg-muted text-muted-foreground"}`}>
-                        {skill.status === "active" ? t("skills.active") : t("skills.draft")}
-                      </span>
-                      <span className="rounded-full bg-muted px-2 py-0.5 text-xs">
-                        {t(`skills.cat${skill.category.charAt(0).toUpperCase() + skill.category.slice(1)}`)}
-                      </span>
-                      <span className="rounded-full bg-muted px-2 py-0.5 text-xs flex items-center gap-1">
-                        <Link2 className="h-3 w-3" />
-                        {skill.tools.length} {t("skills.tools")}
-                      </span>
-                    </div>
-                  </CardContent>
-                </Card>
-              ))}
-            </div>
           )}
-        </div>
-      </div>
-
-      {selectedSkill && (
-        <SkillPanel
-          skill={selectedSkill}
-          tools={tools}
-          onUpdate={handleUpdate}
-          onClose={() => setSelectedSkill(null)}
-        />
-      )}
+        </CardContent>
+      </Card>
 
-      <CreateSkillDialog
+      <SkillDialog
         open={createOpen}
+        title={t("skills.new")}
+        form={form}
+        tools={tools}
+        submitLabel={t("common.create")}
         onOpenChange={setCreateOpen}
-        onCreated={handleCreate}
+        onChange={setForm}
+        onSubmit={createSkill}
+      />
+
+      <SkillDialog
+        open={editOpen}
+        title={editingSkill?.name ?? t("common.edit")}
+        form={form}
         tools={tools}
+        submitLabel={t("common.save")}
+        onOpenChange={setEditOpen}
+        onChange={setForm}
+        onSubmit={updateSkill}
       />
     </div>
   );
 }
 
-function SkillPanel({
+function SkillRow({
   skill,
-  tools,
-  onUpdate,
-  onClose,
+  t,
+  onEdit,
+  onDelete,
 }: {
   skill: Skill;
-  tools: Tool[];
-  onUpdate: (skill: Skill) => void;
-  onClose: () => void;
+  t: (key: string) => string;
+  onEdit: () => void;
+  onDelete: () => void;
 }) {
-  const { t } = useTranslation();
-  const [instruction, setInstruction] = React.useState(skill.instruction);
-  const [editing, setEditing] = React.useState(false);
-
-  React.useEffect(() => {
-    setInstruction(skill.instruction);
-    setEditing(false);
-  }, [skill]);
-
-  function handleSaveInstruction() {
-    onUpdate({ ...skill, instruction });
-    setEditing(false);
-    toast.success(t("skills.saved"));
-  }
-
-  function toggleTool(toolId: string) {
-    const has = skill.tools.some((t) => t.id === toolId);
-    const tool = tools.find((t) => t.id === toolId);
-    if (!tool) return;
-    const newTools = has
-      ? skill.tools.filter((t) => t.id !== toolId)
-      : [...skill.tools, tool];
-    onUpdate({ ...skill, tools: newTools });
-  }
-
   return (
-    <div className="w-[480px] border-l bg-background overflow-auto">
-      <div className="sticky top-0 z-10 flex items-center justify-between border-b bg-background px-6 py-4">
-        <h2 className="font-semibold">{skill.name}</h2>
-        <Button variant="ghost" size="icon" onClick={onClose}>
-          <X className="h-4 w-4" />
-        </Button>
-      </div>
-
-      <div className="space-y-6 p-6">
-        <div>
-          <div className="mb-2 flex items-center justify-between">
-            <label className="text-sm font-medium">{t("skills.instruction")}</label>
-            <Button
-              variant="ghost"
-              size="sm"
-              onClick={() => editing ? handleSaveInstruction() : setEditing(true)}
-            >
-              {editing ? t("common.save") : t("common.edit")}
-            </Button>
-          </div>
-          <p className="mb-2 text-xs text-muted-foreground">{t("skills.instructionHint")}</p>
-          {editing ? (
-            <Textarea
-              value={instruction}
-              onChange={(e) => setInstruction(e.target.value)}
-              className="min-h-[200px] font-mono text-sm"
-              placeholder={t("skills.instructionPlaceholder")}
-            />
-          ) : (
-            <div className={`rounded-md border p-4 text-sm ${skill.instruction ? "bg-muted/50" : "border-dashed"}`}>
-              {skill.instruction ? (
-                <pre className="whitespace-pre-wrap">{skill.instruction}</pre>
-              ) : (
-                <span className="text-muted-foreground">{t("skills.noInstruction")}</span>
-              )}
-            </div>
-          )}
-        </div>
-
-        <div>
-          <label className="mb-2 block text-sm font-medium">{t("skills.tools")}</label>
-          <p className="mb-2 text-xs text-muted-foreground">{t("skills.toolsHint")}</p>
-          <div className="space-y-2">
-            {tools.map((tool) => {
-              const isBound = skill.tools.some((t) => t.id === tool.id);
-              return (
-                <button
-                  key={tool.id}
-                  type="button"
-                  onClick={() => toggleTool(tool.id)}
-                  className={[
-                    "flex w-full items-center gap-3 rounded-md border p-3 text-left transition",
-                    isBound ? "border-primary/40 bg-primary/5" : "border-border hover:bg-muted"
-                  ].join(" ")}
-                >
-                  <div className={[
-                    "flex h-5 w-5 items-center justify-center rounded border",
-                    isBound ? "border-primary bg-primary text-primary-foreground" : "border-muted-foreground"
-                  ].join(" ")}>
-                    {isBound && <FileText className="h-3 w-3" />}
-                  </div>
-                  <div>
-                    <p className="text-sm font-medium">{tool.name}</p>
-                    <p className="text-xs text-muted-foreground">{tool.description}</p>
-                  </div>
-                </button>
-              );
-            })}
-          </div>
-        </div>
-
-        <div className="rounded-md border p-4">
-          <h4 className="mb-3 text-sm font-medium">{t("skills.info")}</h4>
-          <dl className="space-y-2 text-sm">
-            <div className="flex justify-between">
-              <dt className="text-muted-foreground">{t("common.status")}</dt>
-              <dd className={skill.status === "active" ? "text-emerald-600" : "text-muted-foreground"}>
-                {skill.status === "active" ? t("skills.active") : t("skills.draft")}
-              </dd>
-            </div>
-            <div className="flex justify-between">
-              <dt className="text-muted-foreground">{t("skills.category")}</dt>
-              <dd>{t(`skills.cat${skill.category.charAt(0).toUpperCase() + skill.category.slice(1)}`)}</dd>
-            </div>
-            <div className="flex justify-between">
-              <dt className="text-muted-foreground">{t("skills.toolsCount")}</dt>
-              <dd>{skill.tools.length}</dd>
-            </div>
-          </dl>
+    <div className="grid gap-3 px-4 py-4 lg:grid-cols-[1.1fr_1.4fr_150px_140px_110px] lg:items-center">
+      <div className="min-w-0">
+        <div className="flex items-center gap-2">
+          <Puzzle className="h-4 w-4 text-primary" />
+          <span className="truncate text-sm font-medium">{skill.name}</span>
         </div>
+        <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{skill.description || t("skills.emptyHint")}</p>
+      </div>
+      <p className="line-clamp-2 text-sm text-muted-foreground">{skill.instruction || t("skills.noInstruction")}</p>
+      <Badge className="w-fit border-border bg-muted/50 text-muted-foreground">{categoryLabel(skill.category, t)}</Badge>
+      <div className="flex flex-wrap gap-1.5">
+        <Badge className="gap-1 border-primary/20 bg-primary/10 text-primary">
+          <Link2 className="h-3.5 w-3.5" /> {skill.tools.length}
+        </Badge>
+        {skill.tools.slice(0, 1).map((tool) => (
+          <Badge key={tool.id} className="border-border bg-muted text-muted-foreground">{tool.name}</Badge>
+        ))}
+      </div>
+      <div className="flex items-center justify-start gap-1.5 lg:justify-end">
+        <Button size="icon" variant="ghost" onClick={onEdit} aria-label={`Edit ${skill.name}`}>
+          <Pencil className="h-4 w-4" />
+        </Button>
+        <Button size="icon" variant="ghost" className="text-destructive hover:text-destructive" onClick={onDelete} aria-label={`Delete ${skill.name}`}>
+          <Trash2 className="h-4 w-4" />
+        </Button>
       </div>
     </div>
   );
 }
 
-function CreateSkillDialog({
+function SkillDialog({
   open,
-  onOpenChange,
-  onCreated,
+  title,
+  form,
   tools,
+  submitLabel,
+  onOpenChange,
+  onChange,
+  onSubmit,
 }: {
   open: boolean;
-  onOpenChange: (open: boolean) => void;
-  onCreated: (skill: Skill) => void;
+  title: string;
+  form: SkillFormState;
   tools: Tool[];
+  submitLabel: string;
+  onOpenChange: (open: boolean) => void;
+  onChange: (form: SkillFormState) => void;
+  onSubmit: () => void;
 }) {
   const { t } = useTranslation();
-  const [name, setName] = React.useState("");
-  const [description, setDescription] = React.useState("");
-  const [instruction, setInstruction] = React.useState("");
-  const [category, setCategory] = React.useState("service");
-  const [selectedToolIds, setSelectedToolIds] = React.useState<string[]>([]);
-
-  React.useEffect(() => {
-    if (!open) {
-      setName("");
-      setDescription("");
-      setInstruction("");
-      setCategory("service");
-      setSelectedToolIds([]);
-    }
-  }, [open]);
-
-  function handleSubmit(e: React.FormEvent) {
-    e.preventDefault();
-    if (!name.trim()) return;
-    const selectedTools = tools.filter((t) => selectedToolIds.includes(t.id));
-    const newSkill: Skill = {
-      id: `skill_${Date.now()}`,
-      name: name.trim(),
-      description: description.trim(),
-      instruction: instruction.trim(),
-      category,
-      tools: selectedTools,
-      status: "draft",
-    };
-    onCreated(newSkill);
-    toast.success(t("skills.created"));
-  }
+  const set = (key: keyof SkillFormState, value: string | string[]) => onChange({ ...form, [key]: value });
 
   function toggleTool(toolId: string) {
-    if (selectedToolIds.includes(toolId)) {
-      setSelectedToolIds(selectedToolIds.filter((id) => id !== toolId));
-    } else {
-      setSelectedToolIds([...selectedToolIds, toolId]);
-    }
+    const selectedToolIds = form.selectedToolIds.includes(toolId)
+      ? form.selectedToolIds.filter((id) => id !== toolId)
+      : [...form.selectedToolIds, toolId];
+    set("selectedToolIds", selectedToolIds);
   }
 
   return (
-    <Dialog open={open} onOpenChange={onOpenChange} title={t("skills.new")}>
-      <form onSubmit={handleSubmit} className="space-y-4">
-        <div>
-          <label className="text-sm font-medium">{t("common.name")}</label>
-          <Input
-            value={name}
-            onChange={(e) => setName(e.target.value)}
-            placeholder={t("skills.namePlaceholder")}
-            className="mt-1"
-            autoFocus
-          />
-        </div>
-        <div>
-          <label className="text-sm font-medium">{t("common.description")}</label>
-          <Input
-            value={description}
-            onChange={(e) => setDescription(e.target.value)}
-            placeholder={t("skills.descPlaceholder")}
-            className="mt-1"
-          />
-        </div>
-        <div>
-          <label className="text-sm font-medium">{t("skills.category")}</label>
-          <Select
-            value={category}
-            onChange={(e) => setCategory(e.target.value)}
-            options={[
-              { value: "service", label: t("skills.catService") },
-              { value: "analytics", label: t("skills.catAnalytics") },
-              { value: "development", label: t("skills.catDevelopment") },
-              { value: "processing", label: t("skills.catProcessing") },
-            ]}
-            className="mt-1"
-          />
-        </div>
-        <div>
-          <label className="text-sm font-medium">{t("skills.instruction")}</label>
-          <Textarea
-            value={instruction}
-            onChange={(e) => setInstruction(e.target.value)}
-            placeholder={t("skills.instructionPlaceholder")}
-            className="mt-1"
-            rows={4}
-          />
+    <Dialog open={open} onOpenChange={onOpenChange} title={title} className="max-w-4xl">
+      <div className="space-y-5">
+        <div className="grid gap-4 md:grid-cols-2">
+          <Field label={t("common.name")}>
+            <Input value={form.name} onChange={(event) => set("name", event.target.value)} placeholder={t("skills.namePlaceholder")} />
+          </Field>
+          <Field label={t("skills.category")}>
+            <Select
+              value={form.category}
+              onChange={(event) => set("category", event.target.value)}
+              options={[
+                { value: "service", label: t("skills.catService") },
+                { value: "analytics", label: t("skills.catAnalytics") },
+                { value: "development", label: t("skills.catDevelopment") },
+                { value: "processing", label: t("skills.catProcessing") },
+              ]}
+            />
+          </Field>
+          <div className="md:col-span-2">
+            <Field label={t("common.description")}>
+              <Input value={form.description} onChange={(event) => set("description", event.target.value)} placeholder={t("skills.descPlaceholder")} />
+            </Field>
+          </div>
+          <div className="md:col-span-2">
+            <Field label={t("skills.instruction")}>
+              <Textarea
+                value={form.instruction}
+                onChange={(event) => set("instruction", event.target.value)}
+                placeholder={t("skills.instructionPlaceholder")}
+                className="min-h-36 font-mono"
+              />
+            </Field>
+          </div>
         </div>
+
         <div>
-          <label className="text-sm font-medium">{t("skills.selectTools")}</label>
-          <div className="mt-2 space-y-1 max-h-40 overflow-auto">
+          <div className="mb-2">
+            <p className="text-sm font-medium">{t("skills.selectTools")}</p>
+            <p className="text-xs text-muted-foreground">{t("skills.toolsHint")}</p>
+          </div>
+          <div className="grid max-h-60 gap-2 overflow-auto rounded-md border border-border p-2 sm:grid-cols-2">
             {tools.map((tool) => {
-              const isSelected = selectedToolIds.includes(tool.id);
+              const selected = form.selectedToolIds.includes(tool.id);
               return (
                 <button
                   key={tool.id}
                   type="button"
                   onClick={() => toggleTool(tool.id)}
                   className={[
-                    "flex w-full items-center gap-2 rounded-md border p-2 text-left transition",
-                    isSelected ? "border-primary/40 bg-primary/5" : "border-border hover:bg-muted"
+                    "rounded-md border p-3 text-left transition hover:bg-muted",
+                    selected ? "border-primary/40 bg-primary/10" : "border-border bg-muted/20",
                   ].join(" ")}
                 >
-                  <div className={[
-                    "h-4 w-4 rounded border",
-                    isSelected ? "border-primary bg-primary" : "border-muted-foreground"
-                  ].join(" ")} />
-                  <span className="text-sm">{tool.name}</span>
+                  <div className="flex items-start gap-3">
+                    <span className={[
+                      "mt-0.5 grid h-5 w-5 place-items-center rounded border",
+                      selected ? "border-primary bg-primary text-primary-foreground" : "border-muted-foreground/40",
+                    ].join(" ")}>
+                      {selected ? <FileText className="h-3 w-3" /> : null}
+                    </span>
+                    <span className="min-w-0">
+                      <span className="block truncate text-sm font-medium">{tool.name}</span>
+                      <span className="mt-1 block line-clamp-2 text-xs text-muted-foreground">{tool.description}</span>
+                    </span>
+                  </div>
                 </button>
               );
             })}
           </div>
         </div>
-        <div className="flex justify-end gap-2 pt-2">
-          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
-            {t("common.cancel")}
-          </Button>
-          <Button type="submit" disabled={!name.trim()}>
-            {t("common.create")}
-          </Button>
+
+        <div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
+          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>{t("common.cancel")}</Button>
+          <Button type="button" onClick={onSubmit} disabled={!form.name.trim()}>{submitLabel}</Button>
         </div>
-      </form>
+      </div>
     </Dialog>
   );
 }
+
+function Field({ label, children }: { label: string; children: React.ReactNode }) {
+  return (
+    <label className="block space-y-2 text-sm font-medium">
+      <span>{label}</span>
+      {children}
+    </label>
+  );
+}
+
+function fromSkill(skill: Skill): SkillFormState {
+  return {
+    name: skill.name,
+    description: skill.description,
+    instruction: skill.instruction,
+    category: skill.category,
+    selectedToolIds: skill.tools.map((tool) => tool.id),
+  };
+}
+
+function categoryLabel(category: string, t: (key: string) => string) {
+  return t(`skills.cat${formatCategoryKey(category)}`);
+}
+
+function formatCategoryKey(category: string) {
+  return category
+    .split(/[_-]+/)
+    .filter(Boolean)
+    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+    .join("");
+}

+ 110 - 77
web/src/pages/teams/components/CreateTeamDialog.tsx

@@ -1,8 +1,8 @@
 import * as React from "react";
 import { useTranslation } from "react-i18next";
-import { ListPlus, Minus, Settings2, Users } from "lucide-react";
+import { GitBranch, ListPlus, Minus, Plus, Settings2, Users } from "lucide-react";
 import { listAgents } from "@/api/agents";
-import { createTeam, createTeamVersion } from "@/api";
+import { createTeam, createTeamConfig } from "@/api";
 import { Button } from "@/components/ui/button";
 import { Dialog } from "@/components/ui/dialog";
 import { Input, Textarea } from "@/components/ui/input";
@@ -29,6 +29,10 @@ const DEFAULT_POLICY: PolicyDraft = {
   failure_mode: "stop_on_critical",
 };
 
+function createDefaultMember(): MemberDraft {
+  return { role: "worker", agent_id: "", responsibility: "" };
+}
+
 export function CreateTeamDialog({
   open,
   onOpenChange,
@@ -41,7 +45,7 @@ export function CreateTeamDialog({
   const { t } = useTranslation();
   const { userId } = useAuthStore();
   const [form, setForm] = React.useState({ name: "", description: "", coordination_mode: "supervisor", objective: "" });
-  const [members, setMembers] = React.useState<MemberDraft[]>([]);
+  const [members, setMembers] = React.useState<MemberDraft[]>([createDefaultMember()]);
   const [policy, setPolicy] = React.useState<PolicyDraft>(DEFAULT_POLICY);
   const [agents, setAgents] = React.useState<AgentDefinition[]>([]);
   const [formError, setFormError] = React.useState<string>();
@@ -54,7 +58,7 @@ export function CreateTeamDialog({
 
   function reset() {
     setForm({ name: "", description: "", coordination_mode: "supervisor", objective: "" });
-    setMembers([]);
+    setMembers([createDefaultMember()]);
     setPolicy(DEFAULT_POLICY);
     setFormError(undefined);
   }
@@ -63,22 +67,22 @@ export function CreateTeamDialog({
     event.preventDefault();
     if (!form.name.trim()) return;
 
-    const versionPayload = buildVersionConfig(members, policy, t);
-    if (!versionPayload.ok) {
-      setFormError(versionPayload.message);
+    const configPayload = buildTeamConfig(members, policy, t);
+    if (!configPayload.ok) {
+      setFormError(configPayload.message);
       return;
     }
 
     setSubmitting(true);
     setFormError(undefined);
     try {
-      const team = await createTeam({ name: form.name, code: "", description: form.description || undefined, team_type: "collaborative", owner_user_id: userId });
-      await createTeamVersion({
+      const team = await createTeam({ name: form.name, description: form.description || undefined, team_type: "collaborative", owner_user_id: userId });
+      await createTeamConfig({
         team_id: team.id,
         coordination_mode: form.coordination_mode,
         objective: form.objective || undefined,
-        member_refs: versionPayload.memberRefs,
-        policy_json: versionPayload.policyJson,
+        member_refs: configPayload.memberRefs,
+        policy_json: configPayload.policyJson,
         status: "draft",
       });
       toast.success(t("teams.teamCreated"));
@@ -93,47 +97,59 @@ export function CreateTeamDialog({
   }
 
   return (
-    <Dialog open={open} onOpenChange={(value) => { if (!value) reset(); onOpenChange(value); }} title={t("teams.newTeam")} className="max-w-5xl">
-      <form className="space-y-5" onSubmit={submit}>
-        {/* Top row: basic info (left) + members (right) */}
-        <div className="grid gap-6 lg:grid-cols-[1fr_1fr]">
-          {/* Left: basic info */}
+    <Dialog
+      open={open}
+      onOpenChange={(value) => { if (!value) reset(); onOpenChange(value); }}
+      title={t("teams.newTeam")}
+      description={t("teams.createTeamDefineObjective")}
+      className="max-w-6xl"
+    >
+      <form className="space-y-4" onSubmit={submit}>
+        <div className="grid gap-3 rounded-md border border-border bg-muted/20 p-3 sm:grid-cols-3">
+          <SummaryPill icon={<GitBranch className="h-4 w-4" />} label={t("teams.coordination")} value={t(`teams.${form.coordination_mode}`)} />
+          <SummaryPill icon={<Users className="h-4 w-4" />} label={t("teams.members")} value={String(members.length)} />
+          <SummaryPill icon={<Settings2 className="h-4 w-4" />} label={t("teams.maxRoundsField")} value={policy.max_rounds} />
+        </div>
+
+        <div className="grid gap-4 xl:grid-cols-[0.95fr_1.25fr]">
           <div className="space-y-4">
-            <SectionHeader icon={<ListPlus className="h-4 w-4" />} title={t("teams.basicInfo")} />
-            <div className="space-y-4">
-              <Field label={t("common.name")}>
-                <Input required value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} />
-              </Field>
-              <Field label={t("teams.coordinationMode")}>
-                <Select
-                  value={form.coordination_mode}
-                  onChange={(event) => setForm({ ...form, coordination_mode: event.target.value })}
-                  options={[
-                    { value: "supervisor", label: t("teams.supervisor") },
-                    { value: "pipeline", label: t("teams.pipeline") },
-                    { value: "parallel", label: t("teams.parallel") },
-                    { value: "debate", label: t("teams.debate") },
-                  ]}
-                />
-              </Field>
-              <Field label={t("common.description")}>
-                <Textarea value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} />
-              </Field>
-              <Field label={t("teams.objective")}>
-                <Textarea value={form.objective} placeholder={t("teams.describeVersion")} onChange={(event) => setForm({ ...form, objective: event.target.value })} />
-              </Field>
-            </div>
+            <section className="rounded-md border border-border bg-muted/10 p-4">
+              <SectionHeader icon={<ListPlus className="h-4 w-4" />} title={t("teams.basicInfo")} />
+              <div className="mt-4 space-y-4">
+                <div className="grid gap-4 sm:grid-cols-[1fr_190px]">
+                  <Field label={t("common.name")}>
+                    <Input required value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} />
+                  </Field>
+                  <Field label={t("teams.coordinationMode")}>
+                    <Select
+                      value={form.coordination_mode}
+                      onChange={(event) => setForm({ ...form, coordination_mode: event.target.value })}
+                      options={[
+                        { value: "supervisor", label: t("teams.supervisor") },
+                        { value: "pipeline", label: t("teams.pipeline") },
+                        { value: "parallel", label: t("teams.parallel") },
+                        { value: "debate", label: t("teams.debate") },
+                      ]}
+                    />
+                  </Field>
+                </div>
+                <Field label={t("common.description")}>
+                  <Textarea className="min-h-20" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} />
+                </Field>
+                <Field label={t("teams.objective")}>
+                  <Textarea className="min-h-24" value={form.objective} placeholder={t("teams.describeObjective")} onChange={(event) => setForm({ ...form, objective: event.target.value })} />
+                </Field>
+              </div>
+            </section>
+
+            <PolicyEditor policy={policy} onChange={setPolicy} />
           </div>
 
-          {/* Right: members */}
           <MemberEditor members={members} onChange={setMembers} agents={agents} />
         </div>
 
-        {/* Bottom: policy (full width) */}
-        <PolicyEditor policy={policy} onChange={setPolicy} />
-
         {formError ? <p className="rounded-md border border-red-500/25 bg-red-500/5 p-3 text-sm text-foreground">{formError}</p> : null}
-        <div className="flex justify-end gap-2 pt-1">
+        <div className="sticky bottom-0 -mx-4 -mb-4 flex justify-end gap-2 border-t border-border bg-surface-elevated/95 px-4 py-4 backdrop-blur sm:-mx-5 sm:-mb-5 sm:px-5">
           <Button type="button" variant="ghost" onClick={() => { reset(); onOpenChange(false); }}>
             {t("common.cancel")}
           </Button>
@@ -156,28 +172,42 @@ function MemberEditor({ members, onChange, agents }: { members: MemberDraft[]; o
   const agentOptions = React.useMemo(
     () => [
       { value: "", label: t("teams.selectAgent") },
-      ...agents.map((a) => ({ value: a.id, label: `${a.name} (${a.code})` })),
+      ...agents.map((a) => ({ value: a.id, label: a.name })),
     ],
     [agents, t],
   );
 
   return (
-    <section className="space-y-4">
+    <section className="space-y-4 rounded-md border border-border bg-muted/10 p-4">
       <SectionHeader
         icon={<Users className="h-4 w-4" />}
         title={t("teams.members")}
         action={
-          <Button type="button" size="sm" variant="outline" onClick={() => onChange([...members, { role: "worker", agent_id: "", responsibility: "" }])}>
-            <span className="mr-1">+</span> {t("teams.addMember")}
+          <Button type="button" size="sm" variant="outline" onClick={() => onChange([...members, createDefaultMember()])}>
+            <Plus className="h-4 w-4" /> {t("teams.addMember")}
           </Button>
         }
       />
       {members.length ? (
-        <div className="space-y-3">
+        <div className="max-h-[520px] space-y-3 overflow-auto pr-1">
           {members.map((member, index) => (
-            <div key={index} className="rounded-md border border-border bg-muted/30 px-4 py-3">
-              <div className="flex items-center gap-3">
-                <div className="w-28 shrink-0">
+            <div key={index} className="rounded-md border border-border bg-surface-elevated px-4 py-3 shadow-sm">
+              <div className="mb-3 flex items-center justify-between gap-3">
+                <p className="text-sm font-medium">{t("teams.member")} {index + 1}</p>
+                <Button
+                  type="button"
+                  size="icon"
+                  variant="ghost"
+                  className="h-8 w-8 shrink-0 text-muted-foreground hover:text-red-500"
+                  disabled={members.length <= 1}
+                  onClick={() => remove(index)}
+                  aria-label={t("teams.remove")}
+                >
+                  <Minus className="h-4 w-4" />
+                </Button>
+              </div>
+              <div className="grid gap-3 lg:grid-cols-[180px_1fr]">
+                <Field label={t("teams.role")}>
                   <Select
                     aria-label={`${t("teams.role")} ${index + 1}`}
                     value={member.role}
@@ -189,34 +219,25 @@ function MemberEditor({ members, onChange, agents }: { members: MemberDraft[]; o
                       { value: "planner", label: t("teams.planner") },
                     ]}
                   />
-                </div>
-                <div className="min-w-0 flex-1">
+                </Field>
+                <Field label={t("teams.selectAgent")}>
                   <Select
                     aria-label={`${t("teams.agent")} ${index + 1}`}
                     value={member.agent_id}
                     onChange={(event) => update(index, { agent_id: event.target.value })}
                     options={agentOptions}
                   />
-                </div>
-                <Button
-                  type="button"
-                  size="icon"
-                  variant="ghost"
-                  className="h-8 w-8 shrink-0 text-muted-foreground hover:text-red-500"
-                  disabled={members.length <= 1}
-                  onClick={() => remove(index)}
-                  aria-label={t("teams.remove")}
-                >
-                  <Minus className="h-4 w-4" />
-                </Button>
+                </Field>
               </div>
-              <Textarea
-                className="mt-3 min-h-14"
-                aria-label={`${t("teams.responsibility")} ${index + 1}`}
-                value={member.responsibility}
-                placeholder={t("teams.responsibility")}
-                onChange={(event) => update(index, { responsibility: event.target.value })}
-              />
+              <Field label={t("teams.responsibility")}>
+                <Textarea
+                  className="min-h-16"
+                  aria-label={`${t("teams.responsibility")} ${index + 1}`}
+                  value={member.responsibility}
+                  placeholder={t("teams.responsibility")}
+                  onChange={(event) => update(index, { responsibility: event.target.value })}
+                />
+              </Field>
             </div>
           ))}
         </div>
@@ -232,9 +253,9 @@ function MemberEditor({ members, onChange, agents }: { members: MemberDraft[]; o
 function PolicyEditor({ policy, onChange }: { policy: PolicyDraft; onChange: (policy: PolicyDraft) => void }) {
   const { t } = useTranslation();
   return (
-    <section className="space-y-4">
+    <section className="space-y-4 rounded-md border border-border bg-muted/10 p-4">
       <SectionHeader icon={<Settings2 className="h-4 w-4" />} title={t("teams.policy")} />
-      <div className="grid gap-4 sm:grid-cols-3">
+      <div className="grid gap-4">
         <Field label={t("teams.maxRoundsField")}>
           <Input type="number" min={1} max={20} value={policy.max_rounds} onChange={(event) => onChange({ ...policy, max_rounds: event.target.value })} />
         </Field>
@@ -277,6 +298,18 @@ function SectionHeader({ icon, title, action }: { icon: React.ReactNode; title:
   );
 }
 
+function SummaryPill({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
+  return (
+    <div className="flex items-center gap-3 rounded-md border border-border bg-surface-elevated px-3 py-2">
+      <div className="grid h-8 w-8 shrink-0 place-items-center rounded bg-primary/15 text-primary">{icon}</div>
+      <div className="min-w-0">
+        <p className="text-xs text-muted-foreground">{label}</p>
+        <p className="truncate text-sm font-medium">{value}</p>
+      </div>
+    </div>
+  );
+}
+
 function Field({ label, children }: { label: string; children: React.ReactNode }) {
   return (
     <label className="block space-y-1.5 text-sm">
@@ -286,7 +319,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
   );
 }
 
-function buildVersionConfig(
+function buildTeamConfig(
   members: MemberDraft[],
   policy: PolicyDraft,
   t: (key: string) => string,

+ 101 - 74
web/src/pages/teams/components/TeamOverview.tsx

@@ -1,116 +1,143 @@
+import type { ReactNode } from "react";
 import { useTranslation } from "react-i18next";
+import { Activity, AlertTriangle, CalendarClock, GitBranch, ShieldCheck, Target, UserRound, Users } from "lucide-react";
 import { Badge } from "@/components/ui/badge";
-import type { JSONObject, TeamDefinition, TeamRun, TeamVersion } from "@/types";
-import { formatDateTime, relativeTime } from "@/lib/utils";
+import type { JSONObject, TeamConfig, TeamDefinition, TeamRun } from "@/types";
+import { formatDateTime } from "@/lib/utils";
 
 export function TeamOverview({
   team,
-  latestVersion,
-  versionCount,
+  activeConfig,
   runCount,
   failedRunCount,
   latestRun,
 }: {
   team: TeamDefinition;
-  latestVersion?: TeamVersion;
-  versionCount: number;
+  activeConfig?: TeamConfig;
   runCount: number;
   failedRunCount: number;
   latestRun?: TeamRun;
 }) {
   const { t } = useTranslation();
+  const memberCount = activeConfig?.member_refs_json.length ?? 0;
   return (
-    <div className="space-y-6">
-      {/* Summary tiles */}
-      <div className="grid gap-4 md:grid-cols-4">
-        <SummaryTile label={t("common.versions")} value={versionCount} />
-        <SummaryTile label={t("common.runs")} value={runCount} />
-        <SummaryTile label={t("teams.failedRuns")} value={failedRunCount} />
-        <SummaryTile label={t("teams.latest")} value={latestVersion ? `v${latestVersion.version_no}` : t("teams.none")} />
+    <div className="min-w-0 space-y-4">
+      <div className="grid min-w-0 gap-3 md:grid-cols-4">
+        <Signal icon={<Activity className="h-4 w-4" />} label={t("common.runs")} value={runCount} />
+        <Signal icon={<AlertTriangle className="h-4 w-4" />} label={t("teams.failedRuns")} value={failedRunCount} />
+        <Signal icon={<Users className="h-4 w-4" />} label={t("teams.members")} value={memberCount} />
+        <Signal icon={<CalendarClock className="h-4 w-4" />} label={t("teams.lastRun")} value={latestRun ? formatDateTime(latestRun.created_time) : t("teams.noRuns")} />
       </div>
 
-      {/* Team details */}
-      <div className="grid gap-4 text-sm md:grid-cols-2">
-        <Detail label={t("common.code")} value={team.code} mono />
-        <Detail label={t("common.type")} value={readableLabel(team.team_type)} />
-        <Detail label={t("common.created")} value={formatDateTime(team.created_time)} />
-        <Detail label={t("teams.lastRun")} value={latestRun ? relativeTime(latestRun.created_time) : t("teams.noRuns")} />
-        <div className="md:col-span-2">
-          <p className="text-muted-foreground">{t("common.description")}</p>
-          <p className="mt-1 leading-6">{team.description ?? t("teams.noDescription")}</p>
-        </div>
-      </div>
-
-      {/* Current version config */}
-      {latestVersion ? (
-        <div className="space-y-4">
-          <SectionTitle>{t("teams.currentObjective")}</SectionTitle>
-          <p className="text-sm leading-6">{latestVersion.objective ?? t("teams.noObjectiveProvided")}</p>
-
-          {/* Coordination & Policy */}
-          <div className="grid gap-4 md:grid-cols-3">
-            <div className="rounded-md border border-border bg-muted/30 p-4">
-              <p className="text-xs text-muted-foreground">{t("teams.coordination")}</p>
-              <p className="mt-2 text-lg font-semibold">{readableLabel(latestVersion.coordination_mode)}</p>
-            </div>
-            <div className="rounded-md border border-border bg-muted/30 p-4">
-              <p className="text-xs text-muted-foreground">{t("teams.maxRoundsField")}</p>
-              <p className="mt-2 text-lg font-semibold">{getJsonString(latestVersion.policy_json, "max_rounds") ?? t("teams.none")}</p>
-            </div>
-            <div className="rounded-md border border-border bg-muted/30 p-4">
-              <p className="text-xs text-muted-foreground">{t("teams.handoff")}</p>
-              <p className="mt-2 text-lg font-semibold">{readableLabel(getJsonString(latestVersion.policy_json, "handoff") ?? t("teams.none"))}</p>
+      <section className="min-w-0 rounded-md border border-border bg-surface-base p-4">
+        <div className="flex min-w-0 items-start gap-3">
+          <div className="grid h-10 w-10 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">
+            <Target className="h-5 w-5" />
+          </div>
+          <div className="min-w-0 flex-1">
+            <div className="flex min-w-0 flex-wrap items-center gap-2">
+              <h3 className="truncate text-sm font-semibold">{t("teams.currentObjective")}</h3>
+              <Badge className="border-border bg-muted/40 text-muted-foreground">{readableLabel(activeConfig?.coordination_mode ?? team.team_type)}</Badge>
             </div>
+            <p className="mt-2 break-words text-sm leading-6 text-muted-foreground">
+              {activeConfig?.objective || team.description || t("teams.noObjectiveProvided")}
+            </p>
           </div>
+        </div>
+      </section>
+
+      {activeConfig ? (
+        <div className="grid min-w-0 gap-4 xl:grid-cols-[minmax(0,1fr)_280px]">
+          <section className="min-w-0 rounded-md border border-border bg-surface-base p-4">
+            <SectionHeader icon={<Users className="h-4 w-4" />} title={t("teams.members")} />
+            {activeConfig.member_refs_json.length ? (
+              <div className="mt-3 grid min-w-0 gap-2">
+                {activeConfig.member_refs_json.map((member, index) => (
+                  <MemberCard key={index} member={member} index={index} />
+                ))}
+              </div>
+            ) : (
+              <p className="mt-3 text-sm text-muted-foreground">{t("teams.noMembersConfigured")}</p>
+            )}
+          </section>
 
-          {/* Members */}
-          <SectionTitle>{t("teams.members")}</SectionTitle>
-          {latestVersion.member_refs_json.length ? (
-            <div className="grid gap-3 md:grid-cols-2">
-              {latestVersion.member_refs_json.map((member, index) => (
-                <div key={index} className="rounded-md border border-border bg-muted/30 p-4">
-                  <div className="flex items-start justify-between gap-3">
-                    <div className="min-w-0">
-                      <p className="truncate text-sm font-semibold">{getJsonString(member, "name") ?? getJsonString(member, "agent_name") ?? `${t("teams.member")} ${index + 1}`}</p>
-                      <p className="mt-1 truncate font-mono text-xs text-muted-foreground">{getJsonString(member, "agent_id") ?? getJsonString(member, "id") ?? t("teams.unboundAgent")}</p>
-                    </div>
-                    <Badge className="border-border bg-muted/60 text-muted-foreground">{getJsonString(member, "role") ?? t("teams.member")}</Badge>
-                  </div>
-                  <p className="mt-3 text-sm leading-6 text-muted-foreground">{getJsonString(member, "description") ?? getJsonString(member, "responsibility") ?? t("teams.noResponsibilitySummary")}</p>
-                </div>
-              ))}
+          <section className="min-w-0 space-y-3 rounded-md border border-border bg-muted/20 p-4">
+            <SectionHeader icon={<GitBranch className="h-4 w-4" />} title={t("teams.policy")} />
+            <PolicyRow icon={<GitBranch className="h-4 w-4" />} label={t("teams.coordination")} value={readableLabel(activeConfig.coordination_mode)} />
+            <PolicyRow icon={<Activity className="h-4 w-4" />} label={t("teams.maxRoundsField")} value={getJsonString(activeConfig.policy_json, "max_rounds") ?? t("teams.none")} />
+            <PolicyRow icon={<ShieldCheck className="h-4 w-4" />} label={t("teams.handoff")} value={readableLabel(getJsonString(activeConfig.policy_json, "handoff") ?? t("teams.none"))} />
+            <div className="rounded-md border border-border bg-surface-elevated p-3">
+              <p className="text-xs text-muted-foreground">{t("common.created")}</p>
+              <p className="mt-1 truncate text-sm font-medium">{formatDateTime(activeConfig.created_time)}</p>
             </div>
-          ) : (
-            <p className="text-sm text-muted-foreground">{t("teams.noMembersConfigured")}</p>
-          )}
+          </section>
         </div>
       ) : (
-        <p className="text-sm text-muted-foreground">{t("teams.createVersionDefine")}</p>
+        <div className="rounded-md border border-dashed border-border bg-muted/20 p-5 text-sm text-muted-foreground">
+          {t("teams.noTeamConfiguration")}
+        </div>
       )}
     </div>
   );
 }
 
-function SummaryTile({ label, value }: { label: string; value: string | number }) {
+function Signal({ icon, label, value }: { icon: ReactNode; label: string; value: string | number }) {
   return (
-    <div className="rounded-md border border-border bg-muted/30 p-3">
-      <p className="text-xs text-muted-foreground">{label}</p>
-      <p className="mt-1 text-xl font-semibold tabular-nums">{value}</p>
+    <div className="min-w-0 rounded-md border border-border bg-muted/25 p-3">
+      <div className="flex min-w-0 items-center gap-2 text-muted-foreground">
+        <span className="shrink-0 text-primary">{icon}</span>
+        <span className="truncate text-xs">{label}</span>
+      </div>
+      <p className="mt-2 truncate text-xl font-semibold tabular-nums">{value}</p>
     </div>
   );
 }
 
-function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
+function MemberCard({ member, index }: { member: JSONObject; index: number }) {
+  const { t } = useTranslation();
+  const name = getJsonString(member, "name") ?? getJsonString(member, "agent_name") ?? `${t("teams.member")} ${index + 1}`;
+  const role = getJsonString(member, "role") ?? t("teams.member");
+  const responsibility = getJsonString(member, "description") ?? getJsonString(member, "responsibility") ?? t("teams.noResponsibilitySummary");
   return (
-    <div className="min-w-0">
-      <p className="text-muted-foreground">{label}</p>
-      <p className={mono ? "mt-1 truncate font-mono text-xs" : "mt-1 truncate"}>{value}</p>
+    <div className="min-w-0 rounded-md border border-border bg-muted/20 p-3">
+      <div className="flex min-w-0 items-start justify-between gap-3">
+        <div className="flex min-w-0 items-start gap-2">
+          <div className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">
+            <UserRound className="h-4 w-4" />
+          </div>
+          <div className="min-w-0">
+            <p className="truncate text-sm font-semibold">{name}</p>
+            <p className="mt-0.5 truncate text-xs text-muted-foreground">
+              {getJsonString(member, "agent_id") ?? getJsonString(member, "id") ?? t("teams.unboundAgent")}
+            </p>
+          </div>
+        </div>
+        <Badge className="shrink-0 border-border bg-surface-elevated text-muted-foreground">{readableLabel(role)}</Badge>
+      </div>
+      <p className="mt-2 line-clamp-2 break-words text-sm leading-6 text-muted-foreground">{responsibility}</p>
+    </div>
+  );
+}
+
+function PolicyRow({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
+  return (
+    <div className="flex min-w-0 items-center gap-3 rounded-md border border-border bg-surface-elevated p-3">
+      <span className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">{icon}</span>
+      <div className="min-w-0">
+        <p className="truncate text-xs text-muted-foreground">{label}</p>
+        <p className="mt-0.5 truncate text-sm font-medium">{value}</p>
+      </div>
     </div>
   );
 }
 
-function SectionTitle({ children }: { children: React.ReactNode }) {
-  return <h3 className="text-sm font-semibold">{children}</h3>;
+function SectionHeader({ icon, title }: { icon: ReactNode; title: string }) {
+  return (
+    <div className="flex min-w-0 items-center gap-2">
+      <span className="grid h-7 w-7 shrink-0 place-items-center rounded-md bg-primary/10 text-primary">{icon}</span>
+      <h3 className="truncate text-sm font-semibold">{title}</h3>
+    </div>
+  );
 }
 
 function readableLabel(value: string) {

+ 34 - 52
web/src/pages/teams/components/TeamRuns.tsx

@@ -9,20 +9,20 @@ import { Button } from "@/components/ui/button";
 import { Select } from "@/components/ui/select";
 import { Textarea } from "@/components/ui/input";
 import { toast } from "@/components/ui/toaster";
-import type { TeamRun, TeamRunStatus, TeamVersion } from "@/types";
+import type { TeamConfig, TeamRun, TeamRunStatus } from "@/types";
 import { relativeTime, truncateMiddle } from "@/lib/utils";
 
 type RunStatusFilter = "all" | TeamRunStatus;
 
 export function TeamRuns({
   teamId,
-  versions,
+  configs,
   runs,
   loading,
   onRunCreated,
 }: {
   teamId: string;
-  versions: TeamVersion[];
+  configs: TeamConfig[];
   runs: TeamRun[];
   loading: boolean;
   onRunCreated: (run: TeamRun) => void;
@@ -30,22 +30,17 @@ export function TeamRuns({
   const { t } = useTranslation();
   const [statusFilter, setStatusFilter] = React.useState<RunStatusFilter>("all");
   const [inputText, setInputText] = React.useState("");
-  const [versionId, setVersionId] = React.useState(versions[0]?.id ?? "");
   const [submitting, setSubmitting] = React.useState(false);
 
-  const latestVersion = versions[0];
+  const activeConfig = configs[0];
   const filteredRuns = runs.filter((run) => statusFilter === "all" || run.status === statusFilter);
 
-  React.useEffect(() => {
-    setVersionId((current) => (versions.some((v) => v.id === current) ? current : versions[0]?.id ?? ""));
-  }, [versions]);
-
   async function startRun(event: React.FormEvent) {
     event.preventDefault();
-    if (!versionId || !inputText.trim()) return;
+    if (!activeConfig?.id || !inputText.trim()) return;
     setSubmitting(true);
     try {
-      const run = await createTeamRun({ team_id: teamId, team_version_id: versionId, input_text: inputText });
+      const run = await createTeamRun({ team_id: teamId, team_version_id: activeConfig.id, input_text: inputText });
       toast.success(t("teams.teamRunStarted"));
       setInputText("");
       onRunCreated(run);
@@ -58,34 +53,34 @@ export function TeamRuns({
 
   if (loading) return <LoadingSpinner label={t("common.loading")} />;
 
-  if (!latestVersion) {
-    return <EmptyState icon={Play} title={t("teams.noVersion")} description={t("teams.createVersionDefine")} />;
+  if (!activeConfig) {
+    return <EmptyState icon={Play} title="No team configuration" description="Create or save this team before starting a run." />;
   }
 
   return (
-    <div className="space-y-6">
-      {/* Inline start run form */}
-      <form className="space-y-3 rounded-md border border-border bg-muted/30 p-4" onSubmit={startRun}>
-        <h3 className="text-sm font-semibold">{t("teams.startRun")}</h3>
-        {versions.length > 1 && (
-          <Select
-            value={versionId}
-            onChange={(event) => setVersionId(event.target.value)}
-            options={versions.map((v) => ({ value: v.id, label: `v${v.version_no} - ${readableLabel(v.coordination_mode)}` }))}
-          />
-        )}
-        <Textarea required className="min-h-24" value={inputText} placeholder={t("teams.runInput")} onChange={(event) => setInputText(event.target.value)} />
-        <Button size="sm" disabled={submitting || !inputText.trim()}>
+    <aside className="min-w-0 space-y-4">
+      <form className="min-w-0 space-y-3 rounded-md border border-border bg-surface-base p-4" onSubmit={startRun}>
+        <div className="flex items-center justify-between gap-3">
+          <h3 className="truncate text-sm font-semibold">{t("teams.startRun")}</h3>
+          <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">{runs.length}</span>
+        </div>
+        <Textarea
+          required
+          className="min-h-24"
+          value={inputText}
+          placeholder={t("teams.runInput")}
+          onChange={(event) => setInputText(event.target.value)}
+        />
+        <Button className="w-full" size="sm" disabled={submitting || !inputText.trim()}>
           <Play className="h-4 w-4" /> {submitting ? t("teams.starting") : t("teams.startRun")}
         </Button>
       </form>
 
-      {/* Run history */}
-      <div className="space-y-3">
-        <div className="flex items-center justify-between gap-3">
-          <h3 className="text-sm font-semibold">{t("common.runs")}</h3>
+      <div className="min-w-0 space-y-3 rounded-md border border-border bg-muted/20 p-4">
+        <div className="flex min-w-0 items-center justify-between gap-3">
+          <h3 className="truncate text-sm font-semibold">{t("common.runs")}</h3>
           <Select
-            className="w-48"
+            className="w-36 shrink-0"
             aria-label={t("teams.allRunStatuses")}
             value={statusFilter}
             onChange={(event) => setStatusFilter(event.target.value as RunStatusFilter)}
@@ -100,29 +95,19 @@ export function TeamRuns({
           />
         </div>
         {filteredRuns.length ? (
-          <div className="space-y-2">
+          <div className="max-h-[520px] space-y-2 overflow-auto pr-1">
             {filteredRuns.map((run) => (
-              <div key={run.id} className="rounded-md border border-border bg-muted/30 p-4">
-                <div className="flex flex-wrap items-center justify-between gap-3">
+              <div key={run.id} className="min-w-0 rounded-md border border-border bg-surface-elevated p-3">
+                <div className="flex min-w-0 items-start justify-between gap-3">
                   <div className="min-w-0">
                     <p className="truncate font-mono text-xs">{truncateMiddle(run.id, 32)}</p>
-                    <p className="mt-1 truncate text-sm text-muted-foreground">{run.input_text ?? t("teams.structuredInput")}</p>
+                    <p className="mt-1 line-clamp-2 break-words text-sm leading-5 text-muted-foreground">{run.input_text ?? t("teams.structuredInput")}</p>
                   </div>
                   <StatusBadge status={run.status} />
                 </div>
-                <div className="mt-3 grid gap-3 text-sm md:grid-cols-3">
-                  <div className="min-w-0">
-                    <p className="text-muted-foreground">{t("common.created")}</p>
-                    <p className="mt-1">{relativeTime(run.created_time)}</p>
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-muted-foreground">{t("common.version")}</p>
-                    <p className="mt-1 truncate font-mono text-xs">{truncateMiddle(run.team_version_id, 24)}</p>
-                  </div>
-                  <div className="min-w-0">
-                    <p className="text-muted-foreground">{t("agents.output")}</p>
-                    <p className="mt-1 truncate">{run.output_text ?? t("teams.noOutputYet")}</p>
-                  </div>
+                <div className="mt-3 min-w-0 border-t border-border pt-3 text-sm">
+                  <p className="text-xs text-muted-foreground">{relativeTime(run.created_time)}</p>
+                  <p className="mt-1 line-clamp-2 break-words text-muted-foreground">{run.output_text ?? t("teams.noOutputYet")}</p>
                 </div>
               </div>
             ))}
@@ -131,10 +116,7 @@ export function TeamRuns({
           <EmptyState icon={Activity} title={t("teams.noRuns")} description={t("teams.noRunRecords")} />
         )}
       </div>
-    </div>
+    </aside>
   );
 }
 
-function readableLabel(value: string) {
-  return value.split(/[_-]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
-}

+ 0 - 57
web/src/pages/teams/components/TeamVersions.tsx

@@ -1,57 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { FileCode2 } from "lucide-react";
-import { EmptyState } from "@/components/shared/EmptyState";
-import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
-import { StatusBadge } from "@/components/shared/StatusBadge";
-import { Badge } from "@/components/ui/badge";
-import type { TeamVersion } from "@/types";
-import { formatDateTime, truncateMiddle } from "@/lib/utils";
-
-export function TeamVersions({ versions, loading }: { versions: TeamVersion[]; loading: boolean }) {
-  const { t } = useTranslation();
-  if (loading) return <LoadingSpinner label={t("common.loading")} />;
-
-  const sorted = [...versions].sort((a, b) => b.version_no - a.version_no);
-
-  if (!sorted.length) {
-    return <EmptyState icon={FileCode2} title={t("teams.noVersion")} description={t("teams.createVersionDefine")} />;
-  }
-
-  return (
-    <div className="space-y-3">
-      {sorted.map((version) => (
-        <div key={version.id} className="rounded-md border border-border bg-muted/30 p-4">
-          <div className="flex flex-wrap items-start justify-between gap-3">
-            <div>
-              <div className="flex flex-wrap items-center gap-2">
-                <p className="font-medium">v{version.version_no}</p>
-                <StatusBadge status={version.status} />
-                <Badge className="border-border bg-muted/60 text-muted-foreground">{readableLabel(version.coordination_mode)}</Badge>
-              </div>
-              <p className="mt-2 text-sm leading-6 text-muted-foreground">{version.objective ?? t("teams.noObjectiveProvided")}</p>
-            </div>
-            <p className="text-xs text-muted-foreground">{formatDateTime(version.created_time)}</p>
-          </div>
-          <div className="mt-4 grid gap-3 text-sm md:grid-cols-3">
-            <div className="min-w-0">
-              <p className="text-muted-foreground">{t("teams.members")}</p>
-              <p className="mt-1">{version.member_refs_json.length}</p>
-            </div>
-            <div className="min-w-0">
-              <p className="text-muted-foreground">{t("agents.published")}</p>
-              <p className="mt-1">{version.published_time ? formatDateTime(version.published_time) : t("teams.notPublished")}</p>
-            </div>
-            <div className="min-w-0">
-              <p className="text-muted-foreground">{t("teams.versionId")}</p>
-              <p className="mt-1 truncate font-mono text-xs">{truncateMiddle(version.id, 28)}</p>
-            </div>
-          </div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-function readableLabel(value: string) {
-  return value.split(/[_-]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
-}

+ 285 - 479
web/src/pages/tools/ToolsPage.tsx

@@ -1,141 +1,109 @@
 import * as React from "react";
 import { useTranslation } from "react-i18next";
-import { Activity, CheckCircle2, Clock, Code2, Package, Plug, Plus, RefreshCw, SlidersHorizontal, TerminalSquare, Wrench } from "lucide-react";
-import { createTool, createToolVersion, listToolBindings, listToolCredentials, listToolVersions, listTools } from "@/api";
+import {
+  CheckCircle2,
+  Filter,
+  Plug,
+  RefreshCw,
+  Wrench,
+} from "lucide-react";
+import { listToolBindings, listToolConnections, listTools } from "@/api";
 import { ApiErrorState } from "@/components/shared/ApiErrorState";
 import { EmptyState } from "@/components/shared/EmptyState";
-import { EntityListItem } from "@/components/shared/EntityListItem";
-import { JsonViewer } from "@/components/shared/JsonViewer";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
 import { MetricCard } from "@/components/shared/MetricCard";
 import { PageHeader } from "@/components/shared/PageHeader";
 import { SearchInput } from "@/components/shared/SearchInput";
-import { StatusBadge } from "@/components/shared/StatusBadge";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Dialog } from "@/components/ui/dialog";
-import { Input, Textarea } from "@/components/ui/input";
-import { Select } from "@/components/ui/select";
-import { Tabs } from "@/components/ui/tabs";
-import { toast } from "@/components/ui/toaster";
-import { formatDateTime } from "@/lib/utils";
-import type { JSONObject, ToolBinding, ToolCredential, ToolDefinition, ToolVersion } from "@/types";
+import { demoText } from "@/lib/demo-text";
+import type { ToolBinding, ToolConnection, ToolDefinition } from "@/types";
+import { ConnectMcpServerDialog } from "./components/ConnectMcpServerDialog";
+import { ToolDetailSheet } from "./components/ToolDetailSheet";
 
-type ToolStatusFilter = "all" | "ready" | "unversioned" | "bound";
+type ToolStatusFilter = "all" | "ready" | "needs_config" | "bound";
 
-const defaultPayload = JSON.stringify({ customer_id: "cus_demo_001" }, null, 2);
+type McpExposedTool = {
+  name: string;
+  description?: string;
+  inputSchema?: Record<string, unknown>;
+};
 
 export function ToolsPage() {
   const { t } = useTranslation();
   const [tools, setTools] = React.useState<ToolDefinition[]>([]);
   const [bindings, setBindings] = React.useState<ToolBinding[]>([]);
-  const [credentials, setCredentials] = React.useState<ToolCredential[]>([]);
-  const [versions, setVersions] = React.useState<ToolVersion[]>([]);
-  const [allVersions, setAllVersions] = React.useState<ToolVersion[]>([]);
-  const [selectedToolId, setSelectedToolId] = React.useState<string>();
-  const [selectedVersionId, setSelectedVersionId] = React.useState<string>();
+  const [connections, setConnections] = React.useState<ToolConnection[]>([]);
+  const [selectedTool, setSelectedTool] = React.useState<ToolDefinition>();
   const [search, setSearch] = React.useState("");
-  const [typeFilter, setTypeFilter] = React.useState("all");
   const [statusFilter, setStatusFilter] = React.useState<ToolStatusFilter>("all");
-  const [activeTab, setActiveTab] = React.useState("overview");
-  const [payload, setPayload] = React.useState(defaultPayload);
-  const [runResult, setRunResult] = React.useState<JSONObject | undefined>();
   const [loading, setLoading] = React.useState(true);
   const [error, setError] = React.useState<string>();
-  const [createOpen, setCreateOpen] = React.useState(false);
-  const [versionOpen, setVersionOpen] = React.useState(false);
-
-  const selectedTool = tools.find((tool) => tool.id === selectedToolId);
-  const selectedVersion = versions.find((version) => version.id === selectedVersionId) ?? versions[0];
-  const versionIds = new Set(versions.map((version) => version.id));
-  const selectedBindings = bindings.filter((binding) => versionIds.has(binding.tool_version_id));
-  const selectedCredentials = credentials.filter((credential) => selectedBindings.some((binding) => binding.credential_id === credential.id));
-  const boundVersionIds = new Set(bindings.map((binding) => binding.tool_version_id));
-  const readyToolIds = new Set(allVersions.map((version) => version.tool_id));
-  const boundToolIds = new Set(allVersions.filter((version) => boundVersionIds.has(version.id)).map((version) => version.tool_id));
-  const toolTypes = Array.from(new Set(tools.map((tool) => tool.tool_type))).sort();
+  const [mcpOpen, setMcpOpen] = React.useState(false);
+  const [detailOpen, setDetailOpen] = React.useState(false);
+
+  const connectionByTool = React.useMemo(() => {
+    const grouped = new Map<string, ToolConnection[]>();
+    connections.forEach((connection) => {
+      grouped.set(connection.tool_id, [...(grouped.get(connection.tool_id) ?? []), connection]);
+    });
+    grouped.forEach((items, key) => {
+      grouped.set(key, [...items].sort((a, b) => b.version_no - a.version_no));
+    });
+    return grouped;
+  }, [connections]);
+
+  const boundConnectionIds = new Set(bindings.map((binding) => binding.tool_version_id));
+  const readyToolIds = new Set(connections.map((connection) => connection.tool_id));
+  const boundToolIds = new Set(
+    connections.filter((connection) => boundConnectionIds.has(connection.id)).map((connection) => connection.tool_id)
+  );
+  const mcpServerCount = tools.filter((t) => t.tool_type === "mcp").length;
+  const mcpExposedToolCount = tools
+    .filter((t) => t.tool_type === "mcp")
+    .reduce(
+      (count, tool) => count + getMcpExposedTools(connectionByTool.get(tool.id)?.[0]).length,
+      0
+    );
 
   const filtered = tools.filter((tool) => {
-    const haystack = `${tool.name} ${tool.code} ${tool.tool_type} ${tool.description ?? ""}`.toLowerCase();
+    const haystack = `${tool.name} ${tool.tool_type} ${tool.description ?? ""}`.toLowerCase();
     const matchesSearch = haystack.includes(search.toLowerCase());
-    const matchesType = typeFilter === "all" || tool.tool_type === typeFilter;
     const matchesStatus =
       statusFilter === "all" ||
       (statusFilter === "ready" && readyToolIds.has(tool.id)) ||
-      (statusFilter === "unversioned" && !readyToolIds.has(tool.id)) ||
+      (statusFilter === "needs_config" && !readyToolIds.has(tool.id)) ||
       (statusFilter === "bound" && boundToolIds.has(tool.id));
-    return matchesSearch && matchesType && matchesStatus;
+    return matchesSearch && matchesStatus;
   });
 
   const load = React.useCallback(async () => {
     setLoading(true);
     setError(undefined);
     try {
-      const [toolData, bindingData, credentialData, versionData] = await Promise.all([
+      const [toolData, bindingData, connectionData] = await Promise.all([
         listTools(),
         listToolBindings(),
-        listToolCredentials(),
-        listToolVersions(),
+        listToolConnections(),
       ]);
       setTools(toolData);
       setBindings(bindingData);
-      setCredentials(credentialData);
-      setAllVersions(versionData);
-      setSelectedToolId((current) => current ?? toolData[0]?.id);
+      setConnections(connectionData);
     } catch (err) {
       setError(err instanceof Error ? err.message : t("errors.failedToLoad"));
     } finally {
       setLoading(false);
     }
-  }, []);
+  }, [t]);
 
   React.useEffect(() => {
     void load();
   }, [load]);
 
-  React.useEffect(() => {
-    if (!selectedToolId) {
-      setVersions([]);
-      setSelectedVersionId(undefined);
-      return;
-    }
-
-    void listToolVersions(selectedToolId)
-      .then((data) => {
-        setVersions(data);
-        setSelectedVersionId((current) => (current && data.some((version) => version.id === current) ? current : data[0]?.id));
-      })
-      .catch(() => {
-        setVersions([]);
-        setSelectedVersionId(undefined);
-      });
-  }, [selectedToolId]);
-
-  async function reloadVersions() {
-    if (!selectedToolId) return;
-    const data = await listToolVersions(selectedToolId);
-    setVersions(data);
-    setAllVersions((current) => [...data, ...current.filter((version) => version.tool_id !== selectedToolId)]);
-    setSelectedVersionId(data[0]?.id);
-  }
-
-  function simulateRun() {
-    if (!selectedTool || !selectedVersion) return;
-    try {
-      const parsed = JSON.parse(payload || "{}") as JSONObject;
-      setRunResult({
-        ok: true,
-        tool: selectedTool.code,
-        version: selectedVersion.version_no,
-        latency_ms: Math.min(selectedVersion.timeout_ms ?? 800, 800),
-        received: parsed,
-        output_preview: selectedVersion.output_schema_json ?? { status: "mocked" },
-      });
-      toast.success(t("tools.testPayloadSimulated"));
-    } catch {
-      toast.error(t("tools.payloadMustBeJson"));
-    }
+  function openDetail(tool: ToolDefinition) {
+    setSelectedTool(tool);
+    setDetailOpen(true);
   }
 
   if (loading) return <LoadingSpinner label={t("common.loading")} />;
@@ -151,461 +119,299 @@ export function ToolsPage() {
             <Button variant="outline" onClick={() => void load()}>
               <RefreshCw className="h-4 w-4" /> {t("common.refresh")}
             </Button>
-            <Button onClick={() => setCreateOpen(true)}>
-              <Wrench className="h-4 w-4" /> {t("tools.newTool")}
+            <Button onClick={() => setMcpOpen(true)}>
+              <Plug className="h-4 w-4" /> {t("tools.connectMcp")}
             </Button>
           </>
         }
       />
 
       <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
-        <MetricCard label={t("nav.tools")} value={tools.length} icon={Wrench} />
+        <MetricCard label={t("tools.mcpServers")} value={mcpServerCount} icon={Plug} />
+        <MetricCard label={t("tools.exposedTools")} value={mcpExposedToolCount} icon={Wrench} />
         <MetricCard label={t("tools.ready")} value={readyToolIds.size} icon={CheckCircle2} />
-        <MetricCard label={t("common.versions")} value={allVersions.length} icon={Package} />
-        <MetricCard label={t("tools.bindings")} value={bindings.length} icon={Plug} />
+        <MetricCard label={t("tools.connections")} value={connections.length} icon={Plug} />
       </div>
 
-      <div className="grid gap-6 xl:grid-cols-[400px_1fr]">
-        <Card>
-          <CardHeader>
-            <div className="flex items-start justify-between gap-3">
-              <div>
-                <CardTitle>{t("tools.toolList")}</CardTitle>
-                <CardDescription>{t("tools.shown", { count: filtered.length })} / {tools.length}</CardDescription>
-              </div>
-              <SlidersHorizontal className="mt-1 h-4 w-4 text-muted-foreground" />
+      <Card>
+        <CardHeader>
+          <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
+            <div>
+              <CardTitle>{t("tools.toolList")}</CardTitle>
+              <CardDescription>
+                {t("tools.shown", { count: filtered.length })} / {tools.length}
+              </CardDescription>
             </div>
-          </CardHeader>
-          <CardContent className="space-y-4">
-            <SearchInput value={search} onChange={setSearch} placeholder={t("tools.searchTools")} />
-            <div className="grid gap-3 sm:grid-cols-2">
-              <Select
-                aria-label={t("tools.filterByType")}
-                value={typeFilter}
-                onChange={(event) => setTypeFilter(event.target.value)}
-                options={[{ value: "all", label: t("tools.allTypes") }, ...toolTypes.map((type) => ({ value: type, label: type }))]}
-              />
-              <Select
-                aria-label={t("tools.filterByStatus")}
-                value={statusFilter}
-                onChange={(event) => setStatusFilter(event.target.value as ToolStatusFilter)}
-                options={[
-                  { value: "all", label: t("tools.allStatus") },
-                  { value: "ready", label: t("tools.hasVersion") },
-                  { value: "bound", label: t("tools.bound") },
-                  { value: "unversioned", label: t("tools.needsVersion") },
-                ]}
-              />
-            </div>
-
-            {filtered.length ? (
-              <div className="space-y-2">
-                {filtered.map((tool) => {
-                  const versionCount = allVersions.filter((version) => version.tool_id === tool.id).length;
-                  const isBound = boundToolIds.has(tool.id);
-                  return (
-                    <EntityListItem
-                      key={tool.id}
-                      active={tool.id === selectedToolId}
-                      title={tool.name}
-                      subtitle={`${tool.code} · ${tool.tool_type}`}
-                      meta={
-                        <div className="flex flex-col items-end gap-1">
-                          <Badge className="border-border bg-muted/50 text-muted-foreground">v{versionCount}</Badge>
-                          {isBound ? <StatusBadge status="active" /> : null}
-                        </div>
-                      }
-                      onClick={() => {
-                        setSelectedToolId(tool.id);
-                        setRunResult(undefined);
-                        setActiveTab("overview");
-                      }}
-                    />
-                  );
-                })}
-              </div>
-            ) : (
-              <EmptyState icon={Wrench} title={t("tools.noToolsFound")} description={t("tools.adjustFiltersTool")} />
-            )}
-          </CardContent>
-        </Card>
-
-        <Card>
-          <CardHeader>
-            <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
-              <div>
-                <CardTitle>{selectedTool?.name ?? t("tools.toolDetails")}</CardTitle>
-                <CardDescription>{selectedTool?.description ?? t("tools.selectTool")}</CardDescription>
-              </div>
-              {selectedTool ? (
-                <div className="flex flex-wrap gap-2">
-                  <Badge className="border-primary/30 bg-primary/10 text-primary">{selectedTool.tool_type}</Badge>
-                  {versions.length ? <StatusBadge status="active" /> : <StatusBadge status="draft" />}
-                </div>
-              ) : null}
+          </div>
+        </CardHeader>
+        <CardContent className="space-y-4">
+          <ToolFilterBar
+            search={search}
+            statusFilter={statusFilter}
+            onSearch={setSearch}
+            onStatus={setStatusFilter}
+          />
+          {filtered.length ? (
+            <div className="grid gap-3 xl:grid-cols-2">
+              {filtered.map((tool) => (
+                <ToolCard
+                  key={tool.id}
+                  tool={tool}
+                  connection={connectionByTool.get(tool.id)?.[0]}
+                  bindingCount={getBindingCount(tool.id, connections, bindings)}
+                  onClick={() => openDetail(tool)}
+                />
+              ))}
             </div>
-          </CardHeader>
-          <CardContent>
-            {selectedTool ? (
-              <Tabs
-                value={activeTab}
-                onChange={setActiveTab}
-                tabs={[
-                  {
-                    value: "overview",
-                    label: t("common.overview"),
-                    content: (
-                      <OverviewPanel
-                        tool={selectedTool}
-                        latestVersion={versions[0]}
-                        bindingCount={selectedBindings.length}
-                        credentialCount={selectedCredentials.length}
-                      />
-                    ),
-                  },
-                  {
-                    value: "versions",
-                    label: t("common.versions"),
-                    content: (
-                      <VersionPanel
-                        versions={versions}
-                        selectedVersionId={selectedVersion?.id}
-                        onSelectVersion={setSelectedVersionId}
-                        onCreateVersion={() => setVersionOpen(true)}
-                      />
-                    ),
-                  },
-                  {
-                    value: "test",
-                    label: t("tools.test"),
-                    content: (
-                      <TestPanel
-                        disabled={!selectedVersion}
-                        payload={payload}
-                        setPayload={setPayload}
-                        result={runResult}
-                        onRun={simulateRun}
-                      />
-                    ),
-                  },
-                ]}
-              />
-            ) : (
-              <EmptyState icon={Package} title={t("tools.selectTool")} description={t("tools.toolDetails")} />
-            )}
-          </CardContent>
-        </Card>
-      </div>
-
-      {selectedVersion ? (
-        <div className="grid gap-6 lg:grid-cols-3">
-          <JsonSummary title={t("tools.inputSchema")} value={selectedVersion.input_schema_json} />
-          <JsonSummary title={t("tools.invokeConfig")} value={selectedVersion.invoke_config_json} />
-          <JsonSummary title={t("tools.retryPolicy")} value={selectedVersion.retry_policy_json} />
-        </div>
-      ) : null}
-
-      <CreateToolDialog open={createOpen} onOpenChange={setCreateOpen} onCreated={() => void load()} />
-      <CreateToolVersionDialog open={versionOpen} onOpenChange={setVersionOpen} toolId={selectedToolId} onCreated={() => void reloadVersions()} />
+          ) : (
+            <EmptyState
+              icon={Wrench}
+              title={t("tools.noToolsFound")}
+              description={t("tools.adjustFiltersTool")}
+            />
+           )}
+        </CardContent>
+      </Card>
+
+      <ConnectMcpServerDialog open={mcpOpen} onOpenChange={setMcpOpen} onCreated={() => void load()} />
+      {selectedTool && (
+        <ToolDetailSheet
+          tool={selectedTool}
+          open={detailOpen}
+          onOpenChange={setDetailOpen}
+        />
+      )}
     </div>
   );
 }
 
-function OverviewPanel({
+function ToolCard({
   tool,
-  latestVersion,
+  connection,
   bindingCount,
-  credentialCount,
+  onClick,
 }: {
   tool: ToolDefinition;
-  latestVersion?: ToolVersion;
+  connection?: ToolConnection;
   bindingCount: number;
-  credentialCount: number;
+  onClick: () => void;
 }) {
   const { t } = useTranslation();
+  const endpoint = getToolEndpoint(connection);
+  const timeout = connection?.timeout_ms ? `${connection.timeout_ms} ms` : t("tools.notSet");
+  const isMcp = tool.tool_type === "mcp";
+  const exposedTools = isMcp ? getMcpExposedTools(connection) : [];
+
   return (
-    <div className="space-y-4">
-      <div className="grid gap-4 lg:grid-cols-3">
-        <InfoPanel icon={Code2} label={t("common.code")} value={tool.code} />
-        <InfoPanel icon={Package} label={t("tools.latestVersion")} value={latestVersion ? `v${latestVersion.version_no}` : t("tools.none")} />
-        <InfoPanel icon={Clock} label={t("tools.timeout")} value={latestVersion?.timeout_ms ? `${latestVersion.timeout_ms} ms` : t("tools.notSet")} />
+    <div
+      className="cursor-pointer rounded-lg border border-border bg-muted/20 p-4 transition hover:border-primary/35 hover:bg-muted/30"
+      onClick={onClick}
+    >
+      <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
+        <div className="min-w-0">
+          <div className="flex flex-wrap items-center gap-2">
+            <h3 className="truncate text-base font-semibold">{demoText(tool.name, t)}</h3>
+            <Badge className="border-border bg-surface-elevated text-muted-foreground">
+              {isMcp ? t("tools.mcpServer") : tool.tool_type}
+            </Badge>
+          </div>
+          <p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
+            {demoText(tool.description, t) || t("tools.mcpServerConnection")}
+          </p>
+        </div>
+        <ToolConnectionStatus ready={Boolean(connection)} bindingCount={bindingCount} />
       </div>
 
-      <ReadinessStrip versions={latestVersion ? 1 : 0} bindings={bindingCount} credentials={credentialCount} />
-
-      <div className="rounded-md border border-border p-4">
-        <h3 className="text-sm font-semibold">{t("tools.basicInfo")}</h3>
-        <div className="mt-4 grid gap-4 text-sm md:grid-cols-3">
-          <Detail label={t("common.type")} value={tool.tool_type} />
-          <Detail label={t("tools.plugin")} value={tool.plugin_id ?? t("tools.standalone")} />
-          <Detail label={t("common.created")} value={formatDateTime(tool.created_time)} />
+      <div className="mt-4 rounded-md border border-border bg-surface-elevated p-3">
+        <div className="flex items-center justify-between">
+          <p className="text-xs text-muted-foreground">{t("tools.connection")}</p>
+          {connection?.timeout_ms && (
+            <span className="text-xs text-muted-foreground">{t("tools.timeout")}: {timeout}</span>
+          )}
         </div>
+        <p className="mt-1 truncate font-mono text-sm">{endpoint || t("tools.notConfigured")}</p>
       </div>
-    </div>
-  );
-}
 
-function InfoPanel({ icon: Icon, label, value }: { icon: typeof Wrench; label: string; value: string }) {
-  return (
-    <div className="rounded-md border border-border bg-muted/30 p-4">
-      <div className="flex items-center gap-2 text-sm text-muted-foreground">
-        <Icon className="h-4 w-4" />
-        {label}
-      </div>
-      <p className="mt-3 break-words font-mono text-sm text-foreground">{value}</p>
+      {isMcp && exposedTools.length > 0 && (
+        <McpServerToolsPanel tools={exposedTools} connected={Boolean(connection)} />
+      )}
     </div>
   );
 }
 
-function ReadinessStrip({ versions, bindings, credentials }: { versions: number; bindings: number; credentials: number }) {
+function ToolConnectionStatus({ ready, bindingCount }: { ready: boolean; bindingCount: number }) {
   const { t } = useTranslation();
-  const items = [
-    { label: t("tools.definition"), ready: true },
-    { label: t("common.version"), ready: versions > 0 },
-    { label: t("tools.binding"), ready: bindings > 0 },
-    { label: t("tools.credential"), ready: credentials > 0 },
-  ];
+  if (!ready) {
+    return (
+      <Badge className="w-fit border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-200">
+        {t("tools.needsConfig")}
+      </Badge>
+    );
+  }
+  if (bindingCount > 0) {
+    return (
+      <Badge className="w-fit border-green-500/30 bg-green-500/10 text-green-700 dark:text-green-200">
+        {t("tools.bound")}
+      </Badge>
+    );
+  }
   return (
-    <div className="grid gap-2 sm:grid-cols-4">
-      {items.map((item) => (
-        <div key={item.label} className="flex min-h-12 items-center gap-2 rounded-md border border-border px-3 text-sm">
-          <span className={item.ready ? "text-emerald-500" : "text-muted-foreground"}>
-            <CheckCircle2 className="h-4 w-4" />
-          </span>
-          <span>{item.label}</span>
-        </div>
-      ))}
-    </div>
+    <Badge className="w-fit border-green-500/30 bg-green-500/10 text-green-700 dark:text-green-200">
+      {t("tools.connected")}
+    </Badge>
   );
 }
 
-function VersionPanel({
-  versions,
-  selectedVersionId,
-  onSelectVersion,
-  onCreateVersion,
+function ToolFilterBar({
+  search,
+  statusFilter,
+  onSearch,
+  onStatus,
 }: {
-  versions: ToolVersion[];
-  selectedVersionId?: string;
-  onSelectVersion: (id: string) => void;
-  onCreateVersion: () => void;
+  search: string;
+  statusFilter: ToolStatusFilter;
+  onSearch: (value: string) => void;
+  onStatus: (value: ToolStatusFilter) => void;
 }) {
   const { t } = useTranslation();
+  const statusOptions: Array<{ value: ToolStatusFilter; label: string }> = [
+    { value: "all", label: t("tools.allStatus") },
+    { value: "ready", label: t("tools.configured") },
+    { value: "bound", label: t("tools.bound") },
+    { value: "needs_config", label: t("tools.needsConfig") },
+  ];
+
   return (
-    <div className="space-y-4">
-      <div className="flex items-center justify-between gap-3">
-        <div>
-          <h3 className="text-sm font-semibold">{t("common.versions")}</h3>
-          <p className="text-sm text-muted-foreground">{t("tools.versionDescription")}</p>
+    <div className="rounded-xl border border-border bg-muted/20 p-3">
+      <div className="grid gap-3 xl:grid-cols-[minmax(280px,1fr)_auto] xl:items-center">
+        <SearchInput
+          className="sm:w-full"
+          value={search}
+          onChange={onSearch}
+          placeholder={t("tools.searchToolPlaceholder")}
+        />
+        <div className="flex flex-wrap gap-2 xl:justify-end">
+          <FilterSelect
+            label={t("tools.statusLabel")}
+            value={statusFilter}
+            options={statusOptions}
+            onChange={(value) => onStatus(value as ToolStatusFilter)}
+          />
         </div>
-        <Button size="sm" variant="secondary" onClick={onCreateVersion}>
-          <Plus className="h-4 w-4" /> {t("common.createNew")}
-        </Button>
       </div>
-
-      {versions.length ? (
-        <div className="grid gap-3 lg:grid-cols-2">
-          {versions.map((version) => (
-            <button
-              key={version.id}
-              type="button"
-              onClick={() => onSelectVersion(version.id)}
-              className={[
-                "min-h-28 rounded-md border p-4 text-left transition hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary",
-                selectedVersionId === version.id ? "border-primary/45 bg-primary/10" : "border-border bg-muted/20",
-              ].join(" ")}
-            >
-              <div className="flex items-start justify-between gap-3">
-                <div>
-                  <p className="font-semibold">v{version.version_no}</p>
-                  <p className="mt-1 text-xs text-muted-foreground">{formatDateTime(version.created_time)}</p>
-                </div>
-                <Badge className="border-border bg-muted/50 text-muted-foreground">{version.timeout_ms ?? "n/a"} ms</Badge>
-              </div>
-              <div className="mt-4 grid grid-cols-3 gap-2 text-xs text-muted-foreground">
-                <span>{Object.keys(version.input_schema_json ?? {}).length} {t("tools.inputs")}</span>
-                <span>{Object.keys(version.output_schema_json ?? {}).length} {t("tools.outputs")}</span>
-                <span>{Object.keys(version.retry_policy_json ?? {}).length} {t("tools.retry")}</span>
-              </div>
-            </button>
-          ))}
-        </div>
-      ) : (
-        <EmptyState icon={Package} title={t("tools.noVersionsYet")} description={t("tools.createVersionBeforeTesting")} />
-      )}
     </div>
   );
 }
 
-function TestPanel({
-  disabled,
-  payload,
-  setPayload,
-  result,
-  onRun,
+function FilterSelect({
+  label,
+  value,
+  options,
+  onChange,
 }: {
-  disabled: boolean;
-  payload: string;
-  setPayload: (value: string) => void;
-  result?: JSONObject;
-  onRun: () => void;
+  label: string;
+  value: string;
+  options: Array<{ value: string; label: string }>;
+  onChange: (value: string) => void;
 }) {
-  const { t } = useTranslation();
-  return (
-    <div className="grid gap-4 lg:grid-cols-[1fr_340px]">
-      <div className="space-y-3">
-        <div className="flex items-center justify-between gap-3">
-          <div>
-            <h3 className="text-sm font-semibold">{t("tools.payloadTest")}</h3>
-            <p className="text-sm text-muted-foreground">{t("tools.runMockRequest")}</p>
-          </div>
-          <Button size="sm" onClick={onRun} disabled={disabled}>
-            <TerminalSquare className="h-4 w-4" /> {t("tools.run")}
-          </Button>
-        </div>
-        <Textarea className="min-h-64 font-mono text-sm" value={payload} onChange={(event) => setPayload(event.target.value)} />
-      </div>
-      <div>
-        <div className="mb-3 flex items-center gap-2 text-sm font-semibold">
-          <Activity className="h-4 w-4" /> {t("tools.result")}
-        </div>
-        {result ? <JsonViewer value={result} collapsed={false} /> : <EmptyState icon={TerminalSquare} title={t("tools.noRunYet")} description={t("tools.selectVersionRun")} />}
-      </div>
-    </div>
-  );
-}
-
-function JsonSummary({ title, value }: { title: string; value: unknown }) {
   return (
-    <Card>
-      <CardHeader>
-        <CardTitle className="text-sm">{title}</CardTitle>
-      </CardHeader>
-      <CardContent>
-        <JsonViewer value={value ?? {}} />
-      </CardContent>
-    </Card>
+    <label className="inline-flex h-10 items-center gap-2 rounded-full border border-border bg-surface-elevated px-3 text-sm shadow-sm">
+      <Filter className="h-3.5 w-3.5 text-muted-foreground" />
+      <span className="text-xs text-muted-foreground">{label}</span>
+      <select
+        className="bg-transparent text-sm font-medium outline-none"
+        value={value}
+        onChange={(event) => onChange(event.target.value)}
+      >
+        {options.map((option) => (
+          <option key={option.value} value={option.value}>
+            {option.label}
+          </option>
+        ))}
+      </select>
+    </label>
   );
 }
 
-function Detail({ label, value }: { label: string; value: string }) {
-  return (
-    <div>
-      <p className="text-muted-foreground">{label}</p>
-      <p className="mt-1 break-words font-mono text-xs">{value}</p>
-    </div>
-  );
-}
-
-function CreateToolVersionDialog({
-  open,
-  onOpenChange,
-  toolId,
-  onCreated,
+function McpServerToolsPanel({
+  tools,
+  connected,
 }: {
-  open: boolean;
-  onOpenChange: (open: boolean) => void;
-  toolId?: string;
-  onCreated: () => void;
+  tools: McpExposedTool[];
+  connected: boolean;
 }) {
   const { t } = useTranslation();
-  const [timeoutMs, setTimeoutMs] = React.useState("5000");
-  const [endpoint, setEndpoint] = React.useState("https://mock.local/tool");
-  const [maxAttempts, setMaxAttempts] = React.useState("2");
-  const [submitting, setSubmitting] = React.useState(false);
-
-  async function submit(event: React.FormEvent) {
-    event.preventDefault();
-    if (!toolId) return;
-    setSubmitting(true);
-    try {
-      await createToolVersion({
-        tool_id: toolId,
-        timeout_ms: Number(timeoutMs) || null,
-        input_schema_json: { input: "object" },
-        output_schema_json: { result: "object" },
-        invoke_config_json: { url: endpoint },
-        retry_policy_json: { max_attempts: Number(maxAttempts) || 1 },
-      });
-      toast.success(t("tools.toolVersionCreated"));
-      onOpenChange(false);
-      setTimeoutMs("5000");
-      setEndpoint("https://mock.local/tool");
-      setMaxAttempts("2");
-      onCreated();
-    } finally {
-      setSubmitting(false);
-    }
-  }
 
   return (
-    <Dialog open={open} onOpenChange={onOpenChange} title={t("tools.createToolVersion")}>
-      <form className="space-y-4" onSubmit={submit}>
-        <Field label={t("tools.endpoint")}>
-          <Input value={endpoint} onChange={(event) => setEndpoint(event.target.value)} />
-        </Field>
-        <div className="grid gap-4 sm:grid-cols-2">
-          <Field label={t("tools.timeoutMs")}>
-            <Input inputMode="numeric" value={timeoutMs} onChange={(event) => setTimeoutMs(event.target.value)} />
-          </Field>
-          <Field label={t("tools.retryAttempts")}>
-            <Input inputMode="numeric" value={maxAttempts} onChange={(event) => setMaxAttempts(event.target.value)} />
-          </Field>
-        </div>
-        <div className="flex justify-end gap-2">
-          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>{t("common.cancel")}</Button>
-          <Button disabled={submitting || !toolId}>{submitting ? t("common.creating") : t("tools.createVersion")}</Button>
+    <div className="mt-4 rounded-lg border border-border bg-surface-elevated p-3">
+      <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
+        <div>
+          <p className="text-sm font-semibold">{t("tools.mcpToolsInside")}</p>
+          <p className="text-xs text-muted-foreground">
+            {t("tools.mcpToolsInsideDesc")}
+          </p>
         </div>
-      </form>
-    </Dialog>
+        <Badge className="w-fit border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-200">
+          {tools.length ? t("tools.discovered", { count: tools.length }) : connected ? t("tools.discoveryPending") : t("tools.notConnected")}
+        </Badge>
+      </div>
+      <div className="mt-3 grid gap-2 md:grid-cols-2">
+        {tools.map((tool) => (
+          <div key={tool.name} className="rounded-md border border-border bg-muted/20 p-3">
+            <div className="flex items-start gap-2">
+              <Wrench className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
+              <div className="min-w-0">
+                <p className="truncate font-mono text-sm font-semibold">{tool.name}</p>
+                <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
+                  {demoText(tool.description, t) || t("tools.noMcpToolDescription")}
+                </p>
+                {tool.inputSchema && (
+                  <p className="mt-2 text-xs text-muted-foreground">{t("tools.parametersSchemaDetected")}</p>
+                )}
+              </div>
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
   );
 }
 
-function CreateToolDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (open: boolean) => void; onCreated: () => void }) {
-  const { t } = useTranslation();
-  const [form, setForm] = React.useState({ name: "", tool_type: "http", description: "" });
-  const [submitting, setSubmitting] = React.useState(false);
+function getToolEndpoint(connection?: ToolConnection) {
+  const config = connection?.invoke_config_json;
+  if (!config || typeof config !== "object") return undefined;
+  const url = config.url;
+  return typeof url === "string" ? url : undefined;
+}
 
-  async function submit(event: React.FormEvent) {
-    event.preventDefault();
-    setSubmitting(true);
-    try {
-      await createTool({ ...form });
-      toast.success(t("tools.toolCreated"));
-      onOpenChange(false);
-      setForm({ name: "", tool_type: "http", description: "" });
-      onCreated();
-    } finally {
-      setSubmitting(false);
-    }
+function getMcpExposedTools(connection?: ToolConnection): McpExposedTool[] {
+  const config = connection?.invoke_config_json;
+  if (!config) return [];
+  const tools = config.mcp_tools ?? config.tools ?? config.tool_names;
+  if (!Array.isArray(tools)) return [];
+  const result: McpExposedTool[] = [];
+  for (const t of tools) {
+    if (!t || typeof t !== "object") continue;
+    const record = t as Record<string, unknown>;
+    if (typeof record.name !== "string") continue;
+    result.push({
+      name: record.name as string,
+      description: typeof record.description === "string" ? record.description : undefined,
+      inputSchema: record.inputSchema && typeof record.inputSchema === "object"
+        ? record.inputSchema as Record<string, unknown>
+        : undefined,
+    });
   }
-
-  return (
-    <Dialog open={open} onOpenChange={onOpenChange} title={t("tools.createTool")}>
-      <form className="space-y-4" onSubmit={submit}>
-        <Field label={t("common.name")}><Input required value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} /></Field>
-        <Field label={t("common.type")}>
-          <Select
-            value={form.tool_type}
-            onChange={(event) => setForm({ ...form, tool_type: event.target.value })}
-            options={[
-              { value: "http", label: "HTTP" },
-              { value: "retrieval", label: "Retrieval" },
-              { value: "code", label: "Code" },
-              { value: "webhook", label: "Webhook" },
-            ]}
-          />
-        </Field>
-        <Field label={t("common.description")}><Textarea value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /></Field>
-        <div className="flex justify-end gap-2">
-          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>{t("common.cancel")}</Button>
-          <Button disabled={submitting}>{submitting ? t("common.creating") : t("common.create")}</Button>
-        </div>
-      </form>
-    </Dialog>
-  );
+  return result;
 }
 
-function Field({ label, children }: { label: string; children: React.ReactNode }) {
-  return <label className="block space-y-2 text-sm"><span className="text-muted-foreground">{label}</span>{children}</label>;
+function getBindingCount(
+  toolId: string,
+  connections: ToolConnection[],
+  bindings: ToolBinding[]
+): number {
+  const toolConnectionIds = new Set(
+    connections.filter((connection) => connection.tool_id === toolId).map((connection) => connection.id)
+  );
+  return bindings.filter((binding) => toolConnectionIds.has(binding.tool_version_id)).length;
 }

+ 168 - 0
web/src/pages/tools/components/ConnectMcpServerDialog.tsx

@@ -0,0 +1,168 @@
+import * as React from "react";
+import { Plus, Trash2 } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { createTool, createToolConnection } from "@/api";
+import { Button } from "@/components/ui/button";
+import { Dialog } from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { toast } from "@/components/ui/toaster";
+
+interface ConnectMcpServerDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  onCreated: () => void;
+}
+
+export function ConnectMcpServerDialog({ open, onOpenChange, onCreated }: ConnectMcpServerDialogProps) {
+  const { t } = useTranslation();
+  const [serverName, setServerName] = React.useState("");
+  const [url, setUrl] = React.useState("");
+  const [headers, setHeaders] = React.useState<Array<{ key: string; value: string }>>([]);
+  const [timeout, setTimeout] = React.useState("30");
+  const [submitting, setSubmitting] = React.useState(false);
+
+  function reset() {
+    setServerName("");
+    setUrl("");
+    setHeaders([]);
+    setTimeout("30");
+  }
+
+  function addHeader() {
+    setHeaders([...headers, { key: "", value: "" }]);
+  }
+
+  function removeHeader(index: number) {
+    setHeaders(headers.filter((_, i) => i !== index));
+  }
+
+  function updateHeader(index: number, field: "key" | "value", value: string) {
+    const updated = [...headers];
+    const current = updated[index];
+    if (current) {
+      updated[index] = { ...current, [field]: value };
+      setHeaders(updated);
+    }
+  }
+
+  async function submit(event: React.FormEvent) {
+    event.preventDefault();
+    if (!serverName.trim() || !url.trim()) return;
+
+    const headersObj: Record<string, string> = {};
+    for (const h of headers) {
+      if (h.key.trim() && h.value.trim()) {
+        headersObj[h.key.trim()] = h.value.trim();
+      }
+    }
+
+    setSubmitting(true);
+    try {
+      const tool = await createTool({
+        name: serverName.trim(),
+        tool_type: "mcp",
+        description: `MCP SSE server: ${url.trim()}`,
+      });
+      await createToolConnection({
+        tool_id: tool.id,
+        timeout_ms: Number(timeout) * 1000 || null,
+        input_schema_json: {},
+        output_schema_json: {},
+        invoke_config_json: {
+          transport: "sse",
+          server_name: serverName.trim(),
+          url: url.trim(),
+          headers: headersObj,
+          timeout_seconds: Number(timeout) || null,
+        },
+        retry_policy_json: { max_attempts: 1 },
+      });
+      toast.success(t("tools.mcpConnected"));
+      onOpenChange(false);
+      reset();
+      onCreated();
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : t("tools.failedToConnectMcp"));
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange} title={t("tools.connectMcpServer")} className="max-w-lg">
+      <form className="space-y-4" onSubmit={submit}>
+        <Field label={t("tools.serverName")} required>
+          <Input
+            value={serverName}
+            onChange={(e) => setServerName(e.target.value)}
+            placeholder={t("tools.serverNamePlaceholder")}
+            required
+          />
+        </Field>
+        <Field label={t("tools.sseEndpointUrl")} required>
+          <Input
+            value={url}
+            onChange={(e) => setUrl(e.target.value)}
+            placeholder="http://host:9090/sse"
+            required
+          />
+        </Field>
+        <div className="space-y-2">
+          <div className="flex items-center justify-between">
+            <span className="text-sm text-muted-foreground">{t("tools.headersOptional")}</span>
+            <Button type="button" variant="ghost" size="sm" onClick={addHeader}>
+              <Plus className="h-3 w-3 mr-1" /> {t("common.add")}
+            </Button>
+          </div>
+          {headers.map((h, i) => (
+            <div key={i} className="flex gap-2">
+              <Input
+                className="flex-1"
+                value={h.key}
+                onChange={(e) => updateHeader(i, "key", e.target.value)}
+                placeholder={t("tools.headerKey")}
+              />
+              <Input
+                className="flex-1"
+                value={h.value}
+                onChange={(e) => updateHeader(i, "value", e.target.value)}
+                placeholder={t("tools.headerValue")}
+              />
+              <Button type="button" variant="ghost" size="sm" onClick={() => removeHeader(i)}>
+                <Trash2 className="h-3 w-3" />
+              </Button>
+            </div>
+          ))}
+        </div>
+        <Field label={t("tools.timeoutSeconds")}>
+          <Input
+            type="number"
+            value={timeout}
+            onChange={(e) => setTimeout(e.target.value)}
+            placeholder="30"
+          />
+        </Field>
+        <div className="flex justify-end gap-2">
+          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
+            {t("common.cancel")}
+          </Button>
+          <Button disabled={submitting || !serverName.trim() || !url.trim()}>
+            {submitting ? t("tools.connecting") : t("tools.connect")}
+          </Button>
+        </div>
+      </form>
+    </Dialog>
+  );
+}
+
+function Field({ label, children, required }: { label: string; children: React.ReactNode; required?: boolean }) {
+  return (
+    <label className="block space-y-2 text-sm">
+      <span className="text-muted-foreground">
+        {label}
+        {required && <span className="text-red-500 ml-0.5">*</span>}
+      </span>
+      {children}
+    </label>
+  );
+}

+ 113 - 0
web/src/pages/tools/components/CreateToolDialog.tsx

@@ -0,0 +1,113 @@
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import { Globe2, Plug, Search } from "lucide-react";
+import { createTool } from "@/api";
+import { Button } from "@/components/ui/button";
+import { Dialog } from "@/components/ui/dialog";
+import { Input, Textarea } from "@/components/ui/input";
+import { toast } from "@/components/ui/toaster";
+
+const TOOL_TYPES = [
+  { value: "http", label: "HTTP", description: "REST/HTTP API endpoint", icon: Globe2 },
+  { value: "mcp", label: "MCP Server", description: "Model Context Protocol server", icon: Plug },
+  { value: "retrieval", label: "Retrieval", description: "Knowledge base retrieval", icon: Search },
+];
+
+interface CreateToolDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  onCreated: () => void;
+}
+
+export function CreateToolDialog({ open, onOpenChange, onCreated }: CreateToolDialogProps) {
+  const { t } = useTranslation();
+  const [name, setName] = React.useState("");
+  const [toolType, setToolType] = React.useState("http");
+  const [description, setDescription] = React.useState("");
+  const [submitting, setSubmitting] = React.useState(false);
+
+  async function submit(event: React.FormEvent) {
+    event.preventDefault();
+    if (!name.trim()) return;
+    setSubmitting(true);
+    try {
+      await createTool({
+        name: name.trim(),
+        tool_type: toolType,
+        description: description.trim() || null,
+      });
+      toast.success(t("tools.toolCreated"));
+      onOpenChange(false);
+      setName("");
+      setToolType("http");
+      setDescription("");
+      onCreated();
+    } catch (err) {
+      toast.error(err instanceof Error ? err.message : "Failed to create tool");
+    } finally {
+      setSubmitting(false);
+    }
+  }
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange} title={t("tools.createTool")} className="max-w-lg">
+      <form className="space-y-4" onSubmit={submit}>
+        <Field label={t("tools.name")}>
+          <Input
+            value={name}
+            onChange={(e) => setName(e.target.value)}
+            placeholder="e.g. CRM Lookup"
+            required
+          />
+        </Field>
+        <Field label={t("tools.type")}>
+          <div className="grid gap-2 sm:grid-cols-3">
+            {TOOL_TYPES.map((type) => (
+              <button
+                key={type.value}
+                type="button"
+                className={`flex flex-col items-start gap-1 rounded-lg border p-3 text-left transition-colors ${
+                  toolType === type.value
+                    ? "border-primary bg-primary/5"
+                    : "border-border hover:border-primary/50"
+                }`}
+                onClick={() => setToolType(type.value)}
+              >
+                <div className="flex items-center gap-2">
+                  <type.icon className="h-4 w-4" />
+                  <span className="text-sm font-medium">{type.label}</span>
+                </div>
+                <span className="text-xs text-muted-foreground">{type.description}</span>
+              </button>
+            ))}
+          </div>
+        </Field>
+        <Field label={t("tools.description")}>
+          <Textarea
+            value={description}
+            onChange={(e) => setDescription(e.target.value)}
+            placeholder="Optional description for this tool"
+            rows={3}
+          />
+        </Field>
+        <div className="flex justify-end gap-2">
+          <Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
+            {t("common.cancel")}
+          </Button>
+          <Button disabled={submitting || !name.trim()}>
+            {submitting ? t("common.creating") : t("tools.create")}
+          </Button>
+        </div>
+      </form>
+    </Dialog>
+  );
+}
+
+function Field({ label, children }: { label: string; children: React.ReactNode }) {
+  return (
+    <label className="block space-y-2 text-sm">
+      <span className="text-muted-foreground">{label}</span>
+      {children}
+    </label>
+  );
+}

+ 113 - 0
web/src/pages/tools/components/ToolDetailSheet.tsx

@@ -0,0 +1,113 @@
+import * as React from "react";
+import { Wrench } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { listToolConnections } from "@/api";
+import { Sheet } from "@/components/ui/sheet";
+import { toast } from "@/components/ui/toaster";
+import type { ToolConnection, ToolDefinition } from "@/types";
+
+type McpExposedTool = {
+  name: string;
+  description?: string;
+  inputSchema?: Record<string, unknown>;
+};
+
+interface ToolDetailSheetProps {
+  tool: ToolDefinition;
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+}
+
+export function ToolDetailSheet({ tool, open, onOpenChange }: ToolDetailSheetProps) {
+  const { t } = useTranslation();
+  const [connections, setConnections] = React.useState<ToolConnection[]>([]);
+
+  const loadData = React.useCallback(async () => {
+    try {
+      const data = await listToolConnections(tool.id);
+      setConnections(data);
+    } catch {
+      toast.error(t("tools.failedToLoadDetails"));
+    }
+  }, [t, tool.id]);
+
+  React.useEffect(() => {
+    if (open) void loadData();
+  }, [open, loadData]);
+
+  const activeConnection = connections[0];
+  const isMcp = tool.tool_type === "mcp";
+  const mcpTools = isMcp ? getMcpToolsFromConfig(activeConnection?.invoke_config_json) : [];
+
+  return (
+    <Sheet
+      open={open}
+      onOpenChange={onOpenChange}
+      title={tool.name}
+      description={t("tools.availableToolCount", { count: mcpTools.length })}
+      className="max-w-2xl"
+    >
+      <div className="space-y-3">
+        {mcpTools.length > 0 ? (
+          mcpTools.map((mcpTool) => (
+            <McpToolDetail key={mcpTool.name} tool={mcpTool} />
+          ))
+        ) : (
+          <p className="text-sm text-muted-foreground">{t("tools.noToolsDiscovered")}</p>
+        )}
+      </div>
+    </Sheet>
+  );
+}
+
+function McpToolDetail({ tool }: { tool: McpExposedTool }) {
+  const { t } = useTranslation();
+  const params = tool.inputSchema ? Object.entries(tool.inputSchema) : [];
+
+  return (
+    <div className="rounded-md border border-border bg-background p-3">
+      <div className="flex items-start gap-2">
+        <Wrench className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
+        <div className="min-w-0 flex-1">
+          <p className="font-mono text-sm font-semibold">{tool.name}</p>
+          <p className="mt-1 text-xs text-muted-foreground">
+            {tool.description || t("tools.noMcpToolDescription")}
+          </p>
+          {params.length > 0 && (
+            <div className="mt-2">
+              <p className="text-xs font-medium text-muted-foreground">{t("tools.parameters")}</p>
+              <div className="mt-1 space-y-1">
+                {params.map(([name, type]) => (
+                  <div key={name} className="flex items-center gap-2 text-xs">
+                    <span className="rounded bg-muted px-1.5 py-0.5 font-mono">{name}</span>
+                    <span className="text-muted-foreground">{String(type)}</span>
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function getMcpToolsFromConfig(config?: Record<string, unknown> | null): McpExposedTool[] {
+  if (!config) return [];
+  const tools = config.mcp_tools ?? config.tools ?? config.tool_names;
+  if (!Array.isArray(tools)) return [];
+  const result: McpExposedTool[] = [];
+  for (const t of tools) {
+    if (!t || typeof t !== "object") continue;
+    const record = t as Record<string, unknown>;
+    if (typeof record.name !== "string") continue;
+    result.push({
+      name: record.name as string,
+      description: typeof record.description === "string" ? record.description : undefined,
+      inputSchema: record.input_schema && typeof record.input_schema === "object"
+        ? record.input_schema as Record<string, unknown>
+        : undefined,
+    });
+  }
+  return result;
+}

+ 3 - 2
web/src/types/agent.ts

@@ -6,7 +6,6 @@ export type AgentRunStatus = "queued" | "running" | "completed" | "failed" | "ca
 
 export interface AgentDefinition {
   id: string;
-  code: string;
   name: string;
   description?: string | null;
   agent_type: string;
@@ -25,12 +24,15 @@ export interface AgentVersion {
   system_prompt: string;
   model_config_json: JSONObject;
   memory_policy_json: JSONObject;
+  runtime_policy_json?: JSONObject;
   tool_refs_json: JSONObject[];
   skill_refs_json: JSONObject[];
   published_time?: string | null;
   created_time: string;
 }
 
+export type AgentConfig = AgentVersion;
+
 export interface AgentRun {
   id: string;
   agent_id: string;
@@ -46,7 +48,6 @@ export interface AgentRun {
   lease_expire_time?: string | null;
   started_time?: string | null;
   finished_time?: string | null;
-  error_code?: string | null;
   error_message?: string | null;
   created_time: string;
 }

+ 0 - 2
web/src/types/app.ts

@@ -1,7 +1,6 @@
 import type { JSONObject } from "./common";
 
 export interface AppCreateRequest {
-  code: string;
   name: string;
   description?: string | null;
   owner_user_id?: string | null;
@@ -10,7 +9,6 @@ export interface AppCreateRequest {
 
 export interface AppResponse {
   id: string;
-  code: string;
   name: string;
   description?: string | null;
   owner_user_id?: string | null;

+ 0 - 1
web/src/types/auth.ts

@@ -17,7 +17,6 @@ export interface UserContract {
 
 export interface RoleContract {
   id: string;
-  code: string;
   name: string;
   description?: string | null;
   status: RoleStatus;

+ 1 - 1
web/src/types/common.ts

@@ -20,7 +20,7 @@ export interface DownstreamServiceHealth {
   service: string;
   status: string;
   url: string;
-  status_code?: number | null;
+  http_status?: number | null;
   error_message?: string | null;
 }
 

+ 2 - 0
web/src/types/index.ts

@@ -8,6 +8,8 @@ export * from "./agent";
 export * from "./session";
 export * from "./runtime";
 export * from "./tool";
+export * from "./skill";
 export * from "./knowledge";
 export * from "./team";
 export * from "./model-provider";
+export * from "./memory";

+ 0 - 1
web/src/types/knowledge.ts

@@ -2,7 +2,6 @@ import type { JSONObject } from "./common";
 
 export interface KnowledgeBase {
   id: string;
-  code: string;
   name: string;
   description?: string | null;
   status: "active" | "archived";

+ 56 - 0
web/src/types/memory.ts

@@ -0,0 +1,56 @@
+import type { JSONObject } from "./common";
+
+export type MemoryStatus = "active" | "archived" | "deleted";
+export type MemoryScopeType = "global" | "user" | "session" | "agent" | "team";
+
+export interface MemoryItem {
+  id: string;
+  scope_type: MemoryScopeType;
+  scope_id: string;
+  memory_type: string;
+  content_text: string;
+  content_json?: JSONObject | null;
+  metadata_json: JSONObject;
+  embedding_model?: string | null;
+  embedding_json?: number[] | null;
+  owner_agent_id?: string | null;
+  user_id?: string | null;
+  session_id?: string | null;
+  source_ref?: string | null;
+  importance_score: number;
+  status: MemoryStatus;
+  last_accessed_time?: string | null;
+  expires_time?: string | null;
+  created_time: string;
+}
+
+export interface MemoryCreateRequest {
+  scope_type: MemoryScopeType;
+  scope_id: string;
+  memory_type: string;
+  content_text: string;
+  content_json?: JSONObject | null;
+  metadata_json?: JSONObject;
+  owner_agent_id?: string | null;
+  user_id?: string | null;
+  session_id?: string | null;
+  source_ref?: string | null;
+  importance_score?: number;
+  expires_time?: string | null;
+}
+
+export interface MemorySearchRequest {
+  query: string;
+  scope_type?: MemoryScopeType | null;
+  scope_id?: string | null;
+  owner_agent_id?: string | null;
+  user_id?: string | null;
+  session_id?: string | null;
+  limit?: number;
+}
+
+export interface MemorySearchResult {
+  item: MemoryItem;
+  score: number;
+  score_json: JSONObject;
+}

+ 0 - 4
web/src/types/model-provider.ts

@@ -1,12 +1,10 @@
 export type ModelProviderType = "openai" | "anthropic" | "deepseek" | "azure_openai" | "ollama" | "custom";
-export type ModelProviderStatus = "active" | "inactive" | "error";
 export type ModelType = "chat" | "reasoning" | "embedding" | "image" | "audio" | "video" | "rerank" | "moderation" | "other";
 
 export interface ModelProvider {
   id: string;
   name: string;
   provider_type: ModelProviderType;
-  status: ModelProviderStatus;
   base_url: string;
   api_key_ref: string;
   models: ModelItem[];
@@ -20,7 +18,6 @@ export interface ModelItem {
   model_id: string;
   display_name: string;
   model_type: ModelType;
-  enabled: boolean;
 }
 
 export interface ModelProviderCreateRequest {
@@ -39,7 +36,6 @@ export interface ModelProviderUpdateRequest {
   api_key?: string;
   models?: ModelItem[];
   default_model?: string | null;
-  status?: ModelProviderStatus;
   extra_config_json?: Record<string, unknown>;
 }
 

+ 0 - 6
web/src/types/model.ts

@@ -1,16 +1,12 @@
 import type { JSONObject } from "./common";
 
-export type ModelStatus = "active" | "disabled";
-
 export interface ModelDefinition {
   id: string;
-  code: string;
   name: string;
   provider_type: string;
   provider_base_url: string;
   has_provider_api_key: boolean;
   model_name: string;
-  status: ModelStatus;
   description?: string | null;
   capabilities_json: string[];
   context_window?: number | null;
@@ -23,13 +19,11 @@ export interface ModelDefinition {
 }
 
 export interface ModelCreateRequest {
-  code: string;
   name: string;
   provider_type: string;
   provider_base_url: string;
   provider_api_key?: string | null;
   model_name: string;
-  status?: ModelStatus;
   description?: string | null;
   capabilities_json?: string[];
   context_window?: number | null;

+ 0 - 1
web/src/types/runtime.ts

@@ -59,7 +59,6 @@ export interface TraceSpan {
   ended_time?: string | null;
   duration_ms?: number | null;
   attributes_json?: JSONObject | null;
-  error_code?: string | null;
   error_message?: string | null;
   created_time: string;
 }

+ 39 - 0
web/src/types/skill.ts

@@ -0,0 +1,39 @@
+import type { JSONObject } from "./common";
+
+export interface SkillDefinition {
+  id: string;
+  name: string;
+  skill_type: string;
+  description?: string | null;
+  status: string;
+  owner_user_id?: string | null;
+  metadata_json: JSONObject;
+  created_time: string;
+}
+
+export interface SkillVersion {
+  id: string;
+  skill_id: string;
+  version_no: number;
+  status: string;
+  runtime_type: string;
+  entrypoint?: string | null;
+  parameter_schema_json: JSONObject;
+  output_schema_json: JSONObject;
+  implementation_json: JSONObject;
+  published_time?: string | null;
+  created_time: string;
+}
+
+export interface SkillInstallation {
+  id: string;
+  skill_id: string;
+  skill_version_id: string;
+  install_scope: string;
+  scope_id?: string | null;
+  status: string;
+  config_json: JSONObject;
+  installed_by?: string | null;
+  installed_time?: string | null;
+  created_time: string;
+}

+ 2 - 1
web/src/types/team.ts

@@ -5,7 +5,6 @@ export type TeamRunStatus = "queued" | "running" | "completed" | "failed" | "can
 
 export interface TeamDefinition {
   id: string;
-  code: string;
   name: string;
   description?: string | null;
   team_type: string;
@@ -27,6 +26,8 @@ export interface TeamVersion {
   created_time: string;
 }
 
+export type TeamConfig = TeamVersion;
+
 export interface TeamRun {
   id: string;
   team_id: string;

+ 2 - 2
web/src/types/tool.ts

@@ -3,7 +3,6 @@ import type { JSONObject } from "./common";
 export interface ToolDefinition {
   id: string;
   plugin_id?: string | null;
-  code: string;
   name: string;
   tool_type: string;
   description?: string | null;
@@ -28,11 +27,12 @@ export interface ToolBinding {
   tool_version_id: string;
   credential_id?: string | null;
   binding_scope: string;
-  enabled: boolean;
   config_json?: JSONObject | null;
   created_time: string;
 }
 
+export type ToolConnection = ToolVersion;
+
 export interface ToolCredential {
   id: string;
   name: string;

+ 1 - 4
web/src/types/workflow.ts

@@ -14,7 +14,6 @@ export interface EdgeDefinition {
 }
 
 export interface WorkflowDSL {
-  code: string;
   name: string;
   nodes: NodeDefinition[];
   edges: EdgeDefinition[];
@@ -23,7 +22,6 @@ export interface WorkflowDSL {
 export interface WorkflowDefinition {
   id: string;
   app_id: string;
-  code: string;
   name: string;
   workflow_type: string;
   latest_version_no: number;
@@ -32,7 +30,6 @@ export interface WorkflowDefinition {
 
 export interface WorkflowCreateRequest {
   app_id: string;
-  code: string;
   name: string;
   workflow_type: string;
 }
@@ -51,7 +48,7 @@ export interface WorkflowVersion {
 
 export interface WorkflowDesignerDiagnostic {
   severity: "info" | "warning" | "error";
-  code: string;
+  diagnostic_id: string;
   message: string;
   node_id?: string | null;
   edge_index?: number | null;

Vissa filer visades inte eftersom för många filer har ändrats