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 @@
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),
);