diff --git a/docs/pr-media/guard-skill-detail-errors/skill-detail-error-after.png b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-after.png
new file mode 100644
index 00000000..9bb58ac5
Binary files /dev/null and b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-after.png differ
diff --git a/docs/pr-media/guard-skill-detail-errors/skill-detail-error-before.png b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-before.png
new file mode 100644
index 00000000..c8897e04
Binary files /dev/null and b/docs/pr-media/guard-skill-detail-errors/skill-detail-error-before.png differ
diff --git a/static/panels.js b/static/panels.js
index ca7e6733..d6771e21 100644
--- a/static/panels.js
+++ b/static/panels.js
@@ -3244,6 +3244,21 @@ function _renderSkillDetail(name, content, linkedFiles) {
_setSkillHeaderButtons('read');
}
+function _renderSkillError(name, message) {
+ const title = $('skillDetailTitle');
+ const body = $('skillDetailBody');
+ const empty = $('skillDetailEmpty');
+ if (title) title.textContent = name;
+ if (body) {
+ body.innerHTML = `
${esc(message || t('skill_load_failed'))}
`;
+ body.style.display = '';
+ }
+ if (empty) empty.style.display = 'none';
+ _currentSkillDetail = null;
+ _skillMode = 'empty';
+ _setSkillHeaderButtons('empty');
+}
+
function _setSkillHeaderButtons(mode) {
const editBtn = $('btnEditSkillDetail');
const delBtn = $('btnDeleteSkillDetail');
@@ -3264,6 +3279,12 @@ async function openSkill(name, el) {
_editingSkillName = null;
try {
const data = await api(`/api/skills/content?name=${encodeURIComponent(name)}`);
+ if (data && (data.success === false || data.error)) {
+ const message = data.error || t('skill_load_failed');
+ _renderSkillError(name, message);
+ setStatus(t('skill_load_failed') + message);
+ return;
+ }
_currentSkillDetail = { name, content: data.content || '', linked_files: data.linked_files || {} };
_renderSkillDetail(name, data.content || '', data.linked_files || {});
} catch(e) { setStatus(t('skill_load_failed') + e.message); }
@@ -3272,6 +3293,11 @@ async function openSkill(name, el) {
async function openSkillFile(skillName, filePath) {
try {
const data = await api(`/api/skills/content?name=${encodeURIComponent(skillName)}&file=${encodeURIComponent(filePath)}`);
+ if (data && data.error) {
+ _renderSkillError(skillName, data.error);
+ setStatus(t('skill_file_load_failed') + data.error);
+ return;
+ }
const body = $('skillDetailBody');
if (!body) return;
const ext = (filePath.split('.').pop() || '').toLowerCase();
diff --git a/tests/test_skill_detail_error_guard.py b/tests/test_skill_detail_error_guard.py
new file mode 100644
index 00000000..f4cb02aa
--- /dev/null
+++ b/tests/test_skill_detail_error_guard.py
@@ -0,0 +1,28 @@
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parent.parent
+PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
+
+
+def test_skill_detail_api_error_renders_in_detail_pane():
+ assert "function _renderSkillError(name, message)" in PANELS_JS
+ assert "data.success === false || data.error" in PANELS_JS
+ assert "_renderSkillError(name, message);" in PANELS_JS
+ assert "_currentSkillDetail = null;" in PANELS_JS
+ assert "_setSkillHeaderButtons('empty');" in PANELS_JS
+
+
+def test_skill_detail_no_longer_silently_falls_back_on_error_payload():
+ success_guard = PANELS_JS.index("data.success === false || data.error")
+ render_detail = PANELS_JS.index("_renderSkillDetail(name, data.content || ''")
+ assert success_guard < render_detail
+
+
+def test_skill_linked_file_api_error_renders_in_detail_pane():
+ file_fetch = PANELS_JS.index("/api/skills/content?name=${encodeURIComponent(skillName)}&file=")
+ file_error_guard = PANELS_JS.index("if (data && data.error)", file_fetch)
+ file_render = PANELS_JS.index("_renderSkillError(skillName, data.error);", file_fetch)
+ body_lookup = PANELS_JS.index("const body = $('skillDetailBody');", file_fetch)
+ assert file_error_guard < body_lookup
+ assert file_render < body_lookup