clone

a hermes agent plugin for openspec: tools for spec workflows and a dashboard tab for browsing changes and specs

Parse spec files into structured UI: purpose, requirements, scenarios

dami etoile committed 325a322
commit
325a3225c60da666373caad14cb92f73cd0742e3
parent
cbee4d0ec46b1988f56a4c934aa7ad071147149d
author
dami etoile <dami.etoile@gmail.com>
2 files changed, +122 -10
Mdashboard/dist/index.js106+++++++++++++++++++++++++++---
Mdashboard/dist/style.css26+++++++-
M dashboard/dist/index.js +97 -9
@@ -262,20 +262,21 @@
262262 h("div", { className: "os-col-list" },263263 items.length === 0 ? h("span", { className: "os-col-empty" }, "—") :264264 items.map(function (it) {265- var artifacts = [];266- if (it.hasProposal) artifacts.push({ c: "#60a5fa", label: "proposal" });267- if (it.hasTasks) artifacts.push({ c: "#34d399", label: "tasks" });268- if (it.hasDesign) artifacts.push({ c: "#a78bfa", label: "design" });269- if (it.hasSpecs) artifacts.push({ c: "#fbbf24", label: "specs" });265+ var artifacts = [266+ { on: it.hasProposal, c: "#60a5fa", label: "proposal" },267+ { on: it.hasTasks, c: "#34d399", label: "tasks" },268+ { on: it.hasDesign, c: "#a78bfa", label: "design" },269+ { on: it.hasSpecs, c: "#fbbf24", label: "specs" },270+ ];270271 var pct = it.taskStats && it.taskStats.total > 0 ? Math.round((it.taskStats.done / it.taskStats.total) * 100) : 0;271272 return h("button", {272273 key: it.id || it.name, className: cn("os-card", STATUS_TONE[it.status] || ""), onClick: function () { onSelectItem(it); },273274 },274275 h("div", { className: "os-card-title" }, it.title || it.name),275276 h("div", { className: "os-card-foot" },276- artifacts.length ? h("span", { className: "os-card-artifacts" },277- artifacts.map(function (a, i) { return h("span", { key: i, className: "os-card-dot", title: a.label, style: { background: a.c } }); })278- ) : null,277+ h("span", { className: "os-card-artifacts" },278+ artifacts.map(function (a, i) { return h("span", { key: i, className: cn("os-card-dot", !a.on && "os-card-dot-off"), title: a.label }); })279+ ),279280 it.taskStats ? h("span", { className: "os-card-progress" },280281 h("span", { className: "os-card-progress-track" }, h("span", { className: "os-card-progress-fill", style: { width: pct + "%" } })),281282 h("span", { className: "os-card-stats" }, it.taskStats.done + "/" + it.taskStats.total)
@@ -396,6 +397,93 @@
396397 );397398 }398399 400+ // ── Spec content parser + structured view ───────────────────────────401+ function parseSpec(md) {402+ if (!md) return { title: "", purpose: "", requirements: [] };403+ var lines = md.split("\n");404+ var title = "", purpose = "";405+ var reqs = [];406+ var state = "top"; // top | purpose | reqDesc | scenario407+ var currentReq = null, currentScn = null;408+409+ for (var i = 0; i < lines.length; i++) {410+ var line = lines[i];411+ var trim = line.trim();412+413+ if (trim.startsWith("# ") && !trim.startsWith("## ")) {414+ title = trim.replace(/^#\s+/, "").replace(/\s+Specification\s*$/i, "");415+ state = "top"; continue;416+ }417+ if (/^##\s+Purpose/i.test(trim)) { state = "purpose"; continue; }418+ if (/^##\s+Requirements/i.test(trim)) { state = "top"; continue; }419+ if (/^###\s+Requirement:\s*(.+)$/i.test(trim)) {420+ var m = trim.match(/^###\s+Requirement:\s*(.+)$/i);421+ currentReq = { name: m[1].trim(), description: "", scenarios: [] };422+ reqs.push(currentReq);423+ currentScn = null;424+ state = "reqDesc"; continue;425+ }426+ if (/^####\s+Scenario:\s*(.+)$/i.test(trim)) {427+ var m2 = trim.match(/^####\s+Scenario:\s*(.+)$/i);428+ currentScn = { name: m2[1].trim(), steps: [] };429+ if (currentReq) currentReq.scenarios.push(currentScn);430+ state = "scenario"; continue;431+ }432+433+ // Accumulate text based on state434+ if (state === "purpose" && trim) {435+ purpose += (purpose ? "\n" : "") + trim;436+ } else if (state === "reqDesc" && trim) {437+ if (currentReq) currentReq.description += (currentReq.description ? "\n" : "") + trim;438+ } else if (state === "scenario" && trim) {439+ var step = trim.match(/^[-*]\s+\*\*(\w+)\*\*\s*(.+)$/);440+ if (step && currentScn) {441+ currentScn.steps.push({ type: step[1].toUpperCase(), text: step[2].trim() });442+ } else if (currentScn) {443+ // Non-bullet text inside scenario — append to last step or skip444+ if (currentScn.steps.length) {445+ currentScn.steps[currentScn.steps.length - 1].text += " " + trim;446+ }447+ }448+ }449+ }450+ return { title: title, purpose: purpose, requirements: reqs };451+ }452+453+ function SpecContentView(_ref) {454+ var md = _ref.md;455+ var spec = useMemo(function () { return parseSpec(md); }, [md]);456+ if (!spec.title && !spec.purpose && !spec.requirements.length) {457+ return h(Markdown, { md: md });458+ }459+ return h("div", { className: "os-spec-content" },460+ spec.purpose ? h("div", { className: "os-spec-purpose" },461+ h("div", { className: "os-spec-purpose-label" }, "Purpose"),462+ h("p", null, spec.purpose)463+ ) : null,464+ spec.requirements.map(function (req, ri) {465+ return h("div", { key: ri, className: "os-spec-req" },466+ h("div", { className: "os-spec-req-head" },467+ h("span", { className: "os-spec-req-num" }, "R" + (ri + 1)),468+ h("span", { className: "os-spec-req-name" }, req.name)469+ ),470+ req.description ? h("p", { className: "os-spec-req-desc" }, req.description) : null,471+ req.scenarios.map(function (scn, si) {472+ return h("div", { key: si, className: "os-spec-scn" },473+ h("div", { className: "os-spec-scn-name" }, scn.name),474+ scn.steps.map(function (st, sti) {475+ return h("div", { key: sti, className: cn("os-spec-step", "os-spec-step-" + st.type.toLowerCase()) },476+ h("span", { className: "os-spec-step-label" }, st.type),477+ h("span", { className: "os-spec-step-text" }, st.text)478+ );479+ })480+ );481+ })482+ );483+ })484+ );485+ }486+399487 // ── Specs view (spec browser: current / dirty / refs) ────────────────400488 var SORT_MODES = [401489 { value: "alpha", label: "A→Z" },
@@ -509,7 +597,7 @@
509597 selFile.ctime ? h("span", { title: selFile.ctime }, "created ", isoTimeAgo(selFile.ctime)) : null,510598 selFile.mtime ? h("span", { title: selFile.mtime }, "edited ", isoTimeAgo(selFile.mtime)) : null511599 ) : null,512- mode === "current" ? h(Markdown, { md: selFile.after }) :600+ mode === "current" ? h(SpecContentView, { md: selFile.after }) :513601 h("div", { className: "os-spec-diff" },514602 h("div", { className: "os-diff-cols" },515603 h("div", { className: "os-diff-col" }, h("div", { className: "os-diff-col-head" }, "before"), h(Markdown, { md: selFile.before })),
M dashboard/dist/style.css +25 -1
@@ -45,7 +45,12 @@
4545 .os-card-copy { margin-left: auto; display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; border: none; background: transparent; color: var(--color-muted-foreground, #777); cursor: pointer; border-radius: var(--radius-sm, 0.25rem); }4646 .os-card-copy:hover { color: var(--color-foreground, #eee); background: var(--color-secondary, rgba(255,255,255,0.06)); }4747 .os-card-artifacts { display: inline-flex; align-items: center; gap: 0.25rem; }48-.os-card-dot { display: inline-block; width: 0.4375rem; height: 0.4375rem; border-radius: 50%; }48+.os-card-dot { display: inline-block; width: 0.4375rem; height: 0.4375rem; border-radius: 50%; background: var(--c, transparent); }49+.os-card-dot:nth-child(1) { --c: #60a5fa; }50+.os-card-dot:nth-child(2) { --c: #34d399; }51+.os-card-dot:nth-child(3) { --c: #a78bfa; }52+.os-card-dot:nth-child(4) { --c: #fbbf24; }53+.os-card-dot-off { --c: var(--color-muted-foreground, #555); opacity: 0.4; }4954 .os-card-progress { display: flex; align-items: center; gap: 0.25rem; }5055 .os-card-progress-track { width: 2.5rem; height: 0.25rem; border-radius: 0.125rem; background: var(--color-border, rgba(255,255,255,0.15)); overflow: hidden; }5156 .os-card-progress-fill { display: block; height: 100%; border-radius: 0.125rem; background: var(--color-primary, #60a5fa); }
@@ -142,6 +147,25 @@
142147 .os-specs-detail-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }143148 .os-specs-detail-path { font-family: var(--theme-font-mono, monospace); font-size: 0.8rem; font-weight: 500; color: var(--color-foreground, #eee); }144149 .os-specs-meta { display: flex; gap: 0.75rem; margin-bottom: 0.5rem; font-size: 0.7rem; color: var(--color-muted-foreground, #888); }150+151+/* Structured spec content */152+.os-spec-content { display: flex; flex-direction: column; gap: 1rem; }153+.os-spec-purpose { border-left: 3px solid var(--color-primary, #60a5fa); padding-left: 0.625rem; }154+.os-spec-purpose-label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-muted-foreground, #777); margin-bottom: 0.25rem; }155+.os-spec-purpose p { margin: 0; font-size: 0.85rem; line-height: 1.5; color: var(--color-foreground, #eee); }156+.os-spec-req { border: 1px solid var(--color-border, #2a2a2a); border-radius: var(--radius-sm, 0.375rem); overflow: hidden; }157+.os-spec-req-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.625rem; background: var(--color-secondary, rgba(255,255,255,0.04)); }158+.os-spec-req-num { font-size: 0.65rem; font-weight: 700; font-family: var(--theme-font-mono, monospace); color: var(--color-primary, #60a5fa); background: color-mix(in srgb, var(--color-primary, #60a5fa) 12%, transparent); padding: 0.0625rem 0.3125rem; border-radius: var(--radius-sm, 0.25rem); }159+.os-spec-req-name { font-size: 0.82rem; font-weight: 600; color: var(--color-foreground, #eee); }160+.os-spec-req-desc { margin: 0; padding: 0.5rem 0.625rem; font-size: 0.8rem; line-height: 1.5; color: var(--color-muted-foreground, #bbb); }161+.os-spec-scn { margin: 0 0.625rem 0.5rem; padding: 0.5rem; border-left: 2px solid var(--color-border, #333); border-radius: 0 0.25rem 0.25rem 0; background: color-mix(in srgb, var(--color-secondary, rgba(255,255,255,0.03)) 50%, transparent); }162+.os-spec-scn-name { font-size: 0.75rem; font-weight: 600; color: var(--color-foreground, #ddd); margin-bottom: 0.375rem; }163+.os-spec-step { display: flex; align-items: flex-start; gap: 0.375rem; padding: 0.125rem 0; font-size: 0.76rem; line-height: 1.4; }164+.os-spec-step-label { flex-shrink: 0; font-size: 0.62rem; font-weight: 700; font-family: var(--theme-font-mono, monospace); min-width: 2.5rem; text-align: right; }165+.os-spec-step-text { flex: 1; color: var(--color-foreground, #ddd); }166+.os-spec-step-when .os-spec-step-label { color: #38bdf8; }167+.os-spec-step-then .os-spec-step-label { color: #2dd4bf; }168+.os-spec-step-and .os-spec-step-label { color: var(--color-muted-foreground, #888); }145169 .os-diff-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 0.75rem; }146170 .os-diff-col { border: 1px solid var(--color-border, #2a2a2a); border-radius: var(--radius-sm, 0.375rem); padding: 0.5rem; overflow-x: auto; }147171 .os-diff-col-head { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--color-muted-foreground, #777); margin-bottom: 0.375rem; }