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..cf1833d0f 100644
--- a/site/tests/prompts.test.cjs
+++ b/site/tests/prompts.test.cjs
@@ -35,19 +35,32 @@ async function pageForTest(t, options) {
   return page;
 }
 
-test("every headed section starts closed and toggles independently by its heading", async (t) => {
+// 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);
   await page.goto(`${origin}/prompts/`);
   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");
-      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.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.
@@ -63,6 +76,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 +91,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 +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 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 c5e90c53f..07f35fb40 100644
--- a/src/prompts_page.rs
+++ b/src/prompts_page.rs
@@ -517,14 +517,12 @@ 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 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 {
-            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 +546,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 +559,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 +572,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), );