mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Merge pull request #1390 from nearai/staging-promote/94e4d9d3-23273403042
chore: promote staging to staging-promote/ec04354c-23271447493 (2026-03-19 00:12 UTC)
This commit is contained in:
+147
@@ -7,6 +7,153 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.19.0](https://github.com/nearai/ironclaw/compare/v0.18.0...v0.19.0) - 2026-03-17
|
||||
|
||||
### Added
|
||||
|
||||
- verify telegram owner during hot activation ([#1157](https://github.com/nearai/ironclaw/pull/1157))
|
||||
- *(config)* unify config resolution with Settings fallback (Phase 2, #1119) ([#1203](https://github.com/nearai/ironclaw/pull/1203))
|
||||
- *(sandbox)* add retry logic for transient container failures ([#1232](https://github.com/nearai/ironclaw/pull/1232))
|
||||
- *(heartbeat)* fire_at time-of-day scheduling with IANA timezone ([#1029](https://github.com/nearai/ironclaw/pull/1029))
|
||||
- Reuse Codex CLI OAuth tokens for ChatGPT backend LLM calls ([#693](https://github.com/nearai/ironclaw/pull/693))
|
||||
- add pre-push git hook with delta lint mode ([#833](https://github.com/nearai/ironclaw/pull/833))
|
||||
- *(cli)* add `logs` command for gateway log access ([#1105](https://github.com/nearai/ironclaw/pull/1105))
|
||||
- add Feishu/Lark WASM channel plugin ([#1110](https://github.com/nearai/ironclaw/pull/1110))
|
||||
- add Criterion benchmarks for safety layer hot paths ([#836](https://github.com/nearai/ironclaw/pull/836))
|
||||
- *(routines)* human-readable cron schedule summaries in web UI ([#1154](https://github.com/nearai/ironclaw/pull/1154))
|
||||
- *(web)* add follow-up suggestion chips and ghost text ([#1156](https://github.com/nearai/ironclaw/pull/1156))
|
||||
- *(ci)* include commit history in staging promotion PRs ([#952](https://github.com/nearai/ironclaw/pull/952))
|
||||
- *(tools)* add reusable sensitive JSON redaction helper ([#457](https://github.com/nearai/ironclaw/pull/457))
|
||||
- configurable hybrid search fusion strategy ([#234](https://github.com/nearai/ironclaw/pull/234))
|
||||
- *(cli)* add cron subcommand for managing scheduled routines ([#1017](https://github.com/nearai/ironclaw/pull/1017))
|
||||
- adds context-llm tool support ([#616](https://github.com/nearai/ironclaw/pull/616))
|
||||
- *(web-chat)* add hover copy button for user/assistant messages ([#948](https://github.com/nearai/ironclaw/pull/948))
|
||||
- add Slack approval buttons for tool execution in DMs ([#796](https://github.com/nearai/ironclaw/pull/796))
|
||||
- enhance HTTP tool parameter parsing ([#911](https://github.com/nearai/ironclaw/pull/911))
|
||||
- *(routines)* enable tool access in lightweight routine execution ([#257](https://github.com/nearai/ironclaw/pull/257)) ([#730](https://github.com/nearai/ironclaw/pull/730))
|
||||
- add MiniMax as a built-in LLM provider ([#940](https://github.com/nearai/ironclaw/pull/940))
|
||||
- *(cli)* add `ironclaw channels list` subcommand ([#933](https://github.com/nearai/ironclaw/pull/933))
|
||||
- *(cli)* add `ironclaw skills list/search/info` subcommands ([#918](https://github.com/nearai/ironclaw/pull/918))
|
||||
- add cargo-deny for supply chain safety ([#834](https://github.com/nearai/ironclaw/pull/834))
|
||||
- *(setup)* display ASCII art banner during onboarding ([#851](https://github.com/nearai/ironclaw/pull/851))
|
||||
- *(extensions)* unify auth and configure into single entrypoint ([#677](https://github.com/nearai/ironclaw/pull/677))
|
||||
- *(i18n)* Add internationalization support with Chinese and English translations ([#929](https://github.com/nearai/ironclaw/pull/929))
|
||||
- Import OpenClaw memory, history and settings ([#903](https://github.com/nearai/ironclaw/pull/903))
|
||||
|
||||
### Fixed
|
||||
|
||||
- jobs limit ([#1274](https://github.com/nearai/ironclaw/pull/1274))
|
||||
- misleading UI message ([#1265](https://github.com/nearai/ironclaw/pull/1265))
|
||||
- bump channel registry versions for promotion ([#1264](https://github.com/nearai/ironclaw/pull/1264))
|
||||
- cover staging CI all-features and routine batch regressions ([#1256](https://github.com/nearai/ironclaw/pull/1256))
|
||||
- resolve merge conflict fallout and missing config fields
|
||||
- web/CLI routine mutations do not refresh live event trigger cache ([#1255](https://github.com/nearai/ironclaw/pull/1255))
|
||||
- *(jobs)* make completed->completed transition idempotent to prevent race errors ([#1068](https://github.com/nearai/ironclaw/pull/1068))
|
||||
- *(llm)* persist refreshed Anthropic OAuth token after Keychain re-read ([#1213](https://github.com/nearai/ironclaw/pull/1213))
|
||||
- *(worker)* prevent orphaned tool_results and fix parallel merging ([#1069](https://github.com/nearai/ironclaw/pull/1069))
|
||||
- Telegram bot token validation fails intermittently (HTTP 404) ([#1166](https://github.com/nearai/ironclaw/pull/1166))
|
||||
- *(security)* prevent metadata spoofing of internal job monitor flag ([#1195](https://github.com/nearai/ironclaw/pull/1195))
|
||||
- *(security)* default webhook server to loopback when tunnel is configured ([#1194](https://github.com/nearai/ironclaw/pull/1194))
|
||||
- *(auth)* avoid false success and block chat during pending auth ([#1111](https://github.com/nearai/ironclaw/pull/1111))
|
||||
- *(config)* unify ChannelsConfig resolution to env > settings > default ([#1124](https://github.com/nearai/ironclaw/pull/1124))
|
||||
- *(web-chat)* normalize chat copy to plain text ([#1114](https://github.com/nearai/ironclaw/pull/1114))
|
||||
- *(skill)* treat empty url param as absent when installing skills ([#1128](https://github.com/nearai/ironclaw/pull/1128))
|
||||
- preserve AuthError type in oauth_http_client cache ([#1152](https://github.com/nearai/ironclaw/pull/1152))
|
||||
- *(web)* prevent Safari IME composition Enter from sending message ([#1140](https://github.com/nearai/ironclaw/pull/1140))
|
||||
- *(mcp)* handle 400 auth errors, clear auth mode after OAuth, trim tokens ([#1158](https://github.com/nearai/ironclaw/pull/1158))
|
||||
- eliminate panic paths in production code ([#1184](https://github.com/nearai/ironclaw/pull/1184))
|
||||
- N+1 query pattern in event trigger loop (routine_engine) ([#1163](https://github.com/nearai/ironclaw/pull/1163))
|
||||
- *(llm)* add stop_sequences parity for tool completions ([#1170](https://github.com/nearai/ironclaw/pull/1170))
|
||||
- *(channels)* use live owner binding during wasm hot activation ([#1171](https://github.com/nearai/ironclaw/pull/1171))
|
||||
- Non-transactional multi-step context updates between metadata/to… ([#1161](https://github.com/nearai/ironclaw/pull/1161))
|
||||
- *(webhook)* avoid lock-held awaits in server lifecycle paths ([#1168](https://github.com/nearai/ironclaw/pull/1168))
|
||||
- Google Sheets returns 403 PERMISSION_DENIED after completing OAuth ([#1164](https://github.com/nearai/ironclaw/pull/1164))
|
||||
- HTTP webhook secret transmitted in request body rather than via header, docs inconsistency and security concern ([#1162](https://github.com/nearai/ironclaw/pull/1162))
|
||||
- *(ci)* exclude ironclaw_safety from release automation ([#1146](https://github.com/nearai/ironclaw/pull/1146))
|
||||
- *(registry)* bump versions for github, web-search, and discord extensions ([#1106](https://github.com/nearai/ironclaw/pull/1106))
|
||||
- *(mcp)* address 14 audit findings across MCP module ([#1094](https://github.com/nearai/ironclaw/pull/1094))
|
||||
- *(http)* replace .expect() with match in webhook handler ([#1133](https://github.com/nearai/ironclaw/pull/1133))
|
||||
- *(time)* treat empty timezone string as absent ([#1127](https://github.com/nearai/ironclaw/pull/1127))
|
||||
- 5 critical/high-priority bugs (auth bypass, relay failures, unbounded recursion, context growth) ([#1083](https://github.com/nearai/ironclaw/pull/1083))
|
||||
- *(ci)* checkout promotion PR head for metadata refresh ([#1097](https://github.com/nearai/ironclaw/pull/1097))
|
||||
- *(ci)* add missing attachments field and crates/ dir to Dockerfiles ([#1100](https://github.com/nearai/ironclaw/pull/1100))
|
||||
- *(registry)* bump telegram channel version for capabilities change ([#1064](https://github.com/nearai/ironclaw/pull/1064))
|
||||
- *(ci)* repair staging promotion workflow behavior ([#1091](https://github.com/nearai/ironclaw/pull/1091))
|
||||
- *(wasm)* address #1086 review followups -- description hint and coercion safety ([#1092](https://github.com/nearai/ironclaw/pull/1092))
|
||||
- *(ci)* repair staging-ci workflow parsing ([#1090](https://github.com/nearai/ironclaw/pull/1090))
|
||||
- *(extensions)* fix lifecycle bugs + comprehensive E2E tests ([#1070](https://github.com/nearai/ironclaw/pull/1070))
|
||||
- add tool_info schema discovery for WASM tools ([#1086](https://github.com/nearai/ironclaw/pull/1086))
|
||||
- resolve bug_bash UX/logging issues (#1054 #1055 #1058) ([#1072](https://github.com/nearai/ironclaw/pull/1072))
|
||||
- *(http)* fail closed when webhook secret is missing at runtime ([#1075](https://github.com/nearai/ironclaw/pull/1075))
|
||||
- *(service)* set CLI_ENABLED=false in macOS launchd plist ([#1079](https://github.com/nearai/ironclaw/pull/1079))
|
||||
- relax approval requirements for low-risk tools ([#922](https://github.com/nearai/ironclaw/pull/922))
|
||||
- *(web)* make approval requests appear without page reload ([#996](https://github.com/nearai/ironclaw/pull/996)) ([#1073](https://github.com/nearai/ironclaw/pull/1073))
|
||||
- *(routines)* run cron checks immediately on ticker startup ([#1066](https://github.com/nearai/ironclaw/pull/1066))
|
||||
- *(web)* recompute cron next_fire_at when re-enabling routines ([#1080](https://github.com/nearai/ironclaw/pull/1080))
|
||||
- *(memory)* reject absolute filesystem paths with corrective routing ([#934](https://github.com/nearai/ironclaw/pull/934))
|
||||
- remove all inline event handlers for CSP script-src compliance ([#1063](https://github.com/nearai/ironclaw/pull/1063))
|
||||
- *(mcp)* include OAuth state parameter in authorization URLs ([#1049](https://github.com/nearai/ironclaw/pull/1049))
|
||||
- *(mcp)* open MCP OAuth in same browser as gateway ([#951](https://github.com/nearai/ironclaw/pull/951))
|
||||
- *(deploy)* harden production container and bootstrap security ([#1014](https://github.com/nearai/ironclaw/pull/1014))
|
||||
- release lock guards before awaiting channel send ([#869](https://github.com/nearai/ironclaw/pull/869)) ([#1003](https://github.com/nearai/ironclaw/pull/1003))
|
||||
- *(registry)* use versioned artifact URLs and checksums for all WASM manifests ([#1007](https://github.com/nearai/ironclaw/pull/1007))
|
||||
- *(setup)* preserve model selection on provider re-run ([#679](https://github.com/nearai/ironclaw/pull/679)) ([#987](https://github.com/nearai/ironclaw/pull/987))
|
||||
- *(mcp)* attach session manager for non-OAuth HTTP clients ([#793](https://github.com/nearai/ironclaw/pull/793)) ([#986](https://github.com/nearai/ironclaw/pull/986))
|
||||
- *(security)* migrate webhook auth to HMAC-SHA256 signature header ([#970](https://github.com/nearai/ironclaw/pull/970))
|
||||
- *(security)* make unsafe env::set_var calls safe with explicit invariants ([#968](https://github.com/nearai/ironclaw/pull/968))
|
||||
- *(security)* require explicit SANDBOX_ALLOW_FULL_ACCESS to enable FullAccess policy ([#967](https://github.com/nearai/ironclaw/pull/967))
|
||||
- *(security)* add Content-Security-Policy header to web gateway ([#966](https://github.com/nearai/ironclaw/pull/966))
|
||||
- *(test)* stabilize openai compat oversized-body regression ([#839](https://github.com/nearai/ironclaw/pull/839))
|
||||
- *(ci)* disambiguate WASM bundle filenames to prevent tool/channel collision ([#964](https://github.com/nearai/ironclaw/pull/964))
|
||||
- *(setup)* validate channel credentials during setup ([#684](https://github.com/nearai/ironclaw/pull/684))
|
||||
- drain tunnel pipes to prevent zombie process ([#735](https://github.com/nearai/ironclaw/pull/735))
|
||||
- *(mcp)* header safety validation and Authorization conflict bug from #704 ([#752](https://github.com/nearai/ironclaw/pull/752))
|
||||
- *(agent)* block thread_id-based context pollution across users ([#760](https://github.com/nearai/ironclaw/pull/760))
|
||||
- *(mcp)* stdio/unix transports skip initialize handshake ([#890](https://github.com/nearai/ironclaw/pull/890)) ([#935](https://github.com/nearai/ironclaw/pull/935))
|
||||
- *(setup)* drain residual events and filter key kind in onboard prompts ([#937](https://github.com/nearai/ironclaw/pull/937)) ([#949](https://github.com/nearai/ironclaw/pull/949))
|
||||
- *(security)* load WASM tool description and schema from capabilities.json ([#520](https://github.com/nearai/ironclaw/pull/520))
|
||||
- *(security)* resolve DNS once and reuse for SSRF validation to prevent rebinding ([#518](https://github.com/nearai/ironclaw/pull/518))
|
||||
- *(security)* replace regex HTML sanitizer with DOMPurify to prevent XSS ([#510](https://github.com/nearai/ironclaw/pull/510))
|
||||
- *(ci)* improve Claude Code review reliability ([#955](https://github.com/nearai/ironclaw/pull/955))
|
||||
- *(ci)* run gated test jobs during staging CI ([#956](https://github.com/nearai/ironclaw/pull/956))
|
||||
- *(ci)* prevent staging-ci tag failure and chained PR auto-close ([#900](https://github.com/nearai/ironclaw/pull/900))
|
||||
- *(ci)* WASM WIT compat sqlite3 duplicate symbol conflict ([#953](https://github.com/nearai/ironclaw/pull/953))
|
||||
- resolve deferred review items from PRs #883, #848, #788 ([#915](https://github.com/nearai/ironclaw/pull/915))
|
||||
- *(web)* improve UX readability and accessibility in chat UI ([#910](https://github.com/nearai/ironclaw/pull/910))
|
||||
|
||||
### Other
|
||||
|
||||
- Fix Telegram auto-verify flow and routing ([#1273](https://github.com/nearai/ironclaw/pull/1273))
|
||||
- *(e2e)* fix approval waiting regression coverage ([#1270](https://github.com/nearai/ironclaw/pull/1270))
|
||||
- isolate heavy integration tests ([#1266](https://github.com/nearai/ironclaw/pull/1266))
|
||||
- Merge branch 'main' into fix/resolve-conflicts
|
||||
- Refactor owner scope across channels and fix default routing fallback ([#1151](https://github.com/nearai/ironclaw/pull/1151))
|
||||
- *(extensions)* document relay manager init order ([#928](https://github.com/nearai/ironclaw/pull/928))
|
||||
- *(setup)* extract init logic from wizard into owning modules ([#1210](https://github.com/nearai/ironclaw/pull/1210))
|
||||
- mention MiniMax as built-in provider in all READMEs ([#1209](https://github.com/nearai/ironclaw/pull/1209))
|
||||
- Fix schema-guided tool parameter coercion ([#1143](https://github.com/nearai/ironclaw/pull/1143))
|
||||
- Make no-panics CI check test-aware ([#1160](https://github.com/nearai/ironclaw/pull/1160))
|
||||
- *(mcp)* avoid reallocating SSE buffer on each chunk ([#1153](https://github.com/nearai/ironclaw/pull/1153))
|
||||
- *(routines)* avoid full message history clone each tool iteration ([#1172](https://github.com/nearai/ironclaw/pull/1172))
|
||||
- *(registry)* align manifest versions with published artifacts ([#1169](https://github.com/nearai/ironclaw/pull/1169))
|
||||
- remove __pycache__ from repo and add to .gitignore ([#1177](https://github.com/nearai/ironclaw/pull/1177))
|
||||
- *(registry)* move MCP servers from code to JSON manifests ([#1144](https://github.com/nearai/ironclaw/pull/1144))
|
||||
- improve routine schema guidance ([#1089](https://github.com/nearai/ironclaw/pull/1089))
|
||||
- add event-trigger routine e2e coverage ([#1088](https://github.com/nearai/ironclaw/pull/1088))
|
||||
- enforce no .unwrap(), .expect(), or assert!() in production code ([#1087](https://github.com/nearai/ironclaw/pull/1087))
|
||||
- periodic sync main into staging (resolved conflicts) ([#1098](https://github.com/nearai/ironclaw/pull/1098))
|
||||
- fix formatting in cli/mod.rs and mcp/auth.rs ([#1071](https://github.com/nearai/ironclaw/pull/1071))
|
||||
- Expose the shared agent session manager via AppComponents ([#532](https://github.com/nearai/ironclaw/pull/532))
|
||||
- *(agent)* remove unnecessary Worker re-export ([#923](https://github.com/nearai/ironclaw/pull/923))
|
||||
- Fix UTF-8 unsafe truncation in WASM emit_message ([#1015](https://github.com/nearai/ironclaw/pull/1015))
|
||||
- extract safety module into ironclaw_safety crate ([#1024](https://github.com/nearai/ironclaw/pull/1024))
|
||||
- Add Z.AI provider support for GLM-5 ([#938](https://github.com/nearai/ironclaw/pull/938))
|
||||
- *(html_to_markdown)* refresh golden files after renderer bump ([#1016](https://github.com/nearai/ironclaw/pull/1016))
|
||||
- Migrate GitHub webhook normalization into github tool ([#758](https://github.com/nearai/ironclaw/pull/758))
|
||||
- Fix systemctl unit ([#472](https://github.com/nearai/ironclaw/pull/472))
|
||||
- add Russian localization (README.ru.md) ([#850](https://github.com/nearai/ironclaw/pull/850))
|
||||
- Add generic host-verified /webhook/tools/{tool} ingress ([#757](https://github.com/nearai/ironclaw/pull/757))
|
||||
|
||||
## [0.18.0](https://github.com/nearai/ironclaw/compare/v0.17.0...v0.18.0) - 2026-03-11
|
||||
|
||||
### Other
|
||||
|
||||
Generated
+1
-1
@@ -3436,7 +3436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ironclaw"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ exclude = [
|
||||
|
||||
[package]
|
||||
name = "ironclaw"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.92"
|
||||
description = "Secure personal AI assistant that protects your data and expands its capabilities on the fly"
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/discord-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "efa1b9019fa33e243f8db1e1fcc732731d45836336bdd26ca19b6fe227ca8b69"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-discord-0.2.1-wasm32-wasip2.tar.gz",
|
||||
"sha256": "6159cb54aa44a9d8219e29bf0aea9404213b20ff567506fe75f23d4698d6ec18"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
"capabilities": "feishu.capabilities.json",
|
||||
"crate_name": "feishu-channel"
|
||||
},
|
||||
"artifacts": {},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"sha256": "5fca74022264d1c8e78a0853766276f7ffa3cf0d8065b2f51ca10985acad4714",
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-feishu-0.1.1-wasm32-wasip2.tar.gz"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
"method": "manual",
|
||||
"provider": "Feishu / Lark",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/telegram-0.2.3-wasm32-wasip2.tar.gz",
|
||||
"sha256": "b9a83d5a2d1285ce0ec116b354336a1f245f893291ccb01dffbcaccf89d72aed"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/channel-telegram-0.2.4-wasm32-wasip2.tar.gz",
|
||||
"sha256": "a7cb300ec1c946831cfceaa95c1dc8f30d0f42a3924f3cb5de8098821573f4b8"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/github-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "da9fac56b6f20197a415489bbaec9fefb085a5cf6324cab79ea48a47eb19c13b"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-github-0.2.1-wasm32-wasip2.tar.gz",
|
||||
"sha256": "92c530b3ad172e2372d819744b5233f1d8f65768e26eb5a6c213eba3ce1de758"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/llm-context-wasm32-wasip2.tar.gz",
|
||||
"sha256": "581cc5867ef3b75116b7ddc8161e63dd92befe2b53e6ad8213c007639aa243c3"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-llm-context-0.1.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "d9ced2b1226b879135891e0ee40e072c7c95412e1b2462925a23853e1f92497e"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/slack-0.2.1-wasm32-wasip2.tar.gz",
|
||||
"sha256": "d4667e35126986509d862bc3a0088777305d8f41c75de83c1e223b42312ede48"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-slack-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "ccfb0415d7a04f9497726c712d15216de36e86f498b849101283c017f5ab4efb"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/telegram-0.2.2-wasm32-wasip2.tar.gz",
|
||||
"sha256": "b9a83d5a2d1285ce0ec116b354336a1f245f893291ccb01dffbcaccf89d72aed"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-telegram-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "c17065ca41fae5f2a7c43b36144686718cd310a2f22442313bb1aa82bbad0ae4"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.18.0/web-search-0.2.0-wasm32-wasip2.tar.gz",
|
||||
"sha256": "56834573c54ea2a33cea1eb0f04bbdf59f1ef8d8702995cf431b0921302eeccc"
|
||||
"url": "https://github.com/nearai/ironclaw/releases/download/v0.19.0/tool-web-search-0.2.1-wasm32-wasip2.tar.gz",
|
||||
"sha256": "bad275ca4ec314adea5241d6b92c44ccf9cebcbca8e30ba2493cc0bcb4b57218"
|
||||
}
|
||||
},
|
||||
"auth_summary": {
|
||||
|
||||
@@ -3,4 +3,5 @@ git_release_enable = false
|
||||
|
||||
[[package]]
|
||||
name = "ironclaw_safety"
|
||||
publish = false
|
||||
release = false
|
||||
|
||||
@@ -102,6 +102,7 @@ impl GatewayChannel {
|
||||
cost_guard: None,
|
||||
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
startup_time: std::time::Instant::now(),
|
||||
active_config: server::ActiveConfigSnapshot::default(),
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -139,6 +140,7 @@ impl GatewayChannel {
|
||||
cost_guard: self.state.cost_guard.clone(),
|
||||
routine_engine: Arc::clone(&self.state.routine_engine),
|
||||
startup_time: self.state.startup_time,
|
||||
active_config: self.state.active_config.clone(),
|
||||
};
|
||||
mutate(&mut new_state);
|
||||
self.state = Arc::new(new_state);
|
||||
@@ -250,6 +252,12 @@ impl GatewayChannel {
|
||||
self
|
||||
}
|
||||
|
||||
/// Inject the active (resolved) configuration snapshot for the status endpoint.
|
||||
pub fn with_active_config(mut self, config: server::ActiveConfigSnapshot) -> Self {
|
||||
self.rebuild_state(|s| s.active_config = config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the auth token (for printing to console on startup).
|
||||
pub fn auth_token(&self) -> &str {
|
||||
&self.auth_token
|
||||
|
||||
@@ -126,6 +126,14 @@ impl RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot of the active (resolved) configuration exposed to the frontend.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct ActiveConfigSnapshot {
|
||||
pub llm_backend: String,
|
||||
pub llm_model: String,
|
||||
pub enabled_channels: Vec<String>,
|
||||
}
|
||||
|
||||
/// Shared state for all gateway handlers.
|
||||
pub struct GatewayState {
|
||||
/// Channel to send messages to the agent loop.
|
||||
@@ -177,6 +185,8 @@ pub struct GatewayState {
|
||||
pub routine_engine: RoutineEngineSlot,
|
||||
/// Server startup time for uptime calculation.
|
||||
pub startup_time: std::time::Instant,
|
||||
/// Snapshot of active (resolved) configuration for the frontend.
|
||||
pub active_config: ActiveConfigSnapshot,
|
||||
}
|
||||
|
||||
/// Start the gateway HTTP server.
|
||||
@@ -2669,6 +2679,9 @@ async fn gateway_status_handler(
|
||||
daily_cost,
|
||||
actions_this_hour,
|
||||
model_usage,
|
||||
llm_backend: state.active_config.llm_backend.clone(),
|
||||
llm_model: state.active_config.llm_model.clone(),
|
||||
enabled_channels: state.active_config.enabled_channels.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2694,6 +2707,9 @@ struct GatewayStatusResponse {
|
||||
actions_this_hour: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model_usage: Option<Vec<ModelUsageEntry>>,
|
||||
llm_backend: String,
|
||||
llm_model: String,
|
||||
enabled_channels: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2890,6 +2906,7 @@ mod tests {
|
||||
cost_guard: None,
|
||||
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
startup_time: std::time::Instant::now(),
|
||||
active_config: ActiveConfigSnapshot::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+893
-84
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,15 @@ I18n.register('en', {
|
||||
'tab.memory': 'Memory',
|
||||
'tab.jobs': 'Jobs',
|
||||
'tab.routines': 'Routines',
|
||||
'tab.settings': 'Settings',
|
||||
'tab.extensions': 'Extensions',
|
||||
'tab.skills': 'Skills',
|
||||
'tab.logs': 'Logs',
|
||||
'settings.inference': 'Inference',
|
||||
'settings.agent': 'Agent',
|
||||
'settings.channels': 'Channels',
|
||||
'settings.networking': 'Networking',
|
||||
'settings.mcp': 'MCP',
|
||||
|
||||
// Status
|
||||
'status.connected': 'Connected',
|
||||
@@ -131,10 +137,10 @@ I18n.register('en', {
|
||||
|
||||
// Extensions Tab
|
||||
'extensions.installed': 'Installed Extensions',
|
||||
'extensions.available': 'Available WASM Extensions',
|
||||
'extensions.installWasm': 'Install WASM Extension',
|
||||
'extensions.available': 'Available Extensions',
|
||||
'extensions.installWasm': 'Install Extension',
|
||||
'extensions.noInstalled': 'No extensions installed',
|
||||
'extensions.noAvailable': 'No additional WASM extensions available',
|
||||
'extensions.noAvailable': 'No additional extensions available',
|
||||
'extensions.loading': 'Loading...',
|
||||
'extensions.install': 'Install',
|
||||
'extensions.installing': 'Installing...',
|
||||
@@ -156,13 +162,8 @@ I18n.register('en', {
|
||||
'mcp.addCustom': 'Add Custom MCP Server',
|
||||
'mcp.add': 'Add',
|
||||
'mcp.addedSuccess': 'Added MCP server {name}',
|
||||
|
||||
// Registered Tools
|
||||
'tools.registered': 'Registered Tools',
|
||||
'tools.name': 'Name',
|
||||
'tools.description': 'Description',
|
||||
'tools.empty': 'No tools registered',
|
||||
|
||||
|
||||
|
||||
// Skills Tab
|
||||
'skills.installed': 'Installed Skills',
|
||||
'skills.noInstalled': 'No skills installed',
|
||||
@@ -302,6 +303,7 @@ I18n.register('en', {
|
||||
|
||||
// Common
|
||||
'common.loading': 'Loading...',
|
||||
'common.loadFailed': 'Failed to load',
|
||||
'common.noData': 'No data',
|
||||
'common.search': 'Search',
|
||||
'common.add': 'Add',
|
||||
@@ -328,6 +330,8 @@ I18n.register('en', {
|
||||
|
||||
// Extensions
|
||||
'ext.active': 'Active',
|
||||
'ext.inactive': 'Inactive',
|
||||
'ext.builtin': 'Built-in',
|
||||
'ext.remove': 'Remove',
|
||||
'ext.install': 'Install',
|
||||
'ext.installing': 'Installing...',
|
||||
@@ -355,4 +359,160 @@ I18n.register('en', {
|
||||
'config.autoGenerate': 'Auto-generated if empty',
|
||||
'config.save': 'Save',
|
||||
'config.cancel': 'Cancel',
|
||||
|
||||
// Settings toolbar
|
||||
'settings.export': 'Export',
|
||||
'settings.import': 'Import',
|
||||
'settings.searchPlaceholder': 'Search settings...',
|
||||
'settings.exportSuccess': 'Settings exported',
|
||||
'settings.exportFailed': 'Export failed: {message}',
|
||||
'settings.importSuccess': 'Settings imported successfully',
|
||||
'settings.importFailed': 'Import failed: {message}',
|
||||
'settings.restartRequired': 'Restart required for changes to take effect.',
|
||||
'settings.restartNow': 'Restart Now',
|
||||
'settings.noMatchingSettings': 'No settings matching "{query}"',
|
||||
'settings.noSettings': 'No settings found',
|
||||
'settings.saved': 'Saved',
|
||||
'settings.on': 'On',
|
||||
'settings.off': 'Off',
|
||||
'settings.envValue': 'env: {value}',
|
||||
'settings.envDefault': 'env default',
|
||||
'settings.useEnvDefault': 'use env default',
|
||||
|
||||
// Settings groups
|
||||
'cfg.group.llm': 'LLM Provider',
|
||||
'cfg.group.embeddings': 'Embeddings',
|
||||
'cfg.group.agent': 'Agent',
|
||||
'cfg.group.heartbeat': 'Heartbeat',
|
||||
'cfg.group.sandbox': 'Sandbox',
|
||||
'cfg.group.routines': 'Routines',
|
||||
'cfg.group.safety': 'Safety',
|
||||
'cfg.group.skills': 'Skills',
|
||||
'cfg.group.search': 'Search',
|
||||
'cfg.group.tunnel': 'Tunnel',
|
||||
'cfg.group.gateway': 'Gateway',
|
||||
|
||||
// Inference settings
|
||||
'cfg.llm_backend.label': 'Backend',
|
||||
'cfg.llm_backend.desc': 'LLM inference provider',
|
||||
'cfg.selected_model.label': 'Model',
|
||||
'cfg.selected_model.desc': 'Model name or ID for the selected backend',
|
||||
'cfg.ollama_base_url.label': 'Ollama URL',
|
||||
'cfg.ollama_base_url.desc': 'Base URL for Ollama API',
|
||||
'cfg.openai_compatible_base_url.label': 'OpenAI-compatible URL',
|
||||
'cfg.openai_compatible_base_url.desc': 'Base URL for OpenAI-compatible API',
|
||||
'cfg.bedrock_region.label': 'Bedrock Region',
|
||||
'cfg.bedrock_region.desc': 'AWS region for Bedrock',
|
||||
'cfg.bedrock_cross_region.label': 'Cross-Region',
|
||||
'cfg.bedrock_cross_region.desc': 'Enable cross-region inference',
|
||||
'cfg.bedrock_profile.label': 'AWS Profile',
|
||||
'cfg.bedrock_profile.desc': 'AWS profile for Bedrock auth',
|
||||
'cfg.embeddings_enabled.label': 'Enabled',
|
||||
'cfg.embeddings_enabled.desc': 'Enable vector embeddings for memory search',
|
||||
'cfg.embeddings_provider.label': 'Provider',
|
||||
'cfg.embeddings_provider.desc': 'Embeddings API provider',
|
||||
'cfg.embeddings_model.label': 'Model',
|
||||
'cfg.embeddings_model.desc': 'Embedding model name',
|
||||
|
||||
// Agent settings
|
||||
'cfg.agent_name.label': 'Name',
|
||||
'cfg.agent_name.desc': 'Agent display name',
|
||||
'cfg.agent_max_parallel_jobs.label': 'Max Parallel Jobs',
|
||||
'cfg.agent_max_parallel_jobs.desc': 'Maximum concurrent background jobs',
|
||||
'cfg.agent_job_timeout.label': 'Job Timeout',
|
||||
'cfg.agent_job_timeout.desc': 'Max duration per job in seconds',
|
||||
'cfg.agent_max_tool_iterations.label': 'Max Tool Iterations',
|
||||
'cfg.agent_max_tool_iterations.desc': 'Max tool calls per turn',
|
||||
'cfg.agent_use_planning.label': 'Planning',
|
||||
'cfg.agent_use_planning.desc': 'Enable multi-step planning before execution',
|
||||
'cfg.agent_auto_approve.label': 'Auto-approve Tools',
|
||||
'cfg.agent_auto_approve.desc': 'Skip manual approval for tool calls',
|
||||
'cfg.agent_timezone.label': 'Timezone',
|
||||
'cfg.agent_timezone.desc': 'Default timezone (IANA)',
|
||||
'cfg.agent_session_idle.label': 'Session Idle Timeout',
|
||||
'cfg.agent_session_idle.desc': 'Seconds before idle session expires',
|
||||
'cfg.agent_stuck_threshold.label': 'Stuck Threshold',
|
||||
'cfg.agent_stuck_threshold.desc': 'Seconds before a job is considered stuck',
|
||||
'cfg.agent_max_repair.label': 'Max Repair Attempts',
|
||||
'cfg.agent_max_repair.desc': 'Auto-recovery attempts for stuck jobs',
|
||||
'cfg.agent_max_cost.label': 'Max Daily Cost',
|
||||
'cfg.agent_max_cost.desc': 'Daily LLM spend cap in cents (0 = unlimited)',
|
||||
'cfg.agent_max_actions.label': 'Max Actions/Hour',
|
||||
'cfg.agent_max_actions.desc': 'Hourly tool call rate limit (0 = unlimited)',
|
||||
'cfg.agent_allow_local.label': 'Allow Local Tools',
|
||||
'cfg.agent_allow_local.desc': 'Enable local filesystem tool execution',
|
||||
|
||||
// Heartbeat settings
|
||||
'cfg.heartbeat_enabled.label': 'Enabled',
|
||||
'cfg.heartbeat_enabled.desc': 'Run periodic background checks',
|
||||
'cfg.heartbeat_interval.label': 'Interval',
|
||||
'cfg.heartbeat_interval.desc': 'Seconds between heartbeats (default: 1800)',
|
||||
'cfg.heartbeat_notify_channel.label': 'Notify Channel',
|
||||
'cfg.heartbeat_notify_channel.desc': 'Channel to send heartbeat findings to',
|
||||
'cfg.heartbeat_notify_user.label': 'Notify User',
|
||||
'cfg.heartbeat_notify_user.desc': 'User ID to notify',
|
||||
'cfg.heartbeat_quiet_start.label': 'Quiet Hours Start',
|
||||
'cfg.heartbeat_quiet_start.desc': 'Hour (0-23) to stop heartbeats',
|
||||
'cfg.heartbeat_quiet_end.label': 'Quiet Hours End',
|
||||
'cfg.heartbeat_quiet_end.desc': 'Hour (0-23) to resume heartbeats',
|
||||
'cfg.heartbeat_timezone.label': 'Timezone',
|
||||
'cfg.heartbeat_timezone.desc': 'Timezone for quiet hours (IANA)',
|
||||
|
||||
// Sandbox settings
|
||||
'cfg.sandbox_enabled.label': 'Enabled',
|
||||
'cfg.sandbox_enabled.desc': 'Enable Docker sandbox for background jobs',
|
||||
'cfg.sandbox_policy.label': 'Policy',
|
||||
'cfg.sandbox_policy.desc': 'Sandbox security policy',
|
||||
'cfg.sandbox_timeout.label': 'Timeout',
|
||||
'cfg.sandbox_timeout.desc': 'Max job duration in seconds',
|
||||
'cfg.sandbox_memory.label': 'Memory Limit',
|
||||
'cfg.sandbox_memory.desc': 'Container memory limit (MB)',
|
||||
'cfg.sandbox_image.label': 'Docker Image',
|
||||
'cfg.sandbox_image.desc': 'Container image for sandbox jobs',
|
||||
|
||||
// Routines settings
|
||||
'cfg.routines_max_concurrent.label': 'Max Concurrent',
|
||||
'cfg.routines_max_concurrent.desc': 'Maximum routines running simultaneously',
|
||||
'cfg.routines_cooldown.label': 'Default Cooldown',
|
||||
'cfg.routines_cooldown.desc': 'Minimum seconds between routine fires',
|
||||
|
||||
// Safety settings
|
||||
'cfg.safety_max_output.label': 'Max Output Length',
|
||||
'cfg.safety_max_output.desc': 'Maximum output tokens per response',
|
||||
'cfg.safety_injection_check.label': 'Injection Check',
|
||||
'cfg.safety_injection_check.desc': 'Enable prompt injection detection',
|
||||
|
||||
// Skills settings
|
||||
'cfg.skills_max_active.label': 'Max Active Skills',
|
||||
'cfg.skills_max_active.desc': 'Maximum skills active simultaneously',
|
||||
'cfg.skills_max_tokens.label': 'Max Context Tokens',
|
||||
'cfg.skills_max_tokens.desc': 'Token budget for skill prompts',
|
||||
|
||||
// Search settings
|
||||
'cfg.search_fusion.label': 'Fusion Strategy',
|
||||
'cfg.search_fusion.desc': 'Hybrid search ranking method',
|
||||
|
||||
// Networking settings
|
||||
'cfg.tunnel_provider.label': 'Provider',
|
||||
'cfg.tunnel_provider.desc': 'Public URL tunnel provider',
|
||||
'cfg.tunnel_public_url.label': 'Public URL',
|
||||
'cfg.tunnel_public_url.desc': 'Static public URL (if not using tunnel provider)',
|
||||
'cfg.gateway_rate_limit.label': 'Rate Limit',
|
||||
'cfg.gateway_rate_limit.desc': 'Max chat messages per minute',
|
||||
'cfg.gateway_max_connections.label': 'Max Connections',
|
||||
'cfg.gateway_max_connections.desc': 'Max simultaneous SSE/WS connections',
|
||||
|
||||
// Channels subtab
|
||||
'channels.builtin': 'Built-in Channels',
|
||||
'channels.messaging': 'Messaging Channels',
|
||||
'channels.webGateway': 'Web Gateway',
|
||||
'channels.webGatewayDesc': 'Browser-based chat interface',
|
||||
'channels.httpWebhook': 'HTTP Webhook',
|
||||
'channels.httpWebhookDesc': 'Incoming webhook endpoint for external integrations',
|
||||
'channels.cli': 'CLI',
|
||||
'channels.cliDesc': 'Terminal UI with Ratatui',
|
||||
'channels.repl': 'REPL',
|
||||
'channels.replDesc': 'Simple read-eval-print loop for testing',
|
||||
'channels.configureVia': 'Configure via {env}',
|
||||
'channels.runWith': 'Run with: {cmd}',
|
||||
});
|
||||
|
||||
@@ -29,9 +29,15 @@ I18n.register('zh-CN', {
|
||||
'tab.memory': '记忆',
|
||||
'tab.jobs': '任务',
|
||||
'tab.routines': '定时任务',
|
||||
'tab.settings': '设置',
|
||||
'tab.extensions': '扩展',
|
||||
'tab.skills': '技能',
|
||||
'tab.logs': '日志',
|
||||
'settings.inference': '推理',
|
||||
'settings.agent': '代理',
|
||||
'settings.channels': '频道',
|
||||
'settings.networking': '网络',
|
||||
'settings.mcp': 'MCP',
|
||||
|
||||
// 状态
|
||||
'status.connected': '已连接',
|
||||
@@ -131,10 +137,10 @@ I18n.register('zh-CN', {
|
||||
|
||||
// 扩展标签页
|
||||
'extensions.installed': '已安装扩展',
|
||||
'extensions.available': '可用 WASM 扩展',
|
||||
'extensions.installWasm': '安装 WASM 扩展',
|
||||
'extensions.available': '可用扩展',
|
||||
'extensions.installWasm': '安装扩展',
|
||||
'extensions.noInstalled': '没有安装扩展',
|
||||
'extensions.noAvailable': '没有其他可用的 WASM 扩展',
|
||||
'extensions.noAvailable': '没有其他可用扩展',
|
||||
'extensions.loading': '加载中...',
|
||||
'extensions.install': '安装',
|
||||
'extensions.installing': '安装中...',
|
||||
@@ -156,13 +162,8 @@ I18n.register('zh-CN', {
|
||||
'mcp.addCustom': '添加自定义 MCP 服务器',
|
||||
'mcp.add': '添加',
|
||||
'mcp.addedSuccess': '已添加 MCP 服务器 {name}',
|
||||
|
||||
// 注册工具
|
||||
'tools.registered': '注册工具',
|
||||
'tools.name': '名称',
|
||||
'tools.description': '描述',
|
||||
'tools.empty': '没有注册工具',
|
||||
|
||||
|
||||
|
||||
// 技能标签页
|
||||
'skills.installed': '已安装技能',
|
||||
'skills.noInstalled': '没有安装技能',
|
||||
@@ -302,6 +303,7 @@ I18n.register('zh-CN', {
|
||||
|
||||
// 通用
|
||||
'common.loading': '加载中...',
|
||||
'common.loadFailed': '加载失败',
|
||||
'common.noData': '暂无数据',
|
||||
'common.search': '搜索',
|
||||
'common.add': '添加',
|
||||
@@ -328,6 +330,8 @@ I18n.register('zh-CN', {
|
||||
|
||||
// 扩展
|
||||
'ext.active': '已激活',
|
||||
'ext.inactive': '未激活',
|
||||
'ext.builtin': '内置',
|
||||
'ext.remove': '移除',
|
||||
'ext.install': '安装',
|
||||
'ext.installing': '安装中...',
|
||||
@@ -354,4 +358,160 @@ I18n.register('zh-CN', {
|
||||
'config.autoGenerate': '如果为空则自动生成',
|
||||
'config.save': '保存',
|
||||
'config.cancel': '取消',
|
||||
|
||||
// 设置工具栏
|
||||
'settings.export': '导出',
|
||||
'settings.import': '导入',
|
||||
'settings.searchPlaceholder': '搜索设置...',
|
||||
'settings.exportSuccess': '设置已导出',
|
||||
'settings.exportFailed': '导出失败: {message}',
|
||||
'settings.importSuccess': '设置导入成功',
|
||||
'settings.importFailed': '导入失败: {message}',
|
||||
'settings.restartRequired': '需要重启才能使更改生效。',
|
||||
'settings.restartNow': '立即重启',
|
||||
'settings.noMatchingSettings': '没有匹配 "{query}" 的设置',
|
||||
'settings.noSettings': '未找到设置',
|
||||
'settings.saved': '已保存',
|
||||
'settings.on': '开启',
|
||||
'settings.off': '关闭',
|
||||
'settings.envValue': '环境变量: {value}',
|
||||
'settings.envDefault': '使用环境变量默认值',
|
||||
'settings.useEnvDefault': '使用环境变量默认值',
|
||||
|
||||
// 设置分组
|
||||
'cfg.group.llm': 'LLM 提供商',
|
||||
'cfg.group.embeddings': '嵌入向量',
|
||||
'cfg.group.agent': '代理',
|
||||
'cfg.group.heartbeat': '心跳',
|
||||
'cfg.group.sandbox': '沙箱',
|
||||
'cfg.group.routines': '定时任务',
|
||||
'cfg.group.safety': '安全',
|
||||
'cfg.group.skills': '技能',
|
||||
'cfg.group.search': '搜索',
|
||||
'cfg.group.tunnel': '隧道',
|
||||
'cfg.group.gateway': '网关',
|
||||
|
||||
// 推理设置
|
||||
'cfg.llm_backend.label': '后端',
|
||||
'cfg.llm_backend.desc': 'LLM 推理提供商',
|
||||
'cfg.selected_model.label': '模型',
|
||||
'cfg.selected_model.desc': '所选后端的模型名称或 ID',
|
||||
'cfg.ollama_base_url.label': 'Ollama URL',
|
||||
'cfg.ollama_base_url.desc': 'Ollama API 基础 URL',
|
||||
'cfg.openai_compatible_base_url.label': 'OpenAI 兼容 URL',
|
||||
'cfg.openai_compatible_base_url.desc': 'OpenAI 兼容 API 基础 URL',
|
||||
'cfg.bedrock_region.label': 'Bedrock 区域',
|
||||
'cfg.bedrock_region.desc': 'Bedrock 的 AWS 区域',
|
||||
'cfg.bedrock_cross_region.label': '跨区域',
|
||||
'cfg.bedrock_cross_region.desc': '启用跨区域推理',
|
||||
'cfg.bedrock_profile.label': 'AWS 配置文件',
|
||||
'cfg.bedrock_profile.desc': 'Bedrock 认证的 AWS 配置文件',
|
||||
'cfg.embeddings_enabled.label': '启用',
|
||||
'cfg.embeddings_enabled.desc': '启用向量嵌入以支持记忆搜索',
|
||||
'cfg.embeddings_provider.label': '提供商',
|
||||
'cfg.embeddings_provider.desc': '嵌入向量 API 提供商',
|
||||
'cfg.embeddings_model.label': '模型',
|
||||
'cfg.embeddings_model.desc': '嵌入向量模型名称',
|
||||
|
||||
// 代理设置
|
||||
'cfg.agent_name.label': '名称',
|
||||
'cfg.agent_name.desc': '代理显示名称',
|
||||
'cfg.agent_max_parallel_jobs.label': '最大并行任务数',
|
||||
'cfg.agent_max_parallel_jobs.desc': '最大并发后台任务数',
|
||||
'cfg.agent_job_timeout.label': '任务超时',
|
||||
'cfg.agent_job_timeout.desc': '每个任务的最大持续时间(秒)',
|
||||
'cfg.agent_max_tool_iterations.label': '最大工具迭代次数',
|
||||
'cfg.agent_max_tool_iterations.desc': '每轮最大工具调用次数',
|
||||
'cfg.agent_use_planning.label': '规划',
|
||||
'cfg.agent_use_planning.desc': '执行前启用多步规划',
|
||||
'cfg.agent_auto_approve.label': '自动批准工具',
|
||||
'cfg.agent_auto_approve.desc': '跳过工具调用的手动审批',
|
||||
'cfg.agent_timezone.label': '时区',
|
||||
'cfg.agent_timezone.desc': '默认时区(IANA)',
|
||||
'cfg.agent_session_idle.label': '会话空闲超时',
|
||||
'cfg.agent_session_idle.desc': '空闲会话过期前的秒数',
|
||||
'cfg.agent_stuck_threshold.label': '卡住阈值',
|
||||
'cfg.agent_stuck_threshold.desc': '任务被认为卡住前的秒数',
|
||||
'cfg.agent_max_repair.label': '最大修复尝试次数',
|
||||
'cfg.agent_max_repair.desc': '卡住任务的自动恢复尝试次数',
|
||||
'cfg.agent_max_cost.label': '每日最大费用',
|
||||
'cfg.agent_max_cost.desc': '每日 LLM 支出上限(美分,0 = 无限制)',
|
||||
'cfg.agent_max_actions.label': '每小时最大操作数',
|
||||
'cfg.agent_max_actions.desc': '每小时工具调用速率限制(0 = 无限制)',
|
||||
'cfg.agent_allow_local.label': '允许本地工具',
|
||||
'cfg.agent_allow_local.desc': '启用本地文件系统工具执行',
|
||||
|
||||
// 心跳设置
|
||||
'cfg.heartbeat_enabled.label': '启用',
|
||||
'cfg.heartbeat_enabled.desc': '运行定期后台检查',
|
||||
'cfg.heartbeat_interval.label': '间隔',
|
||||
'cfg.heartbeat_interval.desc': '心跳间隔秒数(默认:1800)',
|
||||
'cfg.heartbeat_notify_channel.label': '通知频道',
|
||||
'cfg.heartbeat_notify_channel.desc': '发送心跳发现的频道',
|
||||
'cfg.heartbeat_notify_user.label': '通知用户',
|
||||
'cfg.heartbeat_notify_user.desc': '要通知的用户 ID',
|
||||
'cfg.heartbeat_quiet_start.label': '静默时段开始',
|
||||
'cfg.heartbeat_quiet_start.desc': '停止心跳的小时(0-23)',
|
||||
'cfg.heartbeat_quiet_end.label': '静默时段结束',
|
||||
'cfg.heartbeat_quiet_end.desc': '恢复心跳的小时(0-23)',
|
||||
'cfg.heartbeat_timezone.label': '时区',
|
||||
'cfg.heartbeat_timezone.desc': '静默时段的时区(IANA)',
|
||||
|
||||
// 沙箱设置
|
||||
'cfg.sandbox_enabled.label': '启用',
|
||||
'cfg.sandbox_enabled.desc': '启用 Docker 沙箱以运行后台任务',
|
||||
'cfg.sandbox_policy.label': '策略',
|
||||
'cfg.sandbox_policy.desc': '沙箱安全策略',
|
||||
'cfg.sandbox_timeout.label': '超时',
|
||||
'cfg.sandbox_timeout.desc': '最大任务持续时间(秒)',
|
||||
'cfg.sandbox_memory.label': '内存限制',
|
||||
'cfg.sandbox_memory.desc': '容器内存限制(MB)',
|
||||
'cfg.sandbox_image.label': 'Docker 镜像',
|
||||
'cfg.sandbox_image.desc': '沙箱任务的容器镜像',
|
||||
|
||||
// 定时任务设置
|
||||
'cfg.routines_max_concurrent.label': '最大并发数',
|
||||
'cfg.routines_max_concurrent.desc': '同时运行的最大定时任务数',
|
||||
'cfg.routines_cooldown.label': '默认冷却时间',
|
||||
'cfg.routines_cooldown.desc': '定时任务触发间的最小秒数',
|
||||
|
||||
// 安全设置
|
||||
'cfg.safety_max_output.label': '最大输出长度',
|
||||
'cfg.safety_max_output.desc': '每次响应的最大输出令牌数',
|
||||
'cfg.safety_injection_check.label': '注入检查',
|
||||
'cfg.safety_injection_check.desc': '启用提示注入检测',
|
||||
|
||||
// 技能设置
|
||||
'cfg.skills_max_active.label': '最大活跃技能数',
|
||||
'cfg.skills_max_active.desc': '同时活跃的最大技能数',
|
||||
'cfg.skills_max_tokens.label': '最大上下文令牌数',
|
||||
'cfg.skills_max_tokens.desc': '技能提示的令牌预算',
|
||||
|
||||
// 搜索设置
|
||||
'cfg.search_fusion.label': '融合策略',
|
||||
'cfg.search_fusion.desc': '混合搜索排名方法',
|
||||
|
||||
// 网络设置
|
||||
'cfg.tunnel_provider.label': '提供商',
|
||||
'cfg.tunnel_provider.desc': '公网 URL 隧道提供商',
|
||||
'cfg.tunnel_public_url.label': '公网 URL',
|
||||
'cfg.tunnel_public_url.desc': '静态公网 URL(不使用隧道提供商时)',
|
||||
'cfg.gateway_rate_limit.label': '速率限制',
|
||||
'cfg.gateway_rate_limit.desc': '每分钟最大聊天消息数',
|
||||
'cfg.gateway_max_connections.label': '最大连接数',
|
||||
'cfg.gateway_max_connections.desc': '最大同时 SSE/WS 连接数',
|
||||
|
||||
// 频道子标签
|
||||
'channels.builtin': '内置频道',
|
||||
'channels.messaging': '消息频道',
|
||||
'channels.webGateway': 'Web 网关',
|
||||
'channels.webGatewayDesc': '基于浏览器的聊天界面',
|
||||
'channels.httpWebhook': 'HTTP Webhook',
|
||||
'channels.httpWebhookDesc': '用于外部集成的传入 webhook 端点',
|
||||
'channels.cli': 'CLI',
|
||||
'channels.cliDesc': '使用 Ratatui 的终端 UI',
|
||||
'channels.repl': 'REPL',
|
||||
'channels.replDesc': '用于测试的简单读取-求值-打印循环',
|
||||
'channels.configureVia': '通过 {env} 配置',
|
||||
'channels.runWith': '运行命令: {cmd}',
|
||||
});
|
||||
|
||||
@@ -95,8 +95,7 @@
|
||||
<button data-tab="memory" data-i18n="tab.memory">Memory</button>
|
||||
<button data-tab="jobs" data-i18n="tab.jobs">Jobs</button>
|
||||
<button data-tab="routines" data-i18n="tab.routines">Routines</button>
|
||||
<button data-tab="extensions" data-i18n="tab.extensions">Extensions</button>
|
||||
<button data-tab="skills" data-i18n="tab.skills">Skills</button>
|
||||
<button data-tab="settings" data-i18n="tab.settings">Settings</button>
|
||||
<div class="spacer"></div>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
@@ -271,77 +270,125 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extensions Tab -->
|
||||
<div class="tab-panel" id="tab-extensions">
|
||||
<div class="extensions-container">
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="extensions.installed">Installed Extensions</h3>
|
||||
<div class="extensions-list" id="extensions-list">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||||
</div>
|
||||
<!-- Settings Tab -->
|
||||
<div class="tab-panel" id="tab-settings">
|
||||
<div class="settings-layout">
|
||||
<div class="settings-sidebar">
|
||||
<button class="settings-subtab active" data-settings-subtab="inference" data-i18n="settings.inference">Inference</button>
|
||||
<button class="settings-subtab" data-settings-subtab="agent" data-i18n="settings.agent">Agent</button>
|
||||
<button class="settings-subtab" data-settings-subtab="channels" data-i18n="settings.channels">Channels</button>
|
||||
<button class="settings-subtab" data-settings-subtab="networking" data-i18n="settings.networking">Networking</button>
|
||||
<button class="settings-subtab" data-settings-subtab="extensions" data-i18n="tab.extensions">Extensions</button>
|
||||
<button class="settings-subtab" data-settings-subtab="mcp" data-i18n="settings.mcp">MCP</button>
|
||||
<button class="settings-subtab" data-settings-subtab="skills" data-i18n="tab.skills">Skills</button>
|
||||
</div>
|
||||
<div class="extensions-section" id="available-wasm-section">
|
||||
<h3 data-i18n="extensions.available">Available WASM Extensions</h3>
|
||||
<div class="extensions-list" id="available-wasm-list">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||||
<div class="settings-content">
|
||||
<div class="settings-toolbar">
|
||||
<div class="settings-search">
|
||||
<input type="text" id="settings-search-input" data-i18n-placeholder="settings.searchPlaceholder" placeholder="Search settings..." data-i18n-attr="aria-label" data-i18n="settings.searchPlaceholder" aria-label="Search settings...">
|
||||
</div>
|
||||
<button id="settings-export-btn" class="settings-toolbar-btn" data-i18n="settings.export">Export</button>
|
||||
<button id="settings-import-btn" class="settings-toolbar-btn" data-i18n="settings.import">Import</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="extensions.installWasm">Install WASM Extension</h3>
|
||||
<div class="ext-install-form">
|
||||
<input type="text" id="wasm-install-name" data-i18n-placeholder="common.name" placeholder="Extension name">
|
||||
<input type="text" id="wasm-install-url" placeholder="URL to .tar.gz bundle">
|
||||
<button id="wasm-install-btn" data-i18n="extensions.install">Install</button>
|
||||
<div class="settings-subpanel active" id="settings-inference">
|
||||
<div class="extensions-container" id="settings-inference-content">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading settings...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="mcp.servers">MCP Servers</h3>
|
||||
<div class="extensions-list" id="mcp-servers-list">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||||
<div class="settings-subpanel" id="settings-agent">
|
||||
<div class="extensions-container" id="settings-agent-content">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading settings...</div>
|
||||
</div>
|
||||
</div>
|
||||
<h4 data-i18n="mcp.addCustom">Add Custom MCP Server</h4>
|
||||
<div class="ext-install-form">
|
||||
<input type="text" id="mcp-install-name" data-i18n-placeholder="common.name" placeholder="Server name">
|
||||
<input type="text" id="mcp-install-url" placeholder="MCP server URL (https://...)">
|
||||
<button id="mcp-add-btn" data-i18n="mcp.add">Add</button>
|
||||
<div class="settings-subpanel" id="settings-channels">
|
||||
<div class="extensions-container" id="settings-channels-content">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading channels...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-subpanel" id="settings-networking">
|
||||
<div class="extensions-container" id="settings-networking-content">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-subpanel" id="settings-extensions">
|
||||
<div class="extensions-container">
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="extensions.installed">Installed Extensions</h3>
|
||||
<div class="extensions-list" id="extensions-list">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extensions-section" id="available-wasm-section">
|
||||
<h3 data-i18n="extensions.available">Available Extensions</h3>
|
||||
<div class="extensions-list" id="available-wasm-list">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="extensions.installWasm">Install Extension</h3>
|
||||
<div class="ext-install-form">
|
||||
<input type="text" id="wasm-install-name" data-i18n-placeholder="common.name" placeholder="Extension name">
|
||||
<input type="text" id="wasm-install-url" placeholder="URL to .tar.gz bundle">
|
||||
<button id="wasm-install-btn" data-i18n="extensions.install">Install</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-subpanel" id="settings-mcp">
|
||||
<div class="extensions-container">
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="mcp.servers">MCP Servers</h3>
|
||||
<div class="extensions-list" id="mcp-servers-list">
|
||||
<div class="empty-state" data-i18n="common.loading">Loading...</div>
|
||||
</div>
|
||||
<h4 data-i18n="mcp.addCustom">Add Custom MCP Server</h4>
|
||||
<div class="ext-install-form">
|
||||
<input type="text" id="mcp-install-name" data-i18n-placeholder="common.name" placeholder="Server name">
|
||||
<input type="text" id="mcp-install-url" placeholder="MCP server URL (https://...)">
|
||||
<button id="mcp-add-btn" data-i18n="mcp.add">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-subpanel" id="settings-skills">
|
||||
<div class="extensions-container">
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="skills.searchClawHub">Search ClawHub</h3>
|
||||
<div class="skill-search-box">
|
||||
<input type="text" id="skill-search-input" data-i18n-placeholder="skills.searchPlaceholder" placeholder="Search for skills...">
|
||||
<button id="skill-search-btn" data-i18n="skills.search">Search</button>
|
||||
</div>
|
||||
<div class="extensions-list" id="skill-search-results"></div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="skills.installed">Installed Skills</h3>
|
||||
<div class="extensions-list" id="skills-list">
|
||||
<div class="empty-state" data-i18n="skills.loading">Loading skills...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="skills.installByUrl">Install Skill by URL</h3>
|
||||
<div class="ext-install-form">
|
||||
<input type="text" id="skill-install-name" data-i18n-placeholder="skills.namePlaceholder" placeholder="Skill name or slug">
|
||||
<input type="text" id="skill-install-url" data-i18n-placeholder="skills.urlPlaceholder" placeholder="HTTPS URL to SKILL.md (optional)">
|
||||
<button id="skill-install-btn" data-i18n="extensions.install">Install</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="tools.registered">Registered Tools</h3>
|
||||
<table class="tools-table" id="tools-table">
|
||||
<thead><tr><th data-i18n="tools.name">Name</th><th data-i18n="tools.description">Description</th></tr></thead>
|
||||
<tbody id="tools-tbody"></tbody>
|
||||
</table>
|
||||
<div class="empty-state" id="tools-empty" style="display:none" data-i18n="tools.empty">No tools registered</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills Tab -->
|
||||
<div class="tab-panel" id="tab-skills">
|
||||
<div class="extensions-container">
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="skills.searchClawHub">Search ClawHub</h3>
|
||||
<div class="skill-search-box">
|
||||
<input type="text" id="skill-search-input" data-i18n-placeholder="skills.searchPlaceholder" placeholder="Search...">
|
||||
<button id="skill-search-btn" data-i18n="skills.search">Search</button>
|
||||
</div>
|
||||
<div class="extensions-list" id="skill-search-results"></div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="skills.installed">Installed Skills</h3>
|
||||
<div class="extensions-list" id="skills-list">
|
||||
<div class="empty-state" data-i18n="skills.loading">Loading skills...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extensions-section">
|
||||
<h3 data-i18n="skills.installByUrl">Install Skill by URL</h3>
|
||||
<div class="ext-install-form">
|
||||
<input type="text" id="skill-install-name" data-i18n-placeholder="skills.namePlaceholder" placeholder="Skill name or slug">
|
||||
<input type="text" id="skill-install-url" data-i18n-placeholder="skills.urlPlaceholder" placeholder="HTTPS URL to SKILL.md (optional)">
|
||||
<button id="skill-install-btn" data-i18n="extensions.install">Install</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Confirmation Modal -->
|
||||
<div id="confirm-modal" class="modal-overlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="confirm-modal-title">
|
||||
<div class="modal">
|
||||
<h3 id="confirm-modal-title"></h3>
|
||||
<p id="confirm-modal-message"></p>
|
||||
<div class="modal-actions">
|
||||
<button id="confirm-modal-cancel-btn" class="btn-secondary" data-i18n="btn.cancel">Cancel</button>
|
||||
<button id="confirm-modal-btn" class="btn-danger">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
--radius-lg: 12px;
|
||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
--font-mono: 'IBM Plex Mono', 'SF Mono', 'Fira Code', Consolas, monospace;
|
||||
--text-muted: #71717a;
|
||||
--bg-hover: rgba(255, 255, 255, 0.03);
|
||||
--danger-soft: rgba(230, 76, 76, 0.15);
|
||||
--warning-soft: rgba(245, 166, 35, 0.15);
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 0.2s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -332,10 +338,10 @@ body {
|
||||
.restart-loader-content {
|
||||
position: relative;
|
||||
z-index: 10000;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
margin: 0 1rem;
|
||||
@@ -352,7 +358,7 @@ body {
|
||||
}
|
||||
|
||||
.restart-title {
|
||||
color: #e0e0e0;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 0;
|
||||
@@ -388,10 +394,10 @@ body {
|
||||
.restart-modal-content {
|
||||
position: relative;
|
||||
z-index: 10000;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
margin: 0 1rem;
|
||||
@@ -403,11 +409,11 @@ body {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.restart-modal-header h2 {
|
||||
color: #e0e0e0;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -426,8 +432,8 @@ body {
|
||||
}
|
||||
|
||||
.restart-modal-close:hover {
|
||||
color: #ccc;
|
||||
background-color: #2a2a2a;
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.restart-modal-body {
|
||||
@@ -435,21 +441,21 @@ body {
|
||||
}
|
||||
|
||||
.restart-modal-description {
|
||||
color: #aaa;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.restart-modal-warning {
|
||||
margin-top: 1rem;
|
||||
background-color: #1e1400;
|
||||
border: 1px solid #3a2a00;
|
||||
background-color: var(--warning-soft);
|
||||
border: 1px solid rgba(245, 166, 35, 0.25);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.restart-modal-warning p {
|
||||
color: #facc15;
|
||||
color: var(--warning);
|
||||
font-size: 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -460,7 +466,7 @@ body {
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.restart-modal-btn {
|
||||
@@ -473,28 +479,28 @@ body {
|
||||
}
|
||||
|
||||
.restart-modal-btn.cancel {
|
||||
color: #ccc;
|
||||
color: var(--text-secondary);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.restart-modal-btn.cancel:hover {
|
||||
background-color: #2a2a2a;
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.restart-modal-btn.confirm {
|
||||
background-color: #00D894;
|
||||
color: #111;
|
||||
background-color: var(--accent);
|
||||
color: #09090b;
|
||||
}
|
||||
|
||||
.restart-modal-btn.confirm:hover {
|
||||
background-color: #00be82;
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Progress Bar for Restart */
|
||||
.restart-progress-bar {
|
||||
width: 100%;
|
||||
height: 0.375rem;
|
||||
background-color: #2a2a2a;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -502,7 +508,7 @@ body {
|
||||
.restart-progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 9999px;
|
||||
background-color: #00D894;
|
||||
background-color: var(--accent);
|
||||
width: 40%;
|
||||
animation: indeterminate 1.5s ease-in-out infinite;
|
||||
}
|
||||
@@ -523,14 +529,14 @@ body {
|
||||
}
|
||||
|
||||
.restart-modal-info {
|
||||
color: #666;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.restart-modal-info a {
|
||||
color: #00D894;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -2522,17 +2528,21 @@ body {
|
||||
}
|
||||
|
||||
.extensions-section h3 {
|
||||
font-size: 15px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text);
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.extensions-section h4 {
|
||||
font-size: 13px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.extensions-list {
|
||||
@@ -2544,12 +2554,29 @@ body {
|
||||
.ext-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid transparent;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
transition: border-color var(--transition-base), box-shadow var(--transition-base), transform 0.2s;
|
||||
}
|
||||
|
||||
.ext-card.state-active {
|
||||
border-left-color: var(--success);
|
||||
}
|
||||
|
||||
.ext-card.state-inactive {
|
||||
border-left-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ext-card.state-error {
|
||||
border-left-color: var(--danger);
|
||||
}
|
||||
|
||||
.ext-card.state-pairing {
|
||||
border-left-color: var(--warning);
|
||||
}
|
||||
|
||||
.ext-card:hover {
|
||||
@@ -2592,6 +2619,11 @@ body {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.ext-kind.kind-builtin {
|
||||
background: rgba(161, 161, 170, 0.15);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ext-version {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
@@ -2767,13 +2799,20 @@ body {
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-ext:hover {
|
||||
background: var(--border);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-ext:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.btn-ext.activate {
|
||||
@@ -2873,6 +2912,7 @@ body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2893,7 +2933,7 @@ body {
|
||||
.configure-modal h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.configure-hint {
|
||||
@@ -3036,31 +3076,6 @@ body {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tools-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.tools-table th,
|
||||
.tools-table td {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tools-table th {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tools-table tr:hover td {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
/* --- Activity tab (unified sandbox job events) --- */
|
||||
|
||||
.activity-terminal {
|
||||
@@ -3714,10 +3729,14 @@ mark {
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.ext-install-form input {
|
||||
padding: 6px 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
@@ -3759,6 +3778,10 @@ mark {
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.skill-search-box input {
|
||||
@@ -3795,10 +3818,10 @@ mark {
|
||||
}
|
||||
|
||||
.skill-trust {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
@@ -3942,6 +3965,27 @@ mark {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Settings layout: horizontal subtabs on mobile */
|
||||
.settings-layout { flex-direction: column; }
|
||||
.settings-sidebar {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0;
|
||||
}
|
||||
.settings-subtab {
|
||||
border-left: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
white-space: nowrap;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.settings-subtab.active {
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Extension install form */
|
||||
.ext-install-form {
|
||||
flex-direction: column;
|
||||
@@ -3968,6 +4012,238 @@ mark {
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Settings Tab Layout --- */
|
||||
.settings-layout {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-sidebar {
|
||||
width: 180px;
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
padding: 12px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-subtab {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.settings-subtab:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.settings-subtab.active {
|
||||
color: var(--accent);
|
||||
border-left-color: var(--accent);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-subpanel {
|
||||
display: none;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.settings-subpanel.active {
|
||||
display: flex;
|
||||
animation: settingsFadeIn 0.2s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes settingsFadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Settings form styles (General subtab) */
|
||||
.settings-group {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-group-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
margin: 0 -12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
border-radius: 6px;
|
||||
gap: 16px;
|
||||
max-height: 80px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease, opacity 0.2s ease, margin 0.2s ease, padding 0.2s ease, background var(--transition-fast);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-row:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.settings-row.hidden {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-row.search-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-row:last-child { border-bottom: none; }
|
||||
|
||||
.settings-label {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.settings-input {
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
width: 240px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.settings-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.15);
|
||||
}
|
||||
|
||||
.settings-saved-indicator {
|
||||
font-size: 11px;
|
||||
color: var(--success);
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-saved-indicator.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.settings-description {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.restart-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: var(--warning-soft);
|
||||
border: 1px solid rgba(245, 166, 35, 0.25);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
margin: 8px 16px;
|
||||
animation: settingsFadeIn 0.25s ease forwards;
|
||||
}
|
||||
|
||||
.restart-banner-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.restart-banner-btn {
|
||||
padding: 4px 12px;
|
||||
background: var(--warning);
|
||||
color: #09090b;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.restart-banner-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.settings-label-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
width: 240px;
|
||||
max-width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.15);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Slash command autocomplete dropdown */
|
||||
.slash-autocomplete {
|
||||
position: relative;
|
||||
@@ -4156,3 +4432,211 @@ mark {
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Settings toolbar (search + import/export) */
|
||||
.settings-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-search {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.settings-search input {
|
||||
width: 100%;
|
||||
padding: 6px 10px 6px 32px;
|
||||
background: var(--bg);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='M21 21l-4.35-4.35'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 10px center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
.settings-search input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.15);
|
||||
}
|
||||
|
||||
.settings-toolbar-btn {
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-toolbar-btn:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.settings-toolbar-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Confirmation modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: modalFadeIn 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from { opacity: 0; transform: translateY(10px) scale(0.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
animation: modalSlideIn 0.2s ease;
|
||||
}
|
||||
|
||||
.modal h3 {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
font-size: 16px;
|
||||
color: var(--text);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.modal p {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
padding: 8px 16px;
|
||||
background: var(--danger);
|
||||
border: 1px solid var(--danger);
|
||||
border-radius: var(--radius);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Mobile settings responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.settings-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
max-height: 140px;
|
||||
}
|
||||
.settings-label-wrap {
|
||||
min-width: unset;
|
||||
}
|
||||
.settings-input, .settings-select {
|
||||
width: 100%;
|
||||
}
|
||||
.settings-toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.settings-search {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading skeletons */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.skeleton-bar {
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(90deg, var(--bg-tertiary) 25%, rgba(255,255,255,0.06) 50%, var(--bg-tertiary) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Settings search empty state */
|
||||
.settings-search-empty {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ impl TestGatewayBuilder {
|
||||
cost_guard: None,
|
||||
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
startup_time: std::time::Instant::now(),
|
||||
active_config: crate::channels::web::server::ActiveConfigSnapshot::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -521,6 +521,7 @@ mod tests {
|
||||
cost_guard: None,
|
||||
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
startup_time: std::time::Instant::now(),
|
||||
active_config: crate::channels::web::server::ActiveConfigSnapshot::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,13 +258,6 @@ impl JobContext {
|
||||
new_state: JobState,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
debug_assert!(
|
||||
self.state.can_transition_to(new_state),
|
||||
"BUG: invalid job state transition {} -> {} for job {}",
|
||||
self.state,
|
||||
new_state,
|
||||
self.job_id
|
||||
);
|
||||
if !self.state.can_transition_to(new_state) {
|
||||
return Err(format!(
|
||||
"Cannot transition from {} to {}",
|
||||
|
||||
+21
@@ -323,6 +323,17 @@ async fn async_main() -> anyhow::Result<()> {
|
||||
}));
|
||||
|
||||
// Load WASM channels and register their webhook routes.
|
||||
// Ensure the channels directory exists so the WASM runtime initializes even when
|
||||
// no channels are installed yet — hot-activation needs the runtime to be available.
|
||||
if config.channels.wasm_channels_enabled
|
||||
&& let Err(e) = std::fs::create_dir_all(&config.channels.wasm_channels_dir)
|
||||
{
|
||||
tracing::warn!(
|
||||
path = %config.channels.wasm_channels_dir.display(),
|
||||
error = %e,
|
||||
"Failed to create WASM channels directory"
|
||||
);
|
||||
}
|
||||
if config.channels.wasm_channels_enabled && config.channels.wasm_channels_dir.exists() {
|
||||
let wasm_result = ironclaw::channels::wasm::setup_wasm_channels(
|
||||
&config,
|
||||
@@ -511,6 +522,16 @@ async fn async_main() -> anyhow::Result<()> {
|
||||
gw = gw.with_skill_catalog(Arc::clone(sc));
|
||||
}
|
||||
gw = gw.with_cost_guard(Arc::clone(&components.cost_guard));
|
||||
{
|
||||
let active_model = components.llm.model_name().to_string();
|
||||
let mut enabled = channel_names.clone();
|
||||
enabled.push("gateway".into());
|
||||
gw = gw.with_active_config(ironclaw::channels::web::server::ActiveConfigSnapshot {
|
||||
llm_backend: config.llm.backend.to_string(),
|
||||
llm_model: active_model,
|
||||
enabled_channels: enabled,
|
||||
});
|
||||
}
|
||||
if config.sandbox.enabled {
|
||||
gw = gw.with_prompt_queue(Arc::clone(&prompt_queue));
|
||||
|
||||
|
||||
+11
-7
@@ -22,10 +22,6 @@ pub async fn execute_tool_with_safety(
|
||||
params: &serde_json::Value,
|
||||
job_ctx: &JobContext,
|
||||
) -> Result<String, Error> {
|
||||
debug_assert!(
|
||||
!tool_name.is_empty(),
|
||||
"BUG: execute_tool_with_safety called with empty tool_name"
|
||||
);
|
||||
let tool = tools
|
||||
.get(tool_name)
|
||||
.await
|
||||
@@ -297,8 +293,8 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_execute_empty_tool_name_returns_not_found() {
|
||||
// Regression: execute_tool_with_safety must reject empty tool names before
|
||||
// even attempting a registry lookup (the debug_assert guards this invariant).
|
||||
// Regression: execute_tool_with_safety must reject empty tool names
|
||||
// gracefully via ToolError::NotFound (not a panic).
|
||||
let registry = registry_with(vec![]).await;
|
||||
let safety = test_safety();
|
||||
|
||||
@@ -311,7 +307,15 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Empty tool name should return an error"); // safety: test-only assertion
|
||||
assert!(
|
||||
matches!(
|
||||
result,
|
||||
Err(crate::error::Error::Tool(
|
||||
crate::error::ToolError::NotFound { .. }
|
||||
))
|
||||
),
|
||||
"Empty tool name should return ToolError::NotFound, got: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
+11
-4
@@ -45,12 +45,13 @@ SEL = {
|
||||
"approval_always_btn": ".approval-actions button.always",
|
||||
"approval_deny_btn": ".approval-actions button.deny",
|
||||
"approval_resolved": ".approval-resolved",
|
||||
# Extensions tab – sections
|
||||
# Settings subtabs
|
||||
"settings_subtab": '.settings-subtab[data-settings-subtab="{subtab}"]',
|
||||
"settings_subpanel": "#settings-{subtab}",
|
||||
# Extensions section
|
||||
"extensions_list": "#extensions-list",
|
||||
"available_wasm_list": "#available-wasm-list",
|
||||
"mcp_servers_list": "#mcp-servers-list",
|
||||
"tools_tbody": "#tools-tbody",
|
||||
"tools_empty": "#tools-empty",
|
||||
# Extensions tab – cards
|
||||
"ext_card_installed": "#extensions-list .ext-card",
|
||||
"ext_card_available": "#available-wasm-list .ext-card.ext-available",
|
||||
@@ -92,6 +93,12 @@ SEL = {
|
||||
"ext_stepper": ".ext-stepper",
|
||||
"stepper_step": ".stepper-step",
|
||||
"stepper_circle": ".stepper-circle",
|
||||
# Confirm modal (custom, replaces window.confirm)
|
||||
"confirm_modal": "#confirm-modal",
|
||||
"confirm_modal_btn": "#confirm-modal-btn",
|
||||
"confirm_modal_cancel": "#confirm-modal-cancel-btn",
|
||||
# Channels subtab – cards
|
||||
"channels_ext_card": "#settings-channels-content .ext-card",
|
||||
# Toast notifications
|
||||
"toast": ".toast",
|
||||
"toast_success": ".toast.toast-success",
|
||||
@@ -106,7 +113,7 @@ SEL = {
|
||||
"routines_empty": "#routines-empty",
|
||||
}
|
||||
|
||||
TABS = ["chat", "memory", "jobs", "routines", "extensions", "skills"]
|
||||
TABS = ["chat", "memory", "jobs", "routines", "settings"]
|
||||
|
||||
# Auth token used across all tests
|
||||
AUTH_TOKEN = "e2e-test-token"
|
||||
|
||||
@@ -87,23 +87,21 @@ _REGISTRY_MCP = {
|
||||
"installed": False,
|
||||
}
|
||||
|
||||
_SAMPLE_TOOL = {"name": "echo", "description": "Echo a message"}
|
||||
_SAMPLE_TOOL_2 = {"name": "time", "description": "Get current time"}
|
||||
|
||||
|
||||
# ─── Navigation helpers ────────────────────────────────────────────────────────
|
||||
|
||||
async def go_to_extensions(page):
|
||||
"""Click the Extensions tab and wait for the panel to appear.
|
||||
"""Navigate to Settings > Extensions subtab and wait for content.
|
||||
|
||||
Waits for loadExtensions() to finish rendering by polling for the first
|
||||
content signal (empty-state div or an installed card) rather than sleeping.
|
||||
"""
|
||||
await page.locator(SEL["tab_button"].format(tab="extensions")).click()
|
||||
await page.locator(SEL["tab_panel"].format(tab="extensions")).wait_for(
|
||||
await page.locator(SEL["tab_button"].format(tab="settings")).click()
|
||||
await page.locator(SEL["settings_subtab"].format(subtab="extensions")).click()
|
||||
await page.locator(SEL["settings_subpanel"].format(subtab="extensions")).wait_for(
|
||||
state="visible", timeout=5000
|
||||
)
|
||||
# loadExtensions() fires three parallel fetches then renders. Wait for the
|
||||
# loadExtensions() fires parallel fetches then renders. Wait for the
|
||||
# first concrete DOM signal instead of a hard sleep so the test is
|
||||
# deterministic even under CI load.
|
||||
await page.locator(
|
||||
@@ -111,19 +109,39 @@ async def go_to_extensions(page):
|
||||
).first.wait_for(state="visible", timeout=8000)
|
||||
|
||||
|
||||
async def mock_ext_apis(page, *, installed=None, tools=None, registry=None):
|
||||
"""Intercept the three extension list APIs with fixture data.
|
||||
async def go_to_channels(page):
|
||||
"""Navigate to Settings > Channels subtab and wait for content."""
|
||||
await page.locator(SEL["tab_button"].format(tab="settings")).click()
|
||||
await page.locator(SEL["settings_subtab"].format(subtab="channels")).click()
|
||||
await page.locator(SEL["settings_subpanel"].format(subtab="channels")).wait_for(
|
||||
state="visible", timeout=5000
|
||||
)
|
||||
|
||||
Must be called BEFORE navigating to the extensions tab.
|
||||
|
||||
async def go_to_mcp(page):
|
||||
"""Navigate to Settings > MCP subtab and wait for content."""
|
||||
await page.locator(SEL["tab_button"].format(tab="settings")).click()
|
||||
await page.locator(SEL["settings_subtab"].format(subtab="mcp")).click()
|
||||
await page.locator(SEL["settings_subpanel"].format(subtab="mcp")).wait_for(
|
||||
state="visible", timeout=5000
|
||||
)
|
||||
await page.locator(
|
||||
f"{SEL['mcp_servers_list']} .empty-state, {SEL['ext_card_mcp']}"
|
||||
).first.wait_for(state="visible", timeout=8000)
|
||||
|
||||
|
||||
async def mock_ext_apis(page, *, installed=None, registry=None):
|
||||
"""Intercept the extension list APIs with fixture data.
|
||||
|
||||
Must be called BEFORE navigating to the extensions subtab.
|
||||
"""
|
||||
ext_body = json.dumps({"extensions": installed or []})
|
||||
tools_body = json.dumps({"tools": tools or []})
|
||||
registry_body = json.dumps({"entries": registry or []})
|
||||
|
||||
# Playwright evaluates route handlers in LIFO order (last-registered fires
|
||||
# first). Register the broad handler first so it is checked last; the
|
||||
# specific /tools and /registry handlers are registered after and therefore
|
||||
# checked first — no continue_() fallthrough needed.
|
||||
# specific /registry handler is registered after and therefore checked
|
||||
# first — no continue_() fallthrough needed.
|
||||
async def handle_ext_list(route):
|
||||
path = route.request.url.split("?")[0]
|
||||
if path.endswith("/api/extensions"):
|
||||
@@ -133,13 +151,9 @@ async def mock_ext_apis(page, *, installed=None, tools=None, registry=None):
|
||||
|
||||
await page.route("**/api/extensions*", handle_ext_list)
|
||||
|
||||
async def handle_tools(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body=tools_body)
|
||||
|
||||
async def handle_registry(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body=registry_body)
|
||||
|
||||
await page.route("**/api/extensions/tools", handle_tools)
|
||||
await page.route("**/api/extensions/registry", handle_registry)
|
||||
|
||||
|
||||
@@ -151,46 +165,17 @@ async def wait_for_toast(page, text: str, *, timeout: int = 5000):
|
||||
# ─── Group A: Structural / empty state ────────────────────────────────────────
|
||||
|
||||
async def test_extensions_empty_tab_layout(page):
|
||||
"""Extensions tab with no data shows all three sections with correct empty-state messages."""
|
||||
await mock_ext_apis(page, tools=[])
|
||||
"""Extensions subtab with no data shows sections with correct empty-state messages."""
|
||||
await mock_ext_apis(page)
|
||||
await go_to_extensions(page)
|
||||
|
||||
panel = page.locator(SEL["tab_panel"].format(tab="extensions"))
|
||||
panel = page.locator(SEL["settings_subpanel"].format(subtab="extensions"))
|
||||
assert await panel.is_visible()
|
||||
|
||||
ext_list = page.locator(SEL["extensions_list"])
|
||||
assert await ext_list.is_visible()
|
||||
assert "No extensions installed" in await ext_list.text_content()
|
||||
|
||||
wasm_list = page.locator(SEL["available_wasm_list"])
|
||||
assert await wasm_list.is_visible()
|
||||
assert "No additional WASM extensions available" in await wasm_list.text_content()
|
||||
|
||||
mcp_list = page.locator(SEL["mcp_servers_list"])
|
||||
assert await mcp_list.is_visible()
|
||||
assert "No MCP servers available" in await mcp_list.text_content()
|
||||
|
||||
# Tools table should be empty
|
||||
tbody = page.locator(SEL["tools_tbody"])
|
||||
rows = await tbody.locator("tr").count()
|
||||
empty_visible = await page.locator(SEL["tools_empty"]).is_visible()
|
||||
assert empty_visible or rows == 0, "Expected tools table to be empty"
|
||||
|
||||
|
||||
async def test_extensions_tools_table_populated(page):
|
||||
"""Two mock tools produce two rows in the tools table."""
|
||||
await mock_ext_apis(page, tools=[_SAMPLE_TOOL, _SAMPLE_TOOL_2])
|
||||
await go_to_extensions(page)
|
||||
|
||||
tbody = page.locator(SEL["tools_tbody"])
|
||||
rows = tbody.locator("tr")
|
||||
await rows.first.wait_for(state="visible", timeout=5000)
|
||||
assert await rows.count() == 2
|
||||
|
||||
text = await tbody.text_content()
|
||||
assert "echo" in text
|
||||
assert "time" in text
|
||||
|
||||
|
||||
# ─── Group B: Installed WASM tool cards ───────────────────────────────────────
|
||||
|
||||
@@ -248,9 +233,9 @@ async def test_installed_wasm_tool_authed_shows_reconfigure_btn(page):
|
||||
async def test_installed_mcp_server_active(page):
|
||||
"""Active MCP server shows 'Active' label and no Activate button."""
|
||||
await mock_ext_apis(page, installed=[_MCP_ACTIVE])
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
card = page.locator(SEL["ext_card_installed"]).first
|
||||
card = page.locator(SEL["ext_card_mcp"]).first
|
||||
await card.wait_for(state="visible", timeout=5000)
|
||||
assert await card.locator(SEL["ext_active_label"]).count() == 1
|
||||
assert await card.locator(SEL["ext_activate_btn"]).count() == 0
|
||||
@@ -260,9 +245,9 @@ async def test_installed_mcp_server_active(page):
|
||||
async def test_installed_mcp_server_inactive_shows_activate(page):
|
||||
"""Inactive MCP server shows Activate button."""
|
||||
await mock_ext_apis(page, installed=[_MCP_INACTIVE])
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
card = page.locator(SEL["ext_card_installed"]).first
|
||||
card = page.locator(SEL["ext_card_mcp"]).first
|
||||
await card.wait_for(state="visible", timeout=5000)
|
||||
assert await card.locator(SEL["ext_activate_btn"]).count() == 1
|
||||
|
||||
@@ -270,7 +255,7 @@ async def test_installed_mcp_server_inactive_shows_activate(page):
|
||||
async def test_mcp_server_in_registry_not_installed(page):
|
||||
"""Registry MCP entry (not installed) appears in the MCP section with Install button."""
|
||||
await mock_ext_apis(page, registry=[_REGISTRY_MCP])
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
mcp_list = page.locator(SEL["mcp_servers_list"])
|
||||
card = mcp_list.locator(".ext-card").first
|
||||
@@ -285,7 +270,7 @@ async def test_mcp_server_installed_auth_dot(page):
|
||||
installed_mcp = {**_MCP_ACTIVE, "name": "registry-mcp", "authenticated": False}
|
||||
registry_mcp = {**_REGISTRY_MCP, "name": "registry-mcp"}
|
||||
await mock_ext_apis(page, installed=[installed_mcp], registry=[registry_mcp])
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
mcp_list = page.locator(SEL["mcp_servers_list"])
|
||||
card = mcp_list.locator(".ext-card").first
|
||||
@@ -299,8 +284,9 @@ async def test_mcp_server_installed_auth_dot(page):
|
||||
async def _load_wasm_channel(page, activation_status, activation_error=None):
|
||||
ext = {**_WASM_CHANNEL, "activation_status": activation_status, "activation_error": activation_error}
|
||||
await mock_ext_apis(page, installed=[ext])
|
||||
await go_to_extensions(page)
|
||||
card = page.locator(SEL["ext_card_installed"]).first
|
||||
await go_to_channels(page)
|
||||
# Find the WASM channel card specifically (not built-in channel cards)
|
||||
card = page.locator(SEL["channels_ext_card"], has_text="Test Channel").first
|
||||
await card.wait_for(state="visible", timeout=5000)
|
||||
return card
|
||||
|
||||
@@ -446,9 +432,9 @@ async def test_install_wasm_channel_triggers_configure(page):
|
||||
|
||||
await page.route("**/api/extensions/test-channel/setup", handle_channel_setup)
|
||||
await page.route("**/api/extensions/install", handle_channel_install)
|
||||
await go_to_extensions(page)
|
||||
await go_to_channels(page)
|
||||
|
||||
install_btn = page.locator(SEL["available_wasm_list"]).locator(SEL["ext_install_btn"]).first
|
||||
install_btn = page.locator(SEL["channels_ext_card"]).locator(SEL["ext_install_btn"]).first
|
||||
await install_btn.wait_for(state="visible", timeout=5000)
|
||||
await install_btn.click()
|
||||
|
||||
@@ -523,13 +509,14 @@ async def test_remove_installed_extension_confirmed(page):
|
||||
# Override for subsequent calls
|
||||
await page.route("**/api/extensions*", handle_ext_empty)
|
||||
|
||||
# Auto-accept confirm dialog
|
||||
await page.evaluate("window.confirm = () => true")
|
||||
|
||||
card = page.locator(SEL["ext_card_installed"]).first
|
||||
await card.wait_for(state="visible", timeout=5000)
|
||||
await card.locator(SEL["ext_remove_btn"]).click()
|
||||
|
||||
# Confirm via custom modal
|
||||
await page.locator(SEL["confirm_modal"]).wait_for(state="visible", timeout=5000)
|
||||
await page.locator(SEL["confirm_modal_btn"]).click()
|
||||
|
||||
# Card should disappear
|
||||
await page.wait_for_function(
|
||||
"() => document.querySelectorAll('#extensions-list .ext-card').length === 0",
|
||||
@@ -543,13 +530,14 @@ async def test_remove_cancelled_keeps_card(page):
|
||||
await mock_ext_apis(page, installed=[_WASM_TOOL])
|
||||
await go_to_extensions(page)
|
||||
|
||||
# Reject the confirm dialog
|
||||
await page.evaluate("window.confirm = () => false")
|
||||
|
||||
card = page.locator(SEL["ext_card_installed"]).first
|
||||
await card.wait_for(state="visible", timeout=5000)
|
||||
await card.locator(SEL["ext_remove_btn"]).click()
|
||||
|
||||
# Cancel via custom modal
|
||||
await page.locator(SEL["confirm_modal"]).wait_for(state="visible", timeout=5000)
|
||||
await page.locator(SEL["confirm_modal_cancel"]).click()
|
||||
|
||||
assert await page.locator(SEL["ext_card_installed"]).count() >= 1, "Card should remain after cancel"
|
||||
|
||||
|
||||
@@ -973,14 +961,10 @@ async def test_auth_completed_failure_sse_shows_error_toast_and_reloads_extensio
|
||||
else:
|
||||
await route.continue_()
|
||||
|
||||
async def handle_tools(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body='{"tools":[]}')
|
||||
|
||||
async def handle_registry(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body='{"entries":[]}')
|
||||
|
||||
await page.route("**/api/extensions*", counting_handler)
|
||||
await page.route("**/api/extensions/tools", handle_tools)
|
||||
await page.route("**/api/extensions/registry", handle_registry)
|
||||
|
||||
await go_to_extensions(page)
|
||||
@@ -989,6 +973,9 @@ async def test_auth_completed_failure_sse_shows_error_toast_and_reloads_extensio
|
||||
await _show_auth_card(page, extension_name="gmail", auth_url="https://example.com/oauth")
|
||||
assert await page.locator(SEL["auth_card"] + '[data-extension-name="gmail"]').count() == 1
|
||||
|
||||
# Inject a counter to confirm refreshCurrentSettingsTab is called
|
||||
await page.evaluate("window.__refreshCount = 0; var _origRefresh = refreshCurrentSettingsTab; refreshCurrentSettingsTab = function() { window.__refreshCount++; _origRefresh(); };")
|
||||
|
||||
await page.evaluate("""
|
||||
handleAuthCompleted({
|
||||
extension_name: 'gmail',
|
||||
@@ -999,14 +986,11 @@ async def test_auth_completed_failure_sse_shows_error_toast_and_reloads_extensio
|
||||
|
||||
await wait_for_toast(page, "OAuth flow expired. Please try again.")
|
||||
assert await page.locator(SEL["auth_card"] + '[data-extension-name="gmail"]').count() == 0
|
||||
assert (
|
||||
await page.locator(
|
||||
SEL["toast_error"], has_text="OAuth flow expired. Please try again."
|
||||
).count()
|
||||
>= 1
|
||||
)
|
||||
|
||||
await page.wait_for_timeout(600)
|
||||
# Wait for the refresh to complete
|
||||
await page.wait_for_function("() => window.__refreshCount > 0", timeout=5000)
|
||||
# Give the async fetch time to complete
|
||||
await page.wait_for_timeout(1000)
|
||||
assert len(reload_count) > count_before, "Extensions list did not reload after auth failure"
|
||||
|
||||
|
||||
@@ -1026,9 +1010,9 @@ async def test_activate_mcp_server_success(page):
|
||||
|
||||
await mock_ext_apis(page, installed=[_MCP_INACTIVE])
|
||||
await page.route("**/api/extensions/test-mcp-inactive/activate", handle_activate)
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
activate_btn = page.locator(SEL["ext_card_installed"]).first.locator(SEL["ext_activate_btn"])
|
||||
activate_btn = page.locator(SEL["ext_card_mcp"]).first.locator(SEL["ext_activate_btn"])
|
||||
await activate_btn.wait_for(state="visible", timeout=5000)
|
||||
|
||||
async with page.expect_response("**/api/extensions/test-mcp-inactive/activate", timeout=5000):
|
||||
@@ -1051,9 +1035,9 @@ async def test_activate_awaiting_token_opens_configure(page):
|
||||
|
||||
await page.route("**/api/extensions/test-mcp-inactive/activate", handle_activate)
|
||||
await page.route("**/api/extensions/test-mcp-inactive/setup", handle_setup)
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
activate_btn = page.locator(SEL["ext_card_installed"]).first.locator(SEL["ext_activate_btn"])
|
||||
activate_btn = page.locator(SEL["ext_card_mcp"]).first.locator(SEL["ext_activate_btn"])
|
||||
await activate_btn.wait_for(state="visible", timeout=5000)
|
||||
await activate_btn.click()
|
||||
|
||||
@@ -1070,9 +1054,9 @@ async def test_activate_failure_shows_error_toast(page):
|
||||
await route.fulfill(status=200, content_type="application/json", body=json.dumps({"success": False, "message": "Config missing"}))
|
||||
|
||||
await page.route("**/api/extensions/test-mcp-inactive/activate", handle_activate)
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
activate_btn = page.locator(SEL["ext_card_installed"]).first.locator(SEL["ext_activate_btn"])
|
||||
activate_btn = page.locator(SEL["ext_card_mcp"]).first.locator(SEL["ext_activate_btn"])
|
||||
await activate_btn.wait_for(state="visible", timeout=5000)
|
||||
await activate_btn.click()
|
||||
|
||||
@@ -1088,9 +1072,9 @@ async def test_activate_with_auth_url_opens_popup_and_shows_auth_prompt(page):
|
||||
await route.fulfill(status=200, content_type="application/json", body=json.dumps({"success": True, "auth_url": "https://example.com/oauth"}))
|
||||
|
||||
await page.route("**/api/extensions/test-mcp-inactive/activate", handle_activate)
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
activate_btn = page.locator(SEL["ext_card_installed"]).first.locator(SEL["ext_activate_btn"])
|
||||
activate_btn = page.locator(SEL["ext_card_mcp"]).first.locator(SEL["ext_activate_btn"])
|
||||
await activate_btn.wait_for(state="visible", timeout=5000)
|
||||
await activate_btn.click()
|
||||
|
||||
@@ -1106,7 +1090,7 @@ async def test_activate_with_auth_url_opens_popup_and_shows_auth_prompt(page):
|
||||
# ─── Group J: Tab reload behaviour ────────────────────────────────────────────
|
||||
|
||||
async def test_extensions_tab_reloads_on_revisit(page):
|
||||
"""loadExtensions() is called again when re-navigating to the extensions tab."""
|
||||
"""loadExtensions() is called again when re-navigating to the extensions subtab."""
|
||||
call_count = []
|
||||
|
||||
async def counting_handler(route):
|
||||
@@ -1121,14 +1105,10 @@ async def test_extensions_tab_reloads_on_revisit(page):
|
||||
else:
|
||||
await route.continue_()
|
||||
|
||||
async def handle_tools(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body='{"tools":[]}')
|
||||
|
||||
async def handle_registry(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body='{"entries":[]}')
|
||||
|
||||
await page.route("**/api/extensions*", counting_handler)
|
||||
await page.route("**/api/extensions/tools", handle_tools)
|
||||
await page.route("**/api/extensions/registry", handle_registry)
|
||||
|
||||
# First visit
|
||||
@@ -1148,48 +1128,6 @@ async def test_extensions_tab_reloads_on_revisit(page):
|
||||
assert count_after_second > count_after_first, "loadExtensions not called on return visit"
|
||||
|
||||
|
||||
async def test_auth_completed_sse_triggers_extensions_reload(page):
|
||||
"""auth_completed SSE event while on the extensions tab triggers a reload."""
|
||||
reload_count = []
|
||||
|
||||
async def counting_handler(route):
|
||||
path = route.request.url.split("?")[0]
|
||||
if path.endswith("/api/extensions"):
|
||||
reload_count.append(1)
|
||||
await route.fulfill(
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
body=json.dumps({"extensions": []}),
|
||||
)
|
||||
else:
|
||||
await route.continue_()
|
||||
|
||||
async def handle_tools(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body='{"tools":[]}')
|
||||
|
||||
async def handle_registry(route):
|
||||
await route.fulfill(status=200, content_type="application/json", body='{"entries":[]}')
|
||||
|
||||
await page.route("**/api/extensions*", counting_handler)
|
||||
await page.route("**/api/extensions/tools", handle_tools)
|
||||
await page.route("**/api/extensions/registry", handle_registry)
|
||||
|
||||
await go_to_extensions(page)
|
||||
count_before = len(reload_count)
|
||||
|
||||
# Simulate auth_completed via the shared handler.
|
||||
await page.evaluate("""
|
||||
handleAuthCompleted({
|
||||
extension_name: 'reload-ext',
|
||||
success: true,
|
||||
message: 'Reloaded.',
|
||||
});
|
||||
""")
|
||||
|
||||
await page.wait_for_timeout(600)
|
||||
assert len(reload_count) > count_before, "loadExtensions was not called after auth_completed"
|
||||
|
||||
|
||||
# ─── Regression tests ─────────────────────────────────────────────────────────
|
||||
# Each test below is a regression for a specific bug found after the initial
|
||||
# test suite was written. The bug description is in the docstring.
|
||||
@@ -1267,9 +1205,9 @@ async def test_oauth_url_injection_blocked(page):
|
||||
)
|
||||
|
||||
await page.route("**/api/extensions/test-mcp-inactive/activate", handle_activate)
|
||||
await go_to_extensions(page)
|
||||
await go_to_mcp(page)
|
||||
|
||||
activate_btn = page.locator(SEL["ext_card_installed"]).first.locator(SEL["ext_activate_btn"])
|
||||
activate_btn = page.locator(SEL["ext_card_mcp"]).first.locator(SEL["ext_activate_btn"])
|
||||
await activate_btn.wait_for(state="visible", timeout=5000)
|
||||
await activate_btn.click()
|
||||
|
||||
|
||||
@@ -4,11 +4,18 @@ import pytest
|
||||
from helpers import SEL
|
||||
|
||||
|
||||
async def go_to_skills(page):
|
||||
"""Navigate to Settings > Skills subtab."""
|
||||
await page.locator(SEL["tab_button"].format(tab="settings")).click()
|
||||
await page.locator(SEL["settings_subtab"].format(subtab="skills")).click()
|
||||
await page.locator(SEL["settings_subpanel"].format(subtab="skills")).wait_for(
|
||||
state="visible", timeout=5000
|
||||
)
|
||||
|
||||
|
||||
async def test_skills_tab_visible(page):
|
||||
"""Skills tab shows the search interface."""
|
||||
await page.locator(SEL["tab_button"].format(tab="skills")).click()
|
||||
panel = page.locator(SEL["tab_panel"].format(tab="skills"))
|
||||
await panel.wait_for(state="visible", timeout=5000)
|
||||
"""Skills subtab shows the search interface."""
|
||||
await go_to_skills(page)
|
||||
|
||||
search_input = page.locator(SEL["skill_search_input"])
|
||||
assert await search_input.is_visible(), "Skills search input not visible"
|
||||
@@ -16,7 +23,7 @@ async def test_skills_tab_visible(page):
|
||||
|
||||
async def test_skills_search(page):
|
||||
"""Search ClawHub for skills and verify results appear."""
|
||||
await page.locator(SEL["tab_button"].format(tab="skills")).click()
|
||||
await go_to_skills(page)
|
||||
|
||||
search_input = page.locator(SEL["skill_search_input"])
|
||||
await search_input.fill("markdown")
|
||||
@@ -35,7 +42,7 @@ async def test_skills_search(page):
|
||||
|
||||
async def test_skills_install_and_remove(page):
|
||||
"""Install a skill from search results, then remove it."""
|
||||
await page.locator(SEL["tab_button"].format(tab="skills")).click()
|
||||
await go_to_skills(page)
|
||||
|
||||
# Search
|
||||
search_input = page.locator(SEL["skill_search_input"])
|
||||
@@ -68,10 +75,14 @@ async def test_skills_install_and_remove(page):
|
||||
installed_count = await installed.count()
|
||||
assert installed_count >= 1, "Skill should appear in installed list after install"
|
||||
|
||||
# Remove the skill (confirm is already overridden)
|
||||
# Remove the skill via confirm modal
|
||||
remove_btn = installed.first.locator("button", has_text="Remove")
|
||||
if await remove_btn.count() > 0:
|
||||
await remove_btn.click()
|
||||
# Confirm in the modal
|
||||
confirm_btn = page.locator(SEL["confirm_modal_btn"])
|
||||
await confirm_btn.wait_for(state="visible", timeout=5000)
|
||||
await confirm_btn.click()
|
||||
# Wait for the card to disappear or list to shrink
|
||||
await page.wait_for_timeout(3000)
|
||||
new_count = await page.locator(SEL["skill_installed"]).count()
|
||||
|
||||
@@ -507,10 +507,10 @@ async def test_configure_noninstalled(ironclaw_server):
|
||||
|
||||
|
||||
async def test_extensions_tab_shows_registry(page):
|
||||
"""Extensions tab loads and shows available extensions from registry."""
|
||||
tab_btn = page.locator(SEL["tab_button"].format(tab="extensions"))
|
||||
await tab_btn.click()
|
||||
panel = page.locator(SEL["tab_panel"].format(tab="extensions"))
|
||||
"""Extensions subtab loads and shows available extensions from registry."""
|
||||
await page.locator(SEL["tab_button"].format(tab="settings")).click()
|
||||
await page.locator(SEL["settings_subtab"].format(subtab="extensions")).click()
|
||||
panel = page.locator(SEL["settings_subpanel"].format(subtab="extensions"))
|
||||
await panel.wait_for(state="visible", timeout=5000)
|
||||
|
||||
available_section = page.locator(SEL["available_wasm_list"])
|
||||
|
||||
@@ -214,6 +214,7 @@ async fn start_test_server_with_provider(
|
||||
cost_guard: None,
|
||||
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
startup_time: std::time::Instant::now(),
|
||||
active_config: ironclaw::channels::web::server::ActiveConfigSnapshot::default(),
|
||||
});
|
||||
|
||||
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
@@ -705,6 +706,7 @@ async fn test_no_llm_provider_returns_503() {
|
||||
cost_guard: None,
|
||||
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
startup_time: std::time::Instant::now(),
|
||||
active_config: ironclaw::channels::web::server::ActiveConfigSnapshot::default(),
|
||||
});
|
||||
|
||||
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
|
||||
@@ -234,6 +234,7 @@ impl GatewayWorkflowHarness {
|
||||
cost_guard: Some(Arc::clone(&components.cost_guard)),
|
||||
routine_engine: Arc::clone(&routine_slot),
|
||||
startup_time: Instant::now(),
|
||||
active_config: ironclaw::channels::web::server::ActiveConfigSnapshot::default(),
|
||||
});
|
||||
|
||||
let mut agent = Agent::new(
|
||||
|
||||
@@ -62,6 +62,7 @@ async fn start_test_server() -> (
|
||||
cost_guard: None,
|
||||
routine_engine: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
startup_time: std::time::Instant::now(),
|
||||
active_config: ironclaw::channels::web::server::ActiveConfigSnapshot::default(),
|
||||
});
|
||||
|
||||
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user