From 94bf20fba73f7d8772f0d08c866884b9d1030d1b Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Tue, 13 Oct 2026 14:32:10 -0400 Subject: [PATCH 1/2] Show a skill's SKILL.md as soon as the skill opens On the Prompts and skills page, opening a skill now shows its SKILL.md straight away; it still folds by its heading, and the supporting files stay folded. Co-Authored-By: Claude Opus 5.5 --- site/prompts/index.html | 20 ++++++++++---------- site/tests/prompts.test.cjs | 35 ++++++++++++++++++++++++++++++----- src/prompts_page.rs | 20 +++++++++----------- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/site/prompts/index.html b/site/prompts/index.html index 8ede0cda3..8334bccb4 100644 --- a/site/prompts/index.html +++ b/site/prompts/index.html @@ -431,7 +431,7 @@

Prompts and skills

thirdshift-code-review

Used by 3 · Y · Review

-
+

thirdshift-code-review/SKILL.md

---
 name: thirdshift-code-review
@@ -553,7 +553,7 @@ 

Prompts and skills

thirdshift-codebase-design

Used by the Architecture review, in an Architect run

-
+

thirdshift-codebase-design/SKILL.md

---
 name: thirdshift-codebase-design
@@ -775,7 +775,7 @@ 

Prompts and skills

thirdshift-implement

Used by 2 · M · Implement

-
+

thirdshift-implement/SKILL.md

---
 name: thirdshift-implement
@@ -811,7 +811,7 @@ 

Prompts and skills

thirdshift-improve-codebase-architecture

Used by the Architecture review, in an Architect run

-
+

thirdshift-improve-codebase-architecture/SKILL.md

---
 name: thirdshift-improve-codebase-architecture
@@ -1078,7 +1078,7 @@ 

Prompts and skills

thirdshift-pr

Used by 3 · Y · Review

-
+

thirdshift-pr/SKILL.md

---
 name: thirdshift-pr
@@ -1272,7 +1272,7 @@ 

Prompts and skills

thirdshift-resolving-merge-conflicts

Used by 4 · K · Finish, in a Repair

-
+

thirdshift-resolving-merge-conflicts/SKILL.md

---
 name: thirdshift-resolving-merge-conflicts
@@ -1304,7 +1304,7 @@ 

Prompts and skills

thirdshift-security-audit

Used by the Security audit, in a Security run, or a Security review

-
+

thirdshift-security-audit/SKILL.md

---
 name: thirdshift-security-audit
@@ -6704,7 +6704,7 @@ 

Prompts and skills

thirdshift-tdd

Used by 2 · M · Implement

-
+

thirdshift-tdd/SKILL.md

---
 name: thirdshift-tdd
@@ -6905,7 +6905,7 @@ 

Prompts and skills

thirdshift-to-spec

Used by the Architecture review, in an Architect run, or Security fix publishing, in a Security run

-
+

thirdshift-to-spec/SKILL.md

---
 name: thirdshift-to-spec
@@ -7005,7 +7005,7 @@ 

Prompts and skills

thirdshift-to-tickets

Used by the Architecture review, in an Architect run, or Security fix publishing, in a Security run

-
+

thirdshift-to-tickets/SKILL.md

---
 name: thirdshift-to-tickets
diff --git a/site/tests/prompts.test.cjs b/site/tests/prompts.test.cjs
index 7c6994204..045ae53c9 100644
--- a/site/tests/prompts.test.cjs
+++ b/site/tests/prompts.test.cjs
@@ -35,7 +35,10 @@ async function pageForTest(t, options) {
   return page;
 }
 
-test("every headed section starts closed and toggles independently by its heading", async (t) => {
+// A skill's SKILL.md starts open, so it shows as soon as its skill does.
+const isSkillMd = id => /^file-.*-skill-md$/.test(id);
+
+test("every headed section but a skill's SKILL.md starts closed, and each toggles independently by its heading", async (t) => {
   const page = await pageForTest(t);
   await page.goto(`${origin}/prompts/`);
   const headings = page.locator("main :is(h2, h3, h4)");
@@ -44,10 +47,12 @@ test("every headed section starts closed and toggles independently by its headin
   for (const id of ids) {
     assert.equal(await page.locator(`#${id}`).evaluate(heading => {
       const summary = heading.closest("summary");
-      return Boolean(summary && summary.parentElement.tagName === "DETAILS" && !summary.parentElement.open);
-    }), true, `${id} must control a closed section`);
+      return Boolean(summary && summary.parentElement.tagName === "DETAILS");
+    }), true, `${id} must control a section`);
+    assert.equal(await page.locator(`#${id}`).evaluate(heading => heading.closest("details").open), isSkillMd(id), `${id} starts ${isSkillMd(id) ? "open" : "closed"}`);
   }
-  assert.equal(await page.locator("main details[open]").count(), 0);
+  assert.equal(await page.locator("main details[open]").count(), ids.filter(isSkillMd).length);
+  assert.equal(await page.locator("main > section > .wrap > details[open]").count(), 0, "nothing shows before its section opens");
   for (const id of ids) {
     const heading = page.locator(`#${id}`);
     // Open only the containing sections, as a reader does before reaching a nested heading.
@@ -63,6 +68,8 @@ test("every headed section starts closed and toggles independently by its headin
         await page.locator(`#${parent}`).click();
       }
     }
+    // Fold a SKILL.md first, so every heading is tested opening then folding.
+    if (isSkillMd(id)) await heading.click();
     await heading.click();
     assert.equal(await heading.evaluate(node => node.closest("details").open), true, id);
     const content = heading.locator("xpath=ancestor::summary/parent::details/*[not(self::summary)][1]");
@@ -76,6 +83,24 @@ test("every headed section starts closed and toggles independently by its headin
   assert.equal(await page.locator("#command-title").evaluate(node => node.closest("details").open), true);
 });
 
+test("opening a skill shows its SKILL.md, which still folds by its heading", async (t) => {
+  const page = await pageForTest(t);
+  await page.goto(`${origin}/prompts/`);
+  await page.locator("#skills-title").click();
+  const skillMd = page.locator("#file-thirdshift-code-review-skill-md").locator("xpath=ancestor::details[1]/pre");
+  assert.equal(await skillMd.isVisible(), false);
+  await page.locator("#skill-thirdshift-code-review-title").click();
+  assert.equal(await skillMd.isVisible(), true);
+  // The supporting files stay folded.
+  const others = page.locator("#skill-thirdshift-code-review h4:not(#file-thirdshift-code-review-skill-md)");
+  assert.ok(await others.count() > 0);
+  for (const heading of await others.all()) {
+    assert.equal(await heading.evaluate(node => node.closest("details").open), false);
+  }
+  await page.locator("#file-thirdshift-code-review-skill-md").click();
+  assert.equal(await skillMd.isVisible(), false);
+});
+
 test("fragment links reveal the prompt or supporting file and its containing sections", async (t) => {
   const page = await pageForTest(t);
   await page.goto(`${origin}/prompts/#prompt-resume`);
@@ -95,7 +120,7 @@ test("fragment links reveal the prompt or supporting file and its containing sec
 test("mobile readers can toggle headings with the keyboard without JavaScript", async (t) => {
   const page = await pageForTest(t, { javaScriptEnabled: false, viewport: { width: 390, height: 844 } });
   await page.goto(`${origin}/prompts/`);
-  assert.equal(await page.locator("main details[open]").count(), 0);
+  assert.equal(await page.locator("main > section > .wrap > details[open]").count(), 0);
   const prompts = page.locator("#prompts-title").locator("xpath=parent::summary");
   await prompts.focus();
   await page.keyboard.press("Enter");
diff --git a/src/prompts_page.rs b/src/prompts_page.rs
index c5e90c53f..d53eba2cf 100644
--- a/src/prompts_page.rs
+++ b/src/prompts_page.rs
@@ -517,14 +517,11 @@ fn skills_section(html: &mut String) {
         let mut files = Vec::new();
         collect_files(skill, &mut files);
         // SKILL.md first, as the agent reads it first; then the rest by path.
-        files.sort_by_key(|file| {
-            (
-                file.path().file_name() != Some(OsStr::new("SKILL.md")),
-                file.path(),
-            )
-        });
+        let is_skill_md = |file: &File| file.path().file_name() == Some(OsStr::new("SKILL.md"));
+        files.sort_by_key(|file| (!is_skill_md(file), file.path()));
         for file in files {
-            file_block(html, file, "        ");
+            // Opening a skill shows its SKILL.md; the supporting files stay folded.
+            file_block(html, file, "        ", is_skill_md(file));
         }
         html.push_str("        
\n
\n \n"); } @@ -548,7 +545,7 @@ fn licence_section(html: &mut String) { OsStr::new("LICENSE"), "a file outside any skill needs a place on the page" ); - file_block(html, file, " "); + file_block(html, file, " ", false); } html.push_str("
\n
\n\n"); } @@ -561,8 +558,8 @@ fn collect_files<'a>(dir: &'a Dir<'a>, files: &mut Vec<&'a File<'a>>) { } } -/// A file's path as a disclosure heading, then its raw text. -fn file_block(html: &mut String, file: &File, indent: &str) { +/// A file's path as a disclosure heading, then its raw text, folded unless `open`. +fn file_block(html: &mut String, file: &File, indent: &str, open: bool) { let path = file.path().to_string_lossy(); let text = file .contents_utf8() @@ -574,7 +571,8 @@ fn file_block(html: &mut String, file: &File, indent: &str) { .collect(); let _ = write!( html, - "{indent}
\n{indent}

{path}

\n{indent}
{text}
\n{indent}
\n", + "{indent}
\n{indent}

{path}

\n{indent}
{text}
\n{indent}
\n", + open = if open { " open" } else { "" }, path = escape(&path), text = escape(text), ); From 80dc0effd16432ee9db03fc1d2da13e6cbb0f539 Mon Sep 17 00:00:00 2001 From: Jacob Stephens Date: Tue, 13 Oct 2026 14:35:39 -0400 Subject: [PATCH 2/2] Match only a skill's own SKILL.md after review Open the SKILL.md at the skill's root rather than any file of that name, and have the browser tests read the rendered path rather than guess from the id. The no-JS test again checks that only SKILL.md folds start open. Co-Authored-By: Claude Opus 5.5 --- site/tests/prompts.test.cjs | 19 ++++++++++++++----- src/prompts_page.rs | 3 ++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/site/tests/prompts.test.cjs b/site/tests/prompts.test.cjs index 045ae53c9..cf1833d0f 100644 --- a/site/tests/prompts.test.cjs +++ b/site/tests/prompts.test.cjs @@ -35,8 +35,13 @@ async function pageForTest(t, options) { return page; } -// A skill's SKILL.md starts open, so it shows as soon as its skill does. -const isSkillMd = id => /^file-.*-skill-md$/.test(id); +// A skill's own SKILL.md starts open, so it shows as soon as its skill does. +const skillMdIds = async page => new Set(await page.locator("main h4").evaluateAll(nodes => + nodes.filter(node => /^[^/]+\/SKILL\.md$/.test(node.textContent)).map(node => node.id))); +const openIds = page => page.locator("main details[open]").evaluateAll(nodes => + nodes.map(node => node.querySelector("summary :is(h2,h3,h4)").id)); +// The folds whose opening shows anything at all. +const openSections = "main > section > .wrap > details[open]"; test("every headed section but a skill's SKILL.md starts closed, and each toggles independently by its heading", async (t) => { const page = await pageForTest(t); @@ -44,6 +49,9 @@ test("every headed section but a skill's SKILL.md starts closed, and each toggle const headings = page.locator("main :is(h2, h3, h4)"); assert.ok(await headings.count() > 30); const ids = await headings.evaluateAll(nodes => nodes.map(node => node.id)); + const skillMds = await skillMdIds(page); + assert.ok(skillMds.size > 5); + const isSkillMd = id => skillMds.has(id); for (const id of ids) { assert.equal(await page.locator(`#${id}`).evaluate(heading => { const summary = heading.closest("summary"); @@ -51,8 +59,8 @@ test("every headed section but a skill's SKILL.md starts closed, and each toggle }), true, `${id} must control a section`); assert.equal(await page.locator(`#${id}`).evaluate(heading => heading.closest("details").open), isSkillMd(id), `${id} starts ${isSkillMd(id) ? "open" : "closed"}`); } - assert.equal(await page.locator("main details[open]").count(), ids.filter(isSkillMd).length); - assert.equal(await page.locator("main > section > .wrap > details[open]").count(), 0, "nothing shows before its section opens"); + assert.deepEqual(new Set(await openIds(page)), skillMds); + assert.equal(await page.locator(openSections).count(), 0, "nothing shows before its section opens"); for (const id of ids) { const heading = page.locator(`#${id}`); // Open only the containing sections, as a reader does before reaching a nested heading. @@ -120,7 +128,8 @@ test("fragment links reveal the prompt or supporting file and its containing sec test("mobile readers can toggle headings with the keyboard without JavaScript", async (t) => { const page = await pageForTest(t, { javaScriptEnabled: false, viewport: { width: 390, height: 844 } }); await page.goto(`${origin}/prompts/`); - assert.equal(await page.locator("main > section > .wrap > details[open]").count(), 0); + assert.equal(await page.locator(openSections).count(), 0); + assert.deepEqual(new Set(await openIds(page)), await skillMdIds(page)); const prompts = page.locator("#prompts-title").locator("xpath=parent::summary"); await prompts.focus(); await page.keyboard.press("Enter"); diff --git a/src/prompts_page.rs b/src/prompts_page.rs index d53eba2cf..07f35fb40 100644 --- a/src/prompts_page.rs +++ b/src/prompts_page.rs @@ -517,7 +517,8 @@ fn skills_section(html: &mut String) { let mut files = Vec::new(); collect_files(skill, &mut files); // SKILL.md first, as the agent reads it first; then the rest by path. - let is_skill_md = |file: &File| file.path().file_name() == Some(OsStr::new("SKILL.md")); + let skill_md = skill.path().join("SKILL.md"); + let is_skill_md = |file: &File| file.path() == skill_md; files.sort_by_key(|file| (!is_skill_md(file), file.path())); for file in files { // Opening a skill shows its SKILL.md; the supporting files stay folded.