feat: skills modal redesign + runtime config editing for installed skills

Redesigns the create/edit/view skill modal into a two-column spec manifest
layout (Identity/Manifest/Deployment | Skill Content) matching the Agent
Skills spec structure. Installed (readonly) skills can now have their runtime
config (model, temperature, token limits, enabled) edited independently of
the locked spec/content fields.

- Two-column spec layout with section headings (Identity, Manifest, Deployment,
  Skill Content); content textarea uses monospace font and fills the column
- h3 section headings for screen-reader nav; h3 UA stylesheet reset in CSS
- Runtime Config collapsible uses 3-column grid; license field is now a select
  of SPDX identifiers (MIT, Apache-2.0, GPL-3.0, AGPL-3.0, etc.)
- Origin badge (cyan) shows source URL for installed skills in view mode
- server.py: _SKILL_RUNTIME_CONFIG_FIELDS frozenset; readonly skills filter
  updates to config-only fields (spec fields silently dropped); audit action
  distinguishes skill.update.config from skill.update; license field capped
  at 128 chars in both create and update paths
- governance.js: spec fields disabled for readonly; config fields always
  editable; Save button shown for all skills (labeled "Save Config" when
  readonly); collapsible state reset between modal opens prevents state leak;
  esk-allowed-tools disabled state driven by auto_approve not readonly
- Tests: spec-only body on readonly skill → 400; config-only → 200 with
  spec fields unchanged; mixed body → config fields applied, spec dropped
This commit is contained in:
Patrick Buckley
2026-03-17 15:36:17 -07:00
committed by Patrick Buckley
parent dc464ac313
commit 3f7f8495d6
5 changed files with 427 additions and 116 deletions
+60 -4
View File
@@ -802,8 +802,8 @@ class TestSkillAPI:
)
assert resp.status_code == 404
def test_update_skill_readonly_rejected(self, api_client, api_storage):
"""Updating a readonly (MCP-sourced) skill returns 403."""
def test_update_skill_readonly_spec_fields_rejected(self, api_client, api_storage):
"""Updating spec fields on a readonly skill returns 400 (filtered to nothing)."""
_create_template(
api_storage,
"s1",
@@ -815,9 +815,65 @@ class TestSkillAPI:
)
resp = api_client.put(
"/v1/api/admin/skills/s1",
json={"description": "hacked"},
json={"description": "hacked", "content": "evil"},
)
assert resp.status_code == 403
assert resp.status_code == 400
assert "runtime config" in resp.json()["error"].lower()
def test_update_skill_readonly_runtime_config_allowed(self, api_client, api_storage):
"""Runtime config fields can be updated on a readonly (installed) skill."""
_create_template(
api_storage,
"s1",
"installed-skill",
"external content",
origin="source",
readonly=True,
)
resp = api_client.put(
"/v1/api/admin/skills/s1",
json={"model": "gpt-5", "temperature": 0.5, "enabled": False},
)
assert resp.status_code == 200
data = resp.json()
assert data["model"] == "gpt-5"
assert data["temperature"] == 0.5
assert data["enabled"] is False
# Spec fields must remain unchanged
assert data["content"] == "external content"
def test_update_skill_readonly_mixed_body_filters_spec(self, api_client, api_storage):
"""When JS sends all fields for a readonly skill, spec fields are silently dropped."""
_create_template(
api_storage,
"s1",
"installed",
"original content",
origin="source",
readonly=True,
)
resp = api_client.put(
"/v1/api/admin/skills/s1",
# Simulate what the browser form submits: every field present
json={
"name": "hacked",
"content": "evil content",
"description": "tampered",
"model": "gpt-5",
"enabled": False,
"token_budget": 50000,
},
)
assert resp.status_code == 200
data = resp.json()
# Config fields updated
assert data["model"] == "gpt-5"
assert data["enabled"] is False
assert data["token_budget"] == 50000
# Spec fields unchanged
assert data["name"] == "installed"
assert data["content"] == "original content"
assert data["description"] == ""
def test_update_skill_recomputes_token_estimate(self, api_client, api_storage):
"""Updating content recomputes token_estimate."""
+29 -5
View File
@@ -2134,6 +2134,24 @@ async def admin_delete_policy(request: Request) -> JSONResponse:
_VALID_ACTIVATIONS = {"named", "default", "search"}
# Fields that may be updated on installed (readonly) skills.
# These are local runtime configuration — not part of the SKILL.md spec —
# so they don't compromise the fidelity of an externally-sourced skill.
_SKILL_RUNTIME_CONFIG_FIELDS = frozenset(
{
"model",
"temperature",
"reasoning_effort",
"max_tokens",
"token_budget",
"agent_max_turns",
"auto_approve",
"allowed_tools",
"enabled",
"notify_on_complete",
}
)
def _parse_skill_session_config(body: dict[str, Any]) -> tuple[dict[str, Any], JSONResponse | None]:
"""Parse and validate session config fields from a skill request body.
@@ -2372,7 +2390,7 @@ async def admin_create_skill(request: Request) -> JSONResponse:
org_id = str(body.get("org_id", "")).strip()[:64]
author = str(body.get("author", "")).strip()[:256]
version = str(body.get("version", "1.0.0")).strip()[:64]
license_val = str(body.get("license", "")).strip()
license_val = str(body.get("license", "")).strip()[:128]
compatibility = str(body.get("compatibility", "")).strip()[:500]
raw_tags = body.get("tags", [])
@@ -2462,8 +2480,7 @@ async def admin_update_skill(request: Request) -> JSONResponse:
existing = storage.get_prompt_template(skill_id)
if existing is None:
return JSONResponse({"error": "Skill not found"}, status_code=404)
if existing.get("readonly"):
return JSONResponse({"error": "MCP-sourced skills are read-only"}, status_code=403)
is_readonly = bool(existing.get("readonly"))
body = await read_json_or_400(request)
if isinstance(body, JSONResponse):
@@ -2504,7 +2521,7 @@ async def admin_update_skill(request: Request) -> JSONResponse:
if "version" in body:
updates["version"] = str(body["version"]).strip()[:64]
if "license" in body:
updates["license"] = str(body["license"]).strip()
updates["license"] = str(body["license"]).strip()[:128]
if "compatibility" in body:
updates["compatibility"] = str(body["compatibility"]).strip()[:500]
if "tags" in body:
@@ -2519,6 +2536,13 @@ async def admin_update_skill(request: Request) -> JSONResponse:
tag_str = "[]"
updates["tags"] = tag_str
# Installed (readonly) skills: restrict updates to runtime config only.
# Spec/content fields are locked to preserve external-source fidelity.
if is_readonly:
updates = {k: v for k, v in updates.items() if k in _SKILL_RUNTIME_CONFIG_FIELDS}
if not updates:
return JSONResponse({"error": "No runtime config fields to update"}, status_code=400)
# Snapshot current state for version history before applying update
existing_versions = storage.list_skill_versions(skill_id)
version_int = len(existing_versions) + 1
@@ -2536,7 +2560,7 @@ async def admin_update_skill(request: Request) -> JSONResponse:
record_audit(
storage,
audit_uid,
"skill.update",
"skill.update.config" if is_readonly else "skill.update",
"skill",
skill_id,
updates,
+36 -4
View File
@@ -1187,13 +1187,37 @@ function showEditTemplateModal(tmplId) {
});
};
}
// Reset collapsible state before applying readonly rules (prevents state leak
// when switching between readonly and editable skills in the same session)
var allDetails = document.querySelectorAll(
"#edit-template-box .admin-details",
);
for (var d = 0; d < allDetails.length; d++) allDetails[d].open = false;
// --- Readonly mode for imported skills ---
var isReadonly = tmpl.readonly || false;
var editTitle = document.getElementById("edit-template-title");
if (editTitle)
editTitle.textContent = isReadonly ? "View Skill" : "Edit Skill";
// Origin badge — show provenance for installed skills
var originBadge = document.getElementById("etm-origin-badge");
if (originBadge) {
if (isReadonly && tmpl.source_url) {
originBadge.textContent = "Installed from \u00a0" + tmpl.source_url;
originBadge.style.display = "inline-flex";
} else if (isReadonly && tmpl.origin && tmpl.origin !== "manual") {
originBadge.textContent = "Installed skill";
originBadge.style.display = "inline-flex";
} else {
originBadge.style.display = "none";
}
}
var submitBtn = document.getElementById("etm-submit");
if (submitBtn) submitBtn.style.display = isReadonly ? "none" : "";
if (submitBtn) {
submitBtn.style.display = "";
submitBtn.textContent = isReadonly ? "Save Config" : "Save";
}
// Spec/content fields: locked for installed skills (preserve source fidelity)
[
"etm-name",
"etm-category",
@@ -1206,6 +1230,12 @@ function showEditTemplateModal(tmplId) {
"etm-activation",
"etm-content",
"etm-default",
].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.disabled = isReadonly;
});
// Runtime config fields: always editable (local settings, not part of SKILL.md spec)
[
"esk-model",
"esk-temperature",
"esk-reasoning-effort",
@@ -1213,15 +1243,17 @@ function showEditTemplateModal(tmplId) {
"esk-token-budget",
"esk-agent-max-turns",
"esk-auto-approve",
"esk-allowed-tools",
"esk-enabled",
].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.disabled = isReadonly;
if (el) el.disabled = false;
});
// esk-allowed-tools follows auto_approve state, not readonly state
var allowedToolsEl = document.getElementById("esk-allowed-tools");
if (allowedToolsEl) allowedToolsEl.disabled = tmpl.auto_approve || false;
var cancelBtn = document.querySelector("#edit-template-box .modal-cancel");
if (cancelBtn) cancelBtn.textContent = isReadonly ? "Close" : "Cancel";
// Auto-expand collapsibles in view mode
// Auto-expand Runtime Config collapsible for installed skills so config is visible
if (isReadonly) {
var details = document.querySelectorAll(
"#edit-template-box .admin-details",
+170 -103
View File
@@ -848,67 +848,100 @@ window.TURNSTONE_KB_SHORTCUTS = [
<!-- Create Skill Modal -->
<div id="create-template-overlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="create-template-title">
<div id="create-template-box" class="admin-modal admin-modal-wide">
<div id="create-template-box" class="admin-modal admin-modal-wide admin-modal-skill">
<h2 id="create-template-title">Create Skill</h2>
<div id="create-template-error" role="alert" aria-live="assertive"></div>
<label for="ctm-name">Name</label>
<input id="ctm-name" type="text" placeholder="e.g. Code Review Agent" autocomplete="off">
<label for="ctm-category">Category</label>
<select id="ctm-category">
<option value="general">General</option>
<option value="engineering">Engineering</option>
<option value="support">Support</option>
<option value="custom">Custom</option>
</select>
<label for="skill-description">Description</label>
<textarea id="skill-description" rows="2" placeholder="Brief description for discovery"></textarea>
<label for="skill-tags">Tags</label>
<input id="skill-tags" type="text" placeholder="Comma-separated tags">
<label for="skill-author">Author</label>
<input id="skill-author" type="text" placeholder="Author name">
<label for="skill-version">Version</label>
<input id="skill-version" type="text" placeholder="1.0.0">
<label for="skill-license">License</label>
<input id="skill-license" type="text" placeholder="MIT, Apache-2.0, etc.">
<label for="skill-compatibility">Compatibility <span class="label-hint">environment requirements, max 500 chars</span></label>
<input id="skill-compatibility" type="text" placeholder="Requires git, docker, etc." maxlength="500">
<label for="skill-activation">Activation</label>
<select id="skill-activation">
<option value="named">Named</option>
<option value="default">Default (auto-apply)</option>
<option value="search">Search (BM25 discoverable)</option>
</select>
<label for="ctm-content">Content <span class="label-hint">system message text, use {{model}}, {{ws_id}}, {{node_id}} for placeholders</span></label>
<textarea id="ctm-content" rows="6" placeholder="You are a code reviewer using {{model}}..."></textarea>
<label>Variables <span class="label-hint">auto-detected from content &mdash; available: model, ws_id, node_id</span></label>
<div id="ctm-variables" class="label-hint" style="padding:4px 0;min-height:1.2em"></div>
<label class="admin-checkbox"><input id="ctm-default" type="checkbox"> Set as default for new workstreams</label>
<div class="skill-spec-body">
<div class="skill-spec-col skill-spec-col-meta">
<div class="skill-spec-section">
<h3 class="skill-spec-heading">Identity</h3>
<label for="ctm-name">Name</label>
<input id="ctm-name" type="text" placeholder="e.g. code-review" autocomplete="off">
<label for="ctm-category">Category</label>
<select id="ctm-category">
<option value="general">General</option>
<option value="engineering">Engineering</option>
<option value="support">Support</option>
<option value="custom">Custom</option>
</select>
<label for="skill-description">Description</label>
<textarea id="skill-description" rows="2" placeholder="Brief description for skill discovery"></textarea>
</div>
<div class="skill-spec-section">
<h3 class="skill-spec-heading">Manifest</h3>
<label for="skill-tags">Tags</label>
<input id="skill-tags" type="text" placeholder="python, review, quality">
<label for="skill-author">Author</label>
<input id="skill-author" type="text" placeholder="Author name">
<label for="skill-version">Version</label>
<input id="skill-version" type="text" placeholder="1.0.0">
<label for="skill-license">License</label>
<select id="skill-license">
<option value="">— not specified —</option>
<option value="MIT">MIT</option>
<option value="Apache-2.0">Apache-2.0</option>
<option value="GPL-2.0">GPL-2.0</option>
<option value="GPL-3.0">GPL-3.0</option>
<option value="LGPL-2.1">LGPL-2.1</option>
<option value="LGPL-3.0">LGPL-3.0</option>
<option value="AGPL-3.0">AGPL-3.0</option>
<option value="BSD-2-Clause">BSD-2-Clause</option>
<option value="BSD-3-Clause">BSD-3-Clause</option>
<option value="ISC">ISC</option>
<option value="MPL-2.0">MPL-2.0</option>
<option value="Unlicense">Unlicense</option>
<option value="Proprietary">Proprietary</option>
</select>
<label for="skill-compatibility">Compatibility <span class="label-hint">environment requirements, max 500 chars</span></label>
<input id="skill-compatibility" type="text" placeholder="Requires git, docker, etc." maxlength="500">
</div>
<div class="skill-spec-section">
<h3 class="skill-spec-heading">Deployment</h3>
<label for="skill-activation">Activation <span class="label-hint">how models discover this skill</span></label>
<select id="skill-activation">
<option value="named">Named — explicit /skill invocation</option>
<option value="default">Default — auto-applied to every session</option>
<option value="search">Search — BM25 discoverable</option>
</select>
<label class="admin-checkbox"><input id="ctm-default" type="checkbox"> Apply to new workstreams by default</label>
</div>
</div>
<div class="skill-spec-col skill-spec-col-content">
<div class="skill-spec-section skill-spec-section-content">
<h3 class="skill-spec-heading">Skill Content <span class="label-hint">system message &mdash; {{model}}, {{ws_id}}, {{node_id}}</span></h3>
<textarea id="ctm-content" class="skill-content-area" placeholder="You are a code reviewer using {{model}}..."></textarea>
<div class="skill-vars-row">
<span class="skill-vars-label">Variables</span>
<div id="ctm-variables" class="skill-vars-display label-hint"></div>
</div>
</div>
</div>
</div>
<details class="admin-details">
<summary>Session Config <span class="label-hint">optional &mdash; applied when skill is selected for a workstream</span></summary>
<label for="csk-model">Model</label>
<input id="csk-model" type="text" placeholder="Default model">
<label for="csk-temperature">Temperature</label>
<input id="csk-temperature" type="number" step="0.1" min="0" max="2" placeholder="System default">
<label for="csk-reasoning-effort">Reasoning Effort</label>
<select id="csk-reasoning-effort">
<option value="">System default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<label for="csk-max-tokens">Max Tokens</label>
<input id="csk-max-tokens" type="number" min="1" placeholder="System default">
<label for="csk-token-budget">Token Budget</label>
<input id="csk-token-budget" type="number" min="0" placeholder="0 = unlimited">
<label for="csk-agent-max-turns">Agent Max Turns</label>
<input id="csk-agent-max-turns" type="number" min="1" placeholder="System default">
<summary>Runtime Config <span class="label-hint">model, temperature, token limits</span></summary>
<div class="skill-config-grid">
<div><label for="csk-model">Model</label><input id="csk-model" type="text" placeholder="Default model"></div>
<div><label for="csk-temperature">Temperature</label><input id="csk-temperature" type="number" step="0.1" min="0" max="2" placeholder="System default"></div>
<div>
<label for="csk-reasoning-effort">Reasoning Effort</label>
<select id="csk-reasoning-effort">
<option value="">System default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div><label for="csk-max-tokens">Max Tokens</label><input id="csk-max-tokens" type="number" min="1" placeholder="System default"></div>
<div><label for="csk-token-budget">Token Budget</label><input id="csk-token-budget" type="number" min="0" placeholder="0 = unlimited"></div>
<div><label for="csk-agent-max-turns">Agent Max Turns</label><input id="csk-agent-max-turns" type="number" min="1" placeholder="System default"></div>
</div>
<label class="admin-checkbox"><input id="csk-auto-approve" type="checkbox"> Auto-approve all tools</label>
<label for="csk-allowed-tools">Allowed Tools <span class="label-hint">comma-separated tool names for auto-approve</span></label>
<input id="csk-allowed-tools" type="text" placeholder="bash, read_file, write_file">
<label class="admin-checkbox"><input id="csk-enabled" type="checkbox" checked> Enabled</label>
</details>
<details class="admin-details">
<summary>Resources <span class="label-hint">optional bundled files (scripts, references, assets)</span></summary>
<summary>Resources <span class="label-hint">bundled files (scripts, references, assets)</span></summary>
<div id="ctm-resources-list" role="list" aria-live="polite" aria-label="Pending resources"></div>
<div style="margin-top:8px;display:flex;flex-direction:column;gap:6px">
<label for="ctm-res-path">Path</label>
@@ -929,61 +962,95 @@ window.TURNSTONE_KB_SHORTCUTS = [
<!-- Edit Skill Modal -->
<div id="edit-template-overlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="edit-template-title">
<div id="edit-template-box" class="admin-modal admin-modal-wide">
<div id="edit-template-box" class="admin-modal admin-modal-wide admin-modal-skill">
<h2 id="edit-template-title">Edit Skill</h2>
<div id="etm-origin-badge" class="skill-origin-badge" style="display:none"></div>
<div id="edit-template-error" role="alert" aria-live="assertive"></div>
<input id="etm-id" type="hidden">
<label for="etm-name">Name</label>
<input id="etm-name" type="text" autocomplete="off">
<label for="etm-category">Category</label>
<select id="etm-category">
<option value="general">General</option>
<option value="engineering">Engineering</option>
<option value="support">Support</option>
<option value="custom">Custom</option>
</select>
<label for="etm-description">Description</label>
<textarea id="etm-description" rows="2" placeholder="Brief description for discovery"></textarea>
<label for="etm-tags">Tags</label>
<input id="etm-tags" type="text" placeholder="Comma-separated tags">
<label for="etm-author">Author</label>
<input id="etm-author" type="text" placeholder="Author name">
<label for="etm-version">Version</label>
<input id="etm-version" type="text" placeholder="1.0.0">
<label for="etm-license">License</label>
<input id="etm-license" type="text" placeholder="MIT, Apache-2.0, etc.">
<label for="etm-compatibility">Compatibility <span class="label-hint">environment requirements, max 500 chars</span></label>
<input id="etm-compatibility" type="text" placeholder="Requires git, docker, etc." maxlength="500">
<label for="etm-activation">Activation</label>
<select id="etm-activation">
<option value="named">Named</option>
<option value="default">Default (auto-apply)</option>
<option value="search">Search (BM25 discoverable)</option>
</select>
<label for="etm-content">Content</label>
<textarea id="etm-content" rows="6"></textarea>
<label>Variables <span class="label-hint">auto-detected from content &mdash; available: model, ws_id, node_id</span></label>
<div id="etm-variables" class="label-hint" style="padding:4px 0;min-height:1.2em"></div>
<label class="admin-checkbox"><input id="etm-default" type="checkbox"> Set as default</label>
<div class="skill-spec-body">
<div class="skill-spec-col skill-spec-col-meta">
<div class="skill-spec-section">
<h3 class="skill-spec-heading">Identity</h3>
<label for="etm-name">Name</label>
<input id="etm-name" type="text" autocomplete="off">
<label for="etm-category">Category</label>
<select id="etm-category">
<option value="general">General</option>
<option value="engineering">Engineering</option>
<option value="support">Support</option>
<option value="custom">Custom</option>
</select>
<label for="etm-description">Description</label>
<textarea id="etm-description" rows="2" placeholder="Brief description for skill discovery"></textarea>
</div>
<div class="skill-spec-section">
<h3 class="skill-spec-heading">Manifest</h3>
<label for="etm-tags">Tags</label>
<input id="etm-tags" type="text" placeholder="python, review, quality">
<label for="etm-author">Author</label>
<input id="etm-author" type="text" placeholder="Author name">
<label for="etm-version">Version</label>
<input id="etm-version" type="text" placeholder="1.0.0">
<label for="etm-license">License</label>
<select id="etm-license">
<option value="">— not specified —</option>
<option value="MIT">MIT</option>
<option value="Apache-2.0">Apache-2.0</option>
<option value="GPL-2.0">GPL-2.0</option>
<option value="GPL-3.0">GPL-3.0</option>
<option value="LGPL-2.1">LGPL-2.1</option>
<option value="LGPL-3.0">LGPL-3.0</option>
<option value="AGPL-3.0">AGPL-3.0</option>
<option value="BSD-2-Clause">BSD-2-Clause</option>
<option value="BSD-3-Clause">BSD-3-Clause</option>
<option value="ISC">ISC</option>
<option value="MPL-2.0">MPL-2.0</option>
<option value="Unlicense">Unlicense</option>
<option value="Proprietary">Proprietary</option>
</select>
<label for="etm-compatibility">Compatibility <span class="label-hint">environment requirements, max 500 chars</span></label>
<input id="etm-compatibility" type="text" placeholder="Requires git, docker, etc." maxlength="500">
</div>
<div class="skill-spec-section">
<h3 class="skill-spec-heading">Deployment</h3>
<label for="etm-activation">Activation <span class="label-hint">how models discover this skill</span></label>
<select id="etm-activation">
<option value="named">Named — explicit /skill invocation</option>
<option value="default">Default — auto-applied to every session</option>
<option value="search">Search — BM25 discoverable</option>
</select>
<label class="admin-checkbox"><input id="etm-default" type="checkbox"> Apply to new workstreams by default</label>
</div>
</div>
<div class="skill-spec-col skill-spec-col-content">
<div class="skill-spec-section skill-spec-section-content">
<h3 class="skill-spec-heading">Skill Content <span class="label-hint">{{model}}, {{ws_id}}, {{node_id}}</span></h3>
<textarea id="etm-content" class="skill-content-area"></textarea>
<div class="skill-vars-row">
<span class="skill-vars-label">Variables</span>
<div id="etm-variables" class="skill-vars-display label-hint"></div>
</div>
</div>
</div>
</div>
<details class="admin-details">
<summary>Session Config <span class="label-hint">applied when skill is selected for a workstream</span></summary>
<label for="esk-model">Model</label>
<input id="esk-model" type="text" placeholder="Default model">
<label for="esk-temperature">Temperature</label>
<input id="esk-temperature" type="number" step="0.1" min="0" max="2" placeholder="System default">
<label for="esk-reasoning-effort">Reasoning Effort</label>
<select id="esk-reasoning-effort">
<option value="">System default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<label for="esk-max-tokens">Max Tokens</label>
<input id="esk-max-tokens" type="number" min="1" placeholder="System default">
<label for="esk-token-budget">Token Budget</label>
<input id="esk-token-budget" type="number" min="0" placeholder="0 = unlimited">
<label for="esk-agent-max-turns">Agent Max Turns</label>
<input id="esk-agent-max-turns" type="number" min="1" placeholder="System default">
<summary>Runtime Config <span class="label-hint">model, temperature, token limits</span></summary>
<div class="skill-config-grid">
<div><label for="esk-model">Model</label><input id="esk-model" type="text" placeholder="Default model"></div>
<div><label for="esk-temperature">Temperature</label><input id="esk-temperature" type="number" step="0.1" min="0" max="2" placeholder="System default"></div>
<div>
<label for="esk-reasoning-effort">Reasoning Effort</label>
<select id="esk-reasoning-effort">
<option value="">System default</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div><label for="esk-max-tokens">Max Tokens</label><input id="esk-max-tokens" type="number" min="1" placeholder="System default"></div>
<div><label for="esk-token-budget">Token Budget</label><input id="esk-token-budget" type="number" min="0" placeholder="0 = unlimited"></div>
<div><label for="esk-agent-max-turns">Agent Max Turns</label><input id="esk-agent-max-turns" type="number" min="1" placeholder="System default"></div>
</div>
<label class="admin-checkbox"><input id="esk-auto-approve" type="checkbox"> Auto-approve all tools</label>
<label for="esk-allowed-tools">Allowed Tools <span class="label-hint">comma-separated tool names for auto-approve</span></label>
<input id="esk-allowed-tools" type="text" placeholder="bash, read_file, write_file">
+132
View File
@@ -1207,6 +1207,138 @@
.admin-details summary .label-hint { font-weight: 400; }
.admin-details label:first-of-type { margin-top: 4px; }
/* ==========================================================================
Skill Spec Modal — two-column manifest layout
Left: Identity / Manifest / Deployment | Right: Skill Content
========================================================================== */
.admin-modal-skill { padding: 28px 28px 24px; }
.skill-spec-body {
display: grid;
grid-template-columns: 1fr 1.55fr;
gap: 0;
margin-bottom: 12px;
}
.skill-spec-col-meta {
border-right: 1px solid var(--border);
padding-right: 22px;
}
.skill-spec-col-content {
padding-left: 22px;
display: flex;
flex-direction: column;
}
.skill-spec-section { margin-bottom: 14px; }
.skill-spec-section:last-child { margin-bottom: 0; }
/* h3 used for screen-reader heading structure; reset UA defaults */
h3.skill-spec-heading { font-size: inherit; margin-block: 0; }
.skill-spec-heading {
font-family: var(--font-display);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent);
padding-bottom: 5px;
margin: 14px 0 6px;
border-bottom: 1px solid var(--accent-dim);
}
.skill-spec-section:first-child .skill-spec-heading { margin-top: 0; }
.skill-spec-heading .label-hint {
text-transform: none;
letter-spacing: 0;
font-weight: 400;
font-size: 10px;
opacity: 1;
}
.skill-spec-section-content {
flex: 1;
display: flex;
flex-direction: column;
}
.skill-content-area {
flex: 1;
min-height: 220px;
font-family: var(--font-mono) !important;
font-size: 11.5px !important;
line-height: 1.65 !important;
}
.skill-vars-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
min-height: 18px;
}
.skill-vars-label {
font-family: var(--font-display);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--fg-dim);
white-space: nowrap;
flex-shrink: 0;
}
.skill-vars-display { font-size: 11px; }
.skill-config-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px 14px;
margin: 4px 0 2px;
}
.skill-config-grid > div { min-width: 0; }
/* Origin badge — shown for remotely installed (readonly) skills */
.skill-origin-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-display);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--cyan);
background: rgba(103, 232, 249, 0.07);
border: 1px solid rgba(103, 232, 249, 0.18);
border-radius: var(--radius-sm);
padding: 5px 10px;
margin-bottom: 14px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.skill-origin-badge::before {
content: "\2193";
font-size: 11px;
flex-shrink: 0;
}
@media (max-width: 700px) {
.skill-spec-body { grid-template-columns: 1fr; }
.skill-spec-col-meta {
border-right: none;
padding-right: 0;
border-bottom: 1px solid var(--border);
padding-bottom: 16px;
margin-bottom: 16px;
}
.skill-spec-col-content { padding-left: 0; }
.skill-config-grid { grid-template-columns: 1fr 1fr; }
}
.modal-buttons { display: flex; gap: 10px; margin-top: 20px; }
.modal-cancel {
flex: 1;