mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-27 14:24:47 -06:00
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:
committed by
Patrick Buckley
parent
dc464ac313
commit
3f7f8495d6
+60
-4
@@ -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."""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 — 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 — {{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 — 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 — 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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user