diff --git a/manager/core/board.html b/manager/core/board.html index c3df5c1..a0766fa 100644 --- a/manager/core/board.html +++ b/manager/core/board.html @@ -309,6 +309,14 @@ font-family:var(--mono);font-size:11.5px;line-height:1.5;color:var(--muted); white-space:pre-wrap;overflow-x:auto; } + /* a well as a real disclosure: the summary IS the well, so its › is earned + — it rotates open instead of the fold's usual ▸ prefix */ + .fold.wellfold{max-width:none} + .fold.wellfold summary::before{content:none} + .fold.wellfold summary::-webkit-details-marker{display:none} + .fold.wellfold .lead{transition:transform .12s ease} + .fold.wellfold[open] .lead{transform:rotate(90deg)} + .fold.wellfold pre{max-height:300px;overflow-y:auto} .stopbtn:hover{border-color:var(--alarm);color:var(--alarm)} .f-empty{padding:30px 20px;font-size:12.5px;color:var(--dim)} @@ -857,11 +865,11 @@ function cardFor(task) { const last = run || (smeta && smeta.lastSummary ? { summary: smeta.lastSummary, ok: null } : null); if (last && last.summary) { const active = run || (smeta && smeta.status === 'active'); - liveLine = `
` + + liveLine = `
·` + `${esc(last.summary)}${active ? '' : ''}
`; } } else if (agent) { - liveLine = `
warming up
`; + liveLine = `
·warming up
`; } // tool chips: destinations, not statuses — they live in the card's footer @@ -1448,10 +1456,21 @@ function renderFocus() { const last = run || events[events.length - 1]; const wellCls = last && last.ok === false ? ' bad' : ''; - const act = last - ? `
${esc(last.summary)}` + - `${(run || live) ? '' : ''}
` - : '
no activity yet
'; + const caret = (run || live) ? '' : ''; + let act; + if (last && last.detail) { + // › only when clicking does something: the well is the summary of a + // fold, keyed into S.openFolds so it survives the SSE re-renders + const wkey = `w${last.ts}`; + act = `
` + + `
` + + `${esc(last.summary)}${caret}
` + + `
${esc(last.detail)}
`; + } else if (last) { + act = `
·${esc(last.summary)}${caret}
`; + } else { + act = '
·no activity yet
'; + } const plan = [...events].reverse().find(e => e.kind === 'plan' && e.detail); let steps = ''; @@ -1526,6 +1545,11 @@ function renderFocus() { `
${recent || 'nothing yet'}
`; $('#cgrid').innerHTML = nowPanel + `
${checksPanel}${filesPanel}${recentPanel}
`; + document.querySelectorAll('#cgrid details.fold').forEach(d => { + d.addEventListener('toggle', () => { + if (d.open) S.openFolds.add(d.dataset.key); else S.openFolds.delete(d.dataset.key); + }); + }); const cstop = $('#cstop'); if (cstop) cstop.addEventListener('click', () => stopAgent(cstop.dataset.aid)); diff --git a/tests/test_focus_well.py b/tests/test_focus_well.py new file mode 100644 index 0000000..1b4b955 --- /dev/null +++ b/tests/test_focus_well.py @@ -0,0 +1,83 @@ +"""The Focus "Right now" well and the wells' visual grammar (task 08). + +board.html is a single file with inline JS and no frontend test runner, so +these are source-level invariants: the ones that, if broken, would bring +back the decorative chevron or lose the fold's keyed state across SSE +re-renders. + + python3 -m unittest discover -s tests -v +""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +BOARD = Path(__file__).resolve().parents[1] / "manager" / "core" / "board.html" + + +class FocusWellTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.html = BOARD.read_text(encoding="utf-8") + + def test_chevron_only_where_clicking_does_something(self): + """One glyph, one meaning: every › worn by a well's lead span must + sit inside the wellfold disclosure — no static well may wear it.""" + chevrons = [m.start() for m in + re.finditer(r'›', self.html)] + self.assertTrue(chevrons, "the disclosure well lost its chevron") + for pos in chevrons: + window = self.html[max(0, pos - 300):pos] + self.assertIn('class="fold wellfold"', window, + "a well outside the wellfold disclosure wears ›, " + "but clicking it does nothing") + self.assertIn("", window, + "the › must be inside the disclosure's summary") + + def test_static_wells_wear_a_neutral_lead(self): + """The board cards' live line and the empty-focus well are plain + divs; after the audit they lead with · (or the drive's ✳).""" + for m in re.finditer(r'
(.)', + self.html): + if self.html[:m.start()].rstrip("` +\n").endswith(""): + continue # the wellfold's own summary — the earned › + self.assertIn(m.group(1), "·✳", + f"static well leads with {m.group(1)!r}; " + "only the wellfold summary may use ›") + + def test_report_detail_renders_preformatted(self): + """The full report opens as machine-adjacent prose: a
 right
+        after the wellfold's summary, scrollable when long."""
+        self.assertRegex(self.html,
+                         r"
` \+\s*`
\$\{esc\(last\.detail\)\}
", + "the wellfold must render last.detail in a
")
+        self.assertRegex(self.html, r"\.fold\.wellfold pre\{[^}]*max-height",
+                         "long reports need a scroll bound on the fold's pre")
+
+    def test_open_state_is_keyed_not_dom_only(self):
+        """Focus re-renders every few seconds over SSE; the fold must be
+        re-created from S.openFolds and write back on toggle, exactly like
+        the Sessions timeline's folds."""
+        m = re.search(r"if \(last && last\.detail\) \{(.*?)\}\s*else",
+                      self.html, re.S)
+        self.assertIsNotNone(m, "the Focus well no longer branches on detail")
+        for needle in ("data-key", "S.openFolds.has"):
+            self.assertIn(needle, m.group(1),
+                          f"the wellfold lost {needle}: open state would "
+                          "snap shut on the next re-render")
+        self.assertIn("#cgrid details.fold", self.html,
+                      "renderFocus must re-attach toggle listeners so "
+                      "toggles land back in S.openFolds")
+
+    def test_sessions_timeline_fold_unchanged(self):
+        """The timeline's own fold mechanism is the pattern being reused,
+        not replaced."""
+        self.assertIn("function fold(key, label, body)", self.html)
+        self.assertIn("ev.kind === 'report' ? 'the report'", self.html)
+        self.assertIn("#ftl details", self.html)
+
+
+if __name__ == "__main__":
+    unittest.main()