mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 05:50:44 +08:00
统一技能安装与 Skills 市场接入链路。
新增公开安装工具与多来源 provider 支持,补齐管理端与目录加载逻辑,并同步更新相关测试与文档以保证可见性和路径安全。 Made-with: Cursor
This commit is contained in:
parent
d5e30542aa
commit
4d9232f3b3
66 changed files with 7369 additions and 1099 deletions
|
|
@ -15,6 +15,7 @@ from oclaw.runtime.skill_installer import (
|
|||
install_skill_from_local_dir,
|
||||
install_skill_from_registry_archive,
|
||||
list_skills_with_status,
|
||||
repair_skill_dependencies,
|
||||
set_skill_enabled,
|
||||
uninstall_skill,
|
||||
)
|
||||
|
|
@ -29,11 +30,13 @@ from oclaw.runtime.skill_role_binding import (
|
|||
)
|
||||
from oclaw.runtime.skills_prompt import collect_skill_catalog_entries
|
||||
from oclaw.runtime.skills import _allowed_tool_names_after_wire_policy, discover_workspace_skill_manifests
|
||||
from oclaw.runtime.skills_market import get_market_adapter
|
||||
from oclaw.runtime.skills_market import get_market_adapter, normalize_skill_market_provider_setting
|
||||
from oclaw.runtime.tools.skills_runtime.subprocess_exec import run_skill_runtime_entry
|
||||
from oclaw.platform.config.paths import db_path
|
||||
from oclaw.platform.persistence.sqlite_store import SqliteStore
|
||||
|
||||
_SKILL_MARKET_PROVIDER_KEY = "AIA_SKILL_MARKET_PROVIDER"
|
||||
|
||||
|
||||
def include_skill_routes(
|
||||
router: APIRouter,
|
||||
|
|
@ -95,7 +98,13 @@ def include_skill_routes(
|
|||
raw_toolcall = str(store.get_setting("AIA_SKILL_TOOLCALL_ENABLED") or "").strip().lower()
|
||||
prompt_in_system = raw_prompt not in {"0", "false", "no", "off"}
|
||||
toolcall_enabled = raw_toolcall in {"1", "true", "yes", "on"}
|
||||
return {"ok": True, "prompt_in_system": bool(prompt_in_system), "toolcall_enabled": bool(toolcall_enabled)}
|
||||
market_provider = normalize_skill_market_provider_setting(str(store.get_setting(_SKILL_MARKET_PROVIDER_KEY) or ""))
|
||||
return {
|
||||
"ok": True,
|
||||
"prompt_in_system": bool(prompt_in_system),
|
||||
"toolcall_enabled": bool(toolcall_enabled),
|
||||
"market_provider": market_provider,
|
||||
}
|
||||
|
||||
@sk.post("/mode")
|
||||
def api_skills_mode_save(
|
||||
|
|
@ -110,19 +119,31 @@ def include_skill_routes(
|
|||
store.set_setting("AIA_SKILLS_PROMPT_IN_SYSTEM", "1" if bool(payload.get("prompt_in_system")) else "0")
|
||||
if "toolcall_enabled" in payload:
|
||||
store.set_setting("AIA_SKILL_TOOLCALL_ENABLED", "1" if bool(payload.get("toolcall_enabled")) else "0")
|
||||
if "market_provider" in payload:
|
||||
store.set_setting(_SKILL_MARKET_PROVIDER_KEY, normalize_skill_market_provider_setting(str(payload.get("market_provider") or "")))
|
||||
raw_prompt = str(store.get_setting("AIA_SKILLS_PROMPT_IN_SYSTEM") or "").strip().lower()
|
||||
raw_toolcall = str(store.get_setting("AIA_SKILL_TOOLCALL_ENABLED") or "").strip().lower()
|
||||
prompt_in_system = raw_prompt not in {"0", "false", "no", "off"}
|
||||
toolcall_enabled = raw_toolcall in {"1", "true", "yes", "on"}
|
||||
market_provider = normalize_skill_market_provider_setting(str(store.get_setting(_SKILL_MARKET_PROVIDER_KEY) or ""))
|
||||
_audit(
|
||||
store,
|
||||
ctx,
|
||||
action="skill_mode_update",
|
||||
target_id="skill_mode",
|
||||
status="ok",
|
||||
detail={"prompt_in_system": bool(prompt_in_system), "toolcall_enabled": bool(toolcall_enabled)},
|
||||
detail={
|
||||
"prompt_in_system": bool(prompt_in_system),
|
||||
"toolcall_enabled": bool(toolcall_enabled),
|
||||
"market_provider": market_provider,
|
||||
},
|
||||
)
|
||||
return {"ok": True, "prompt_in_system": bool(prompt_in_system), "toolcall_enabled": bool(toolcall_enabled)}
|
||||
return {
|
||||
"ok": True,
|
||||
"prompt_in_system": bool(prompt_in_system),
|
||||
"toolcall_enabled": bool(toolcall_enabled),
|
||||
"market_provider": market_provider,
|
||||
}
|
||||
|
||||
@sk.post("/install")
|
||||
def api_skills_install(
|
||||
|
|
@ -204,7 +225,7 @@ def include_skill_routes(
|
|||
query = str(q or "").strip()
|
||||
lim = int(limit) if isinstance(limit, int) and limit > 0 else 20
|
||||
lim = max(1, min(lim, 200))
|
||||
provider = str(store.get_setting("AIA_SKILL_MARKET_PROVIDER") or "clawhub").strip()
|
||||
provider = normalize_skill_market_provider_setting(str(store.get_setting(_SKILL_MARKET_PROVIDER_KEY) or ""))
|
||||
items = get_market_adapter(provider).search(query, limit=lim)
|
||||
return {"ok": True, "items": items}
|
||||
|
||||
|
|
@ -219,7 +240,7 @@ def include_skill_routes(
|
|||
s = str(slug or "").strip()
|
||||
if not s:
|
||||
raise HTTPException(status_code=400, detail="slug_required")
|
||||
provider = str(store.get_setting("AIA_SKILL_MARKET_PROVIDER") or "clawhub").strip()
|
||||
provider = normalize_skill_market_provider_setting(str(store.get_setting(_SKILL_MARKET_PROVIDER_KEY) or ""))
|
||||
detail = get_market_adapter(provider).detail(s)
|
||||
return {"ok": True, "detail": detail}
|
||||
|
||||
|
|
@ -238,7 +259,7 @@ def include_skill_routes(
|
|||
requested_version = str(payload.get("version") or "").strip()
|
||||
overwrite = bool(payload.get("overwrite"))
|
||||
|
||||
provider = str(store.get_setting("AIA_SKILL_MARKET_PROVIDER") or "clawhub").strip()
|
||||
provider = normalize_skill_market_provider_setting(str(store.get_setting(_SKILL_MARKET_PROVIDER_KEY) or ""))
|
||||
adapter = get_market_adapter(provider)
|
||||
archive_url, chosen_version = adapter.resolve_archive_url(slug=s, version=requested_version or None)
|
||||
|
||||
|
|
@ -673,6 +694,98 @@ def include_skill_routes(
|
|||
},
|
||||
}
|
||||
|
||||
@sk.post("/repair-deps")
|
||||
def api_skills_repair_deps(
|
||||
payload: dict[str, Any] | None = Body(default=None),
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, Any]:
|
||||
payload = payload or {}
|
||||
store = SqliteStore(db_path())
|
||||
ctx = resolve_auth(store, authorization)
|
||||
_require_admin(ctx)
|
||||
name = str(payload.get("name") or "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="name_required")
|
||||
_audit(
|
||||
store,
|
||||
ctx,
|
||||
action="skill_repair_deps_started",
|
||||
target_id=name,
|
||||
status="start",
|
||||
)
|
||||
out = repair_skill_dependencies(store=store, skill_name=name)
|
||||
_audit(
|
||||
store,
|
||||
ctx,
|
||||
action="skill_repair_deps_finished" if bool(out.get("ok")) else "skill_repair_deps_failed",
|
||||
target_id=name,
|
||||
status="ok" if bool(out.get("ok")) else "fail",
|
||||
detail=out,
|
||||
)
|
||||
return {"ok": bool(out.get("ok")), "result": out}
|
||||
|
||||
@sk.post("/repair-deps-all")
|
||||
def api_skills_repair_deps_all(
|
||||
payload: dict[str, Any] | None = Body(default=None),
|
||||
authorization: str | None = Header(default=None),
|
||||
) -> dict[str, Any]:
|
||||
_payload = payload or {}
|
||||
store = SqliteStore(db_path())
|
||||
ctx = resolve_auth(store, authorization)
|
||||
_require_admin(ctx)
|
||||
items = list_skills_with_status(store=store)
|
||||
results: list[dict[str, Any]] = []
|
||||
ok_count = 0
|
||||
warn_count = 0
|
||||
fail_count = 0
|
||||
for it in items:
|
||||
name = str((it or {}).get("name") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
_audit(
|
||||
store,
|
||||
ctx,
|
||||
action="skill_repair_deps_started",
|
||||
target_id=name,
|
||||
status="start",
|
||||
detail={"batch": True},
|
||||
)
|
||||
out = repair_skill_dependencies(store=store, skill_name=name)
|
||||
warnings = list(out.get("warnings") or []) if isinstance(out, dict) else []
|
||||
if bool(out.get("ok")):
|
||||
if warnings:
|
||||
warn_count += 1
|
||||
else:
|
||||
ok_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
_audit(
|
||||
store,
|
||||
ctx,
|
||||
action="skill_repair_deps_finished" if bool(out.get("ok")) else "skill_repair_deps_failed",
|
||||
target_id=name,
|
||||
status="ok" if bool(out.get("ok")) else "fail",
|
||||
detail={"batch": True, **(out if isinstance(out, dict) else {})},
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"name": name,
|
||||
"ok": bool(out.get("ok")) if isinstance(out, dict) else False,
|
||||
"warnings": warnings,
|
||||
"detail": str((out or {}).get("detail") or "") if isinstance(out, dict) else "",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"summary": {
|
||||
"total": len(results),
|
||||
"ok_count": ok_count,
|
||||
"warn_count": warn_count,
|
||||
"fail_count": fail_count,
|
||||
},
|
||||
"items": results,
|
||||
}
|
||||
|
||||
@sk.post("/test-run")
|
||||
def api_skills_test_run(
|
||||
payload: dict[str, Any] | None = Body(default=None),
|
||||
|
|
@ -778,7 +891,7 @@ def include_skill_routes(
|
|||
"execution_checked_total": len(execution_checks),
|
||||
"execution_checks": execution_checks,
|
||||
"classification_counts": classification_counts,
|
||||
"market_provider": str(store.get_setting("AIA_SKILL_MARKET_PROVIDER") or "clawhub"),
|
||||
"market_provider": normalize_skill_market_provider_setting(str(store.get_setting(_SKILL_MARKET_PROVIDER_KEY) or "")),
|
||||
}
|
||||
|
||||
router.include_router(sk)
|
||||
|
|
|
|||
|
|
@ -1087,6 +1087,16 @@ async function apiPost(path, body) {
|
|||
return data ?? {};
|
||||
}
|
||||
|
||||
/** Skills endpoints often return HTTP 200 with `{ ok: false, result: {...} }` on failure — treat as error for UX. */
|
||||
function assertSkillMutationOk(r, fallbackMessage) {
|
||||
if (!r || r.ok !== false) return;
|
||||
const res = r.result && typeof r.result === "object" ? r.result : {};
|
||||
const code = res.error_code != null ? String(res.error_code).trim() : "";
|
||||
const detail = res.detail != null ? String(res.detail).trim() : "";
|
||||
const msg = [code, detail].filter(Boolean).join(": ") || String(fallbackMessage || "skill operation failed");
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
async function apiRequest(method, path, body) {
|
||||
const url = resolveAdminApiUrl(path);
|
||||
const token = getStoredAuthToken();
|
||||
|
|
@ -7363,7 +7373,11 @@ async function renderSkills() {
|
|||
const skillModeStatus = el("div", { class: "muted", text: "" });
|
||||
const skillPromptModeCb = el("input", { type: "checkbox" });
|
||||
const skillToolcallModeCb = el("input", { type: "checkbox" });
|
||||
const marketQ = el("input", { class: "input", placeholder: "search ClawHub skills" });
|
||||
const skillMarketProviderSelect = el("select", { class: "input", style: "min-width:160px;" }, [
|
||||
el("option", { value: "clawhub", text: "clawhub (ClawHub)" }),
|
||||
el("option", { value: "cocoloop", text: "cocoloop (CocoLoop)" }),
|
||||
]);
|
||||
const marketQ = el("input", { class: "input", placeholder: "search skills (keyword)" });
|
||||
const marketLimitInp = el("input", { class: "input", placeholder: "limit", value: "40", style: "max-width:120px;" });
|
||||
const marketTbody = el("tbody");
|
||||
const marketDetailPre = el("pre", { class: "muted pre", text: "" });
|
||||
|
|
@ -7374,6 +7388,8 @@ async function renderSkills() {
|
|||
const r = await apiGet("/admin/api/skills/mode");
|
||||
skillPromptModeCb.checked = !!r.prompt_in_system;
|
||||
skillToolcallModeCb.checked = !!r.toolcall_enabled;
|
||||
const mp = String(r.market_provider || "clawhub").trim().toLowerCase();
|
||||
skillMarketProviderSelect.value = mp === "cocoloop" ? "cocoloop" : "clawhub";
|
||||
skillModeStatus.textContent = "";
|
||||
} catch (e) {
|
||||
skillModeStatus.textContent = `mode: ${String(e && e.message ? e.message : e)}`;
|
||||
|
|
@ -7385,10 +7401,13 @@ async function renderSkills() {
|
|||
const r = await apiPost("/admin/api/skills/mode", {
|
||||
prompt_in_system: !!skillPromptModeCb.checked,
|
||||
toolcall_enabled: !!skillToolcallModeCb.checked,
|
||||
market_provider: String(skillMarketProviderSelect.value || "clawhub").trim(),
|
||||
});
|
||||
skillPromptModeCb.checked = !!r.prompt_in_system;
|
||||
skillToolcallModeCb.checked = !!r.toolcall_enabled;
|
||||
skillModeStatus.textContent = `saved: prompt=${String(!!r.prompt_in_system)} toolcall=${String(!!r.toolcall_enabled)}`;
|
||||
const mp = String(r.market_provider || "clawhub").trim().toLowerCase();
|
||||
skillMarketProviderSelect.value = mp === "cocoloop" ? "cocoloop" : "clawhub";
|
||||
skillModeStatus.textContent = `saved: prompt=${String(!!r.prompt_in_system)} toolcall=${String(!!r.toolcall_enabled)} market=${String(skillMarketProviderSelect.value)}`;
|
||||
} catch (e) {
|
||||
skillModeStatus.textContent = `mode: ${String(e && e.message ? e.message : e)}`;
|
||||
}
|
||||
|
|
@ -7429,7 +7448,8 @@ async function renderSkills() {
|
|||
openSkillInstallModal(`Installing ${s}...`);
|
||||
try {
|
||||
const r = await apiPost("/admin/api/skills/market/install", { slug: s, version: version ? String(version) : undefined, overwrite: false });
|
||||
status.textContent = `install-clawhub success: ${JSON.stringify(r.result || {})}`;
|
||||
assertSkillMutationOk(r, "Market install failed");
|
||||
status.textContent = `install-market success: ${JSON.stringify(r.result || {})}`;
|
||||
marketStatus.textContent = `installed: ${s}`;
|
||||
await refreshSkillsState();
|
||||
finishSkillInstallModal(true, `${s} installed successfully.`);
|
||||
|
|
@ -7446,14 +7466,13 @@ async function renderSkills() {
|
|||
const slug = String(x.slug || "");
|
||||
const ver = String(x.version || "");
|
||||
const btnDetail = el("button", { class: "btn btn--small", text: "Detail", onclick: async () => await loadMarketDetail(slug) });
|
||||
const btnInstall = el("button", { class: "btn btn--small btn--primary", text: "Install", onclick: async () => await installFromMarket(slug, ver || undefined) });
|
||||
marketTbody.appendChild(
|
||||
el("tr", {}, [
|
||||
el("td", { text: slug }),
|
||||
el("td", { text: String(x.name || "") }),
|
||||
el("td", { text: ver }),
|
||||
el("td", { text: shortText(String(x.description || ""), 80) }),
|
||||
el("td", {}, [btnDetail, el("span", { style: "display:inline-block;width:6px" }), btnInstall]),
|
||||
el("td", {}, [btnDetail]),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
|
@ -7462,7 +7481,7 @@ async function renderSkills() {
|
|||
const btnMarketSearch = el("button", { class: "btn", text: "Search", onclick: async () => await loadMarket(marketQ.value) });
|
||||
const btnMarketLatest = el("button", { class: "btn", text: "Latest", onclick: async () => await loadMarket("") });
|
||||
const marketBox = el("details", { style: "margin:10px 0 14px 0;" }, [
|
||||
el("summary", { text: "ClawHub Market", style: "cursor:pointer;user-select:none;" }),
|
||||
el("summary", { text: "Skill market (ClawHub / CocoLoop)", style: "cursor:pointer;user-select:none;" }),
|
||||
el("div", { style: "height:8px" }),
|
||||
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-bottom:8px;" }, [marketQ, marketLimitInp, btnMarketSearch, btnMarketLatest]),
|
||||
marketStatus,
|
||||
|
|
@ -8400,6 +8419,24 @@ async function renderSkills() {
|
|||
openSkillTestRunModal(name);
|
||||
},
|
||||
});
|
||||
const repairDepsBtn = el("button", {
|
||||
class: "chat-sess-menu-item",
|
||||
text: "Repair deps",
|
||||
onclick: async () => {
|
||||
closeSkillActionMenu();
|
||||
try {
|
||||
status.textContent = `repair deps: ${name}...`;
|
||||
const r = await apiPost("/admin/api/skills/repair-deps", { name });
|
||||
assertSkillMutationOk(r, "Repair deps failed");
|
||||
status.textContent = `repair deps: ${JSON.stringify((r && r.result) || {}, null, 0)}`;
|
||||
await loadRows();
|
||||
await loadAudits();
|
||||
repaint();
|
||||
} catch (e) {
|
||||
status.textContent = `repair deps failed: ${String(e && e.message ? e.message : e)}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
const uninstallBtn = el("button", {
|
||||
class: "chat-sess-menu-item",
|
||||
text: "Uninstall",
|
||||
|
|
@ -8409,6 +8446,7 @@ async function renderSkills() {
|
|||
try {
|
||||
status.textContent = `uninstalling: ${name}...`;
|
||||
const r = await apiPost("/admin/api/skills/uninstall", { name });
|
||||
assertSkillMutationOk(r, "Uninstall failed");
|
||||
status.textContent = `uninstall success: ${JSON.stringify((r && r.result) || {}, null, 0)}`;
|
||||
await loadRows();
|
||||
await loadAudits();
|
||||
|
|
@ -8428,6 +8466,7 @@ async function renderSkills() {
|
|||
const menu = el("div", { class: "chat-sess-menu-pop", style: "position:fixed;z-index:250;" }, [
|
||||
toggleBtn,
|
||||
testRunBtn,
|
||||
repairDepsBtn,
|
||||
uninstallBtn,
|
||||
]);
|
||||
const rect = ev.currentTarget.getBoundingClientRect();
|
||||
|
|
@ -8483,6 +8522,7 @@ async function renderSkills() {
|
|||
return;
|
||||
}
|
||||
const r = await apiPost("/admin/api/skills/retry-install", { source: src, target });
|
||||
assertSkillMutationOk(r, "Retry install failed");
|
||||
status.textContent = `retry: ${JSON.stringify(r.result || {})}`;
|
||||
await loadRows();
|
||||
await loadAudits();
|
||||
|
|
@ -8544,6 +8584,7 @@ async function renderSkills() {
|
|||
description: String(descInp.value || "").trim(),
|
||||
body_markdown: String(bodyInp.value || ""),
|
||||
});
|
||||
assertSkillMutationOk(r, "Create skill failed");
|
||||
status.textContent = `create: ${JSON.stringify(r.result || {})}`;
|
||||
await loadRows();
|
||||
await loadAudits();
|
||||
|
|
@ -8566,6 +8607,7 @@ async function renderSkills() {
|
|||
const r = await apiPost("/admin/api/skills/install-registry", {
|
||||
archive_url: String(regInp.value || "").trim(),
|
||||
});
|
||||
assertSkillMutationOk(r, "Registry install failed");
|
||||
status.textContent = `install-registry success: ${JSON.stringify(r.result || {})}`;
|
||||
await refreshSkillsState();
|
||||
finishSkillInstallModal(true, "Registry skill installed successfully.");
|
||||
|
|
@ -8591,6 +8633,7 @@ async function renderSkills() {
|
|||
const r = await apiPost("/admin/api/skills/install", {
|
||||
source_dir: String(localDirInp.value || "").trim(),
|
||||
});
|
||||
assertSkillMutationOk(r, "Local install failed");
|
||||
status.textContent = `install-local success: ${JSON.stringify(r.result || {})}`;
|
||||
await refreshSkillsState();
|
||||
finishSkillInstallModal(true, "Local skill installed successfully.");
|
||||
|
|
@ -8615,6 +8658,26 @@ async function renderSkills() {
|
|||
}
|
||||
},
|
||||
});
|
||||
const btnRepairDepsAll = el("button", {
|
||||
class: "btn",
|
||||
text: "Repair all deps",
|
||||
onclick: async () => {
|
||||
const prev = btnRepairDepsAll.textContent;
|
||||
btnRepairDepsAll.disabled = true;
|
||||
btnRepairDepsAll.textContent = "Repairing...";
|
||||
try {
|
||||
const r = await apiPost("/admin/api/skills/repair-deps-all", {});
|
||||
const s = r && typeof r.summary === "object" ? r.summary : {};
|
||||
status.textContent = `repair all deps: total=${Number(s.total || 0)} ok=${Number(s.ok_count || 0)} warn=${Number(s.warn_count || 0)} fail=${Number(s.fail_count || 0)}`;
|
||||
await refreshSkillsState();
|
||||
} catch (e) {
|
||||
status.textContent = `repair all deps failed: ${String(e && e.message ? e.message : e)}`;
|
||||
} finally {
|
||||
btnRepairDepsAll.disabled = false;
|
||||
btnRepairDepsAll.textContent = prev;
|
||||
}
|
||||
},
|
||||
});
|
||||
retryableOnlyCb.addEventListener("change", () => {
|
||||
localStorage.setItem(SKILL_AUDIT_RETRYABLE_ONLY_KEY, retryableOnlyCb.checked ? "1" : "0");
|
||||
repaint();
|
||||
|
|
@ -8650,6 +8713,10 @@ async function renderSkills() {
|
|||
el("div", { class: "row", style: "gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:8px;" }, [
|
||||
el("label", { class: "row", style: "gap:6px;align-items:center;" }, [skillPromptModeCb, el("span", { text: "Prompt mode (inject SKILL.md)" })]),
|
||||
el("label", { class: "row", style: "gap:6px;align-items:center;" }, [skillToolcallModeCb, el("span", { text: "Toolcall mode (runtime as tools)" })]),
|
||||
el("label", { class: "row", style: "gap:6px;align-items:center;flex-wrap:wrap;" }, [
|
||||
el("span", { text: "Market (AIA_SKILL_MARKET_PROVIDER)" }),
|
||||
skillMarketProviderSelect,
|
||||
]),
|
||||
el("button", { class: "btn", text: "Save skill mode", onclick: saveSkillMode }),
|
||||
skillModeStatus,
|
||||
]),
|
||||
|
|
@ -8663,8 +8730,7 @@ async function renderSkills() {
|
|||
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-bottom:8px;" }, [nameInp, descInp]),
|
||||
el("div", { style: "margin-bottom:8px;" }, [bodyInp]),
|
||||
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-bottom:8px;" }, [btnCreate]),
|
||||
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-bottom:8px;" }, [regInp, btnInstallRegistry, btnRefresh]),
|
||||
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-bottom:8px;" }, [localDirInp, btnInstallLocal]),
|
||||
el("div", { class: "row", style: "gap:8px;flex-wrap:wrap;margin-bottom:8px;" }, [btnRefresh, btnRepairDepsAll]),
|
||||
el("div", { class: "table-wrap" }, [
|
||||
el("table", { class: "table table--compact" }, [
|
||||
el("thead", {}, [el("tr", {}, [
|
||||
|
|
|
|||
|
|
@ -475,6 +475,7 @@ body.theme-ds-body .card {
|
|||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
max-width: min(max(0px, calc(50% + 2cm - 58px)), 100%);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.chat-msg-col--user {
|
||||
|
|
@ -573,7 +574,8 @@ body.theme-ds-body .card {
|
|||
|
||||
.chat-msg--assistant {
|
||||
align-self: flex-start;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--ds-border, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue