diff options
Diffstat (limited to 'todo.org')
| -rw-r--r-- | todo.org | 221 |
1 files changed, 108 insertions, 113 deletions
@@ -109,10 +109,6 @@ Phase 5 (commit =9daeff15=). Editing a family base recolors the whole family (sh *** 2026-06-10 Wed @ 01:17:45 -0500 Warnings, seeding, export, README close-out landed Phase 6 (commit =c175e2be=). Export stays a flat palette and import needs no reconstruction (#roundtriptest: export→import→export byte-identical). =seedPkgmap= reads the flat palette unchanged. The too-similar warning stays on the full palette — the planned ramp-step exemption was dropped after analysis: ramp steps are a stepL apart (well above the ΔE threshold) so they never warn, and exempting same-family pairs would hide genuine near-duplicates (caught by #deltatest). README documents families, the ground strip, the count control/regenerate, removed-step references, and the ramp-panel removal. -** DONE [#B] Color-family grouping for hue-adjacent warm colors :feature:theme-studio:research: -CLOSED: [2026-06-10 Wed] -Resolved by two independent reviews of =~/color-sorting.org= (=~/color-sorting-codex.org=, =~/color-sorting-fable.org=, Fable's harness at =~/working/color-sorting-fable/=). Both converged on lightness-conditioned complete-linkage clustering + a floored neutral threshold; implemented in commit =04b82bbe= (replacing the hue anchors). Measured F1 0.63→0.96 on the real palette: gold and olive separate, red/blue ramps stay whole, intense-red isolates, all grays/steels consolidate, and the gray+1/gray+2/white neutral-leak bugs are fixed. The only residual (pale yellow+2 lands on the olive ramp) is geometrically irreducible from the hex — see the hint-override task below. - ** TODO [#C] Color-family per-hex hint override :feature:theme-studio: For the ~1 color per palette that sits on a ramp-collision point (e.g. yellow+2 on the distinguished palette, which by every hex signal belongs to the olive ramp though its name says gold), automatic grouping cannot recover the designer's intent. Add a per-hex family override: drag a swatch to a different column, store the override keyed by hex (never the name, so renaming is still free), consult it after the LCCL clustering, and drop/mark-stale it when the hex changes substantially. Export stays mostly flat; only overrides are extra metadata. Both reviews recommend this exact shape; details in =~/color-sorting-fable.org= (§ "The irreducible case") and =~/color-sorting-codex.org= (§ "What to store"). @@ -122,30 +118,6 @@ Show the currently-playing song while streaming an internet radio station. Lives ** TODO [#C] Evaluate jamescherti essential-emacs-packages list :packages:research: Review [[https://www.jamescherti.com/essential-emacs-packages/][James Cherti's essential Emacs packages]] for anything worth installing. Cross-check each candidate against what is already in the config (=modules/= + =init.el=), skip the ones already present, and shortlist the genuinely new ones with a one-line rationale. Future-installation research, not a commitment to install. -** DONE [#B] theme-studio comprehensive previews (org/magit/elfeed/ghostel/mu4e/dashboard) :feature:theme:theme-studio: -CLOSED: [2026-06-08 Mon] -Expanded the bespoke previews to near-complete face coverage and added three new ones. org now exercises 83/88 faces (document + agenda; the 5 skipped are non-visual: org-hide, org-indent, org-clock-overlay, org-default, org-date-selected). magit 97/98 (status buffer + blame/reflog/sequence/bisect/signature sampler rows). elfeed 13/13. New bespoke previews: ghostel 19/19 (mock terminal, 16 ANSI colors + default + fake cursor), mu4e 37/37 (curated face list, not in the generated inventory; headers list + message view + compose), dashboard 8/8. So clicking a face row flashes a real preview element for nearly every face. Originally filed as just the org preview. - -** DONE [#A] theme-studio theme.json -> dupre-*.el converter :feature:theme:theme-studio: -CLOSED: [2026-06-08 Mon] -Built as scripts/theme-studio/build-theme.el (sibling to build-inventory.el), emitting a single self-contained themes/<name>-theme.el deftheme (not the palette/faces/theme trio — a theme.json carries resolved per-face hex, not dupre's semantic layer). All four tiers convert: default from assignments.bg/.p, syntax categories -> font-lock/tree-sitter faces with bold/italic sets, UI passthrough, packages with :inherit/:height/weight/slant. 20 ERT tests in tests/test-build-theme.el (Normal/Boundary/Error + an end-to-end load + a WCAG-AA assertion on the round-tripped result). One mapping limitation documented: the dec (decorator) key has no independent Emacs face (Emacs renders decorators with font-lock-type-face, which ty owns), so dec is omitted and decorators follow the type color. - -The last link in the pipeline: turn a theme.json exported by the theme-studio into a real loadable Emacs theme. Elisp (per Craig), TDD — this is the correctness-sensitive piece. - -Inputs (all on disk; no chat history needed): -- theme.json contract: =scripts/theme-studio/README.md= (theme.json section) and =docs/design/theme-studio-package-faces-spec.org= (State and export policy, Relative height, Inheritance). -- Reference face layout: existing =themes/dupre-palette.el= + =themes/dupre-faces.el= + =themes/dupre-theme.el=, and =tests/test-dupre-theme.el= (WCAG-contrast helper to reuse). -- Conventions: =.claude/rules/elisp.md=, =.claude/rules/elisp-testing.md=. - -Scope: -1. Read theme.json. Set =default= from =assignments.bg= / =assignments.p=. -2. Author the syntax category -> font-lock face map (~21 keys: kw->font-lock-keyword-face, str->font-lock-string-face, fnd->font-lock-function-name-face, fnc->font-lock-function-call-face, op->font-lock-operator-face, punc->font-lock-punctuation-face, etc. incl. the Emacs-29 tree-sitter additions). Apply =bold= / =italic= sets. -3. UI faces: the =ui= keys are already real face names (region, cursor, mode-line, ...) -> near 1:1 passthrough of fg/bg. -4. Package faces: =packages= -> each face spec, writing =:inherit PARENT= for inherited faces + only the overridden attrs, =:height= when != 1.0, weight/slant. -5. Emit a deftheme file (or palette+faces+theme trio mirroring dupre's layout). - -TDD targets: old-JSON (no packages) loads; every category maps; round-trip of fg/bg/bold/italic/inherit/height into valid face specs; WCAG-contrast assertion on the result. Decide whether the converter lives under =scripts/theme-studio/= (emits to =themes/=) or =themes/=. - ** TODO [#B] Dupre diff-changed / diff-refine-changed legibility :bug:dupre: Surfaced 2026-06-07 from a pearl session designing its modified-ticket indicator (pearl marks a changed field by inheriting =diff-changed=). dupre's =diff-refine-changed= is bright gold (#ffd700) under near-white text (#f0fef0) -- WCAG contrast ~1.35, unreadable as a plain background. It only looks fine inside diff-mode because diff-mode overlays its own dark foreground. =diff-changed= (#875f00 amber) is ~5.49, readable but off the modus model. Every modus variant keeps both faces legible (contrast 9-16) by pairing a dark low-saturation background with a hue-matched foreground. @@ -168,49 +140,6 @@ A full =make test= run (2026-06-07) is green across 516 of 517 files; the only f Build a new theme (working name "dupre-clear", final name TBD) that takes dupre's color identity and rebuilds it Prot's way: contrast-first, targeting WCAG AAA (~7:1 on the ground), where the in-progress dupre revision is mood/depth-first and lands at AA. Same hues (dupre blue, emerald, gold, terracotta, regal violet, mint) brightened to clear the AAA floor; same modus-style role mapping (blue keywords bold, gold functions, violet types, emerald strings, terracotta constants, silver default, warm-grey comments, metallic greys, navy + regal fills). Build the dupre revision first; this reuses its hue choices as the starting point. Full design + methodology + starting palette + open questions in the spec: [[file:docs/design/dupre-clear-theme.org][docs/design/dupre-clear-theme.org]]. Key prerequisite/context: the dupre-redesign entry in =.ai/session-context.org= (the AA palette this brightens). Hardest slot: blue keywords (a deep dupre blue can't be AAA on near-black — decide brighten vs keep-AA-exception vs lift-the-ground). -** DONE [#B] theme-studio tier-3 package faces :feature:theme:theme-studio: -CLOSED: [2026-06-08 Mon] -Package-specific face editing in the theme-studio: org/magit/elfeed bespoke (complete face tables + live previews) plus a generated all-package inventory so every installed package is themeable. Spec is Ready, all opens resolved: [[file:docs/design/theme-studio-package-faces-spec.org][docs/design/theme-studio-package-faces-spec.org]]. Phases below run in dependency order; phases 1-5 deliver the three high-value apps, phase 6 opens the long tail, phase 7 documents. The =theme.json= -> =dupre-*.el= converter (Elisp) is a separate downstream task. - -*** 2026-06-08 Mon @ 00:17:41 -0500 Phase 1 — package state + schema landed -Added =APPS= (org starter) and =PKGMAP= ({app:{face:{fg,bg,bold,italic,inherit,height,source}}}), pure helpers (=seedPkgmap= / =packagesForExport= / =mergePackagesInto=), and wired export/import for the =packages= key with old-JSON compat. The =height= float (relative size, read off the face not cascaded through inherit) and the fixed-pitch inherits are seeded in the org starter. No UI yet (Phase 3). Verified: node-check, plus a guarded =#selftest= harness (headless Chrome) confirming seed->export->import round-trip, old-JSON merge, and inherit/height/source survival — all PASS. - -*** 2026-06-08 Mon @ 02:16:24 -0500 Phase 2 — curated app data (org/magit/elfeed) landed -Filled =APPS= with the complete own-defface sets built from embedded face-name lists + a curated seed-color map: org 88 (85 seeded, incl. org-agenda, heading heights, fixed-pitch inherits), magit 98 (64 seeded), elfeed 13 (all seeded). Long-tail faces seed to default fg. Verified: 199 faces total, no seed typos / no dupes, schema self-test PASS seeding all of them. Seeded-default aesthetics still go to Manual testing once the Phase 3 UI lands. - -*** 2026-06-08 Mon @ 02:23:56 -0500 Phase 3 — package face table UI landed -Added the "package faces" section: app selector (org/magit/elfeed), per-app face table with fg/bg dropdowns, bold/italic toggles, inherit dropdown (base faces + the app's own faces), relative-height stepper, live contrast readout on the effective (inherit-resolved) color, per-face and per-app reset, and a text filter. Refactored the fg/bg dropdown into a shared =colorDropdown= helper the ui-faces table now also uses (no =uiSelect= fork). Palette edits propagate to package faces; import/export carry them. Right pane is the generic preview (face names in their own resolved colors) until the bespoke org/magit/elfeed previews land (phases 4-5). Verified: node, headless screenshot, schema self-test PASS. - -*** 2026-06-08 Mon @ 02:27:51 -0500 Phase 4 — org preview landed -Added =renderOrgPreview()=: a mock org document painted live from the org package faces (title, headings with heights, TODO/DONE, tag, scheduled date, property drawer, inline code/verbatim, link, checkbox, quote, src block, header-row table). The preview pane dispatches on the app's preview key; org-mode gets this, others keep the generic list. Verified: node, headless screenshot, self-test PASS. - -*** 2026-06-08 Mon @ 02:30:42 -0500 Phase 5 — magit + elfeed previews landed -Bespoke =renderMagitPreview()= (status buffer: head/branches, untracked, a diff hunk with context/added/removed, recent commits with hashes/authors/keyword/tag) and =renderElfeedPreview()= (search list: filter, dated entries with feed/unread-title/read-title/tags, log lines by level). The preview label now names the app and notes generic vs bespoke. Verified: node, headless screenshots, self-test PASS. - -*** 2026-06-08 Mon @ 02:32:44 -0500 Phase 6 — generated all-package inventory landed -=build-inventory.el= (loaded into a running Emacs) groups every installed package's faces by the defining package and writes =package-inventory.json=. =generate.py= embeds it and merges each package into the dropdown as an editable generic app, leaving org/magit/elfeed bespoke. 40 apps now (3 bespoke + 37 inventory, 643 faces). Committed data artifact, refreshed by reloading the .el; never browser-side discovery. Verified: node, self-test PASS, app count + bespoke-preserved checks. - -*** 2026-06-08 Mon @ 02:34:01 -0500 Phase 7 — docs landed -Rewrote =README.md= for the full tool: three face tiers + palette, the in-page picker (with the AA/AAA mask), package faces (bespoke vs generic previews), modeled inheritance + relative height (family stays in font-config.el), the packages schema with inherit/height/source, export-vs-save, and the inventory-refresh command (=build-inventory.el=) + its loaded-config dependency. Notes =theme-studio.html= is generated. Test-surface fixtures tracked separately below. - -*** 2026-06-08 Mon @ 02:40:00 -0500 theme-studio tier 3 — test surface landed -Extended the guarded =#selftest= harness (headless Chrome) to assert the acceptance criteria against the real emitted code: old-JSON import (no =packages=), full round-trip (fg/bg/bold/italic/inherit/height/source), cleared-state export, unknown-package/face preservation, and inheritance-cycle termination — all PASS. The two DOM-coupled regressions are handled structurally: =updateColor= remaps =PKGMAP= on a palette-color edit, and =PKGMAP= stores hexes so a deleted palette color leaves package refs in the "(gone)" recoverable state. =generate.py= rebuilds =theme-studio.html= each run. - -** DONE [#B] theme-studio perceptual color metrics :feature:theme:theme-studio: -CLOSED: [2026-06-08 Mon] -Spec (Ready, opens confirmed 2026-06-08): [[file:docs/design/theme-studio-perceptual-color-metrics-spec.org][docs/design/theme-studio-perceptual-color-metrics-spec.org]]. OKLCH model + perceptual-L/APCA readouts + pairwise ΔE, for building low-contrast themes by metric rather than by eye. All five phases shipped 2026-06-08 (commits 49342bf5, 78260018, 77c7f126, 163d3730, 22605426, 582d8a6a): colormath.js core inlined + WCAG/HSV helpers migrated; picker OKLCH/APCA readouts; palette ΔE warnings; OKLCH edit-model dials; C×L gamut plane. 17 Node tests (colormath 100/93.75/100), six browser hash gates green, inline-integrity guard. vNext deferrals (low-contrast preset, CIEDE2000) remain the two [#D] tasks below. Manual eyeballs tracked under Manual testing. -*** 2026-06-08 Mon @ 19:43:50 -0500 Color-math foundation + Node tests landed -Pure color core in =scripts/theme-studio/colormath.js= (OKLab/OKLCH, APCA-W3 0.1.9 exact constants, ΔE-OK, binary-search gamut clamp returning ={hex,clamped}=) shipped in 49342bf5; this phase finished the integration in 78260018. =generate.py= now inlines the colormath.js body into the page script (export-stripped, =COLORMATH_J= placeholder), and the page's lin/rl/contrast/rating/hsv2rgb/rgb2hsv/hex2rgb/rgb2hex copies moved into the module — =rl= reuses the canonical =lin= (0.04045 cutoff), byte-identical to the old 0.03928 form on every #rrggbb (no 8-bit channel falls between the cutoffs; verified over 200k pairs, zero contrast change). =test-colormath.mjs= gained Normal/Boundary/Error cases for the migrated helpers, a seeded hsv-rgb round-trip property test, and an inline-integrity check that the generated page carries the module body verbatim. Gate met: =node --test scripts/theme-studio/*.mjs= 15 pass, colormath.js 100% line / 93.75% branch / 100% func; =node --check= on the spliced script clean; =#selftest= + =#cursortest= PASS in headless Chrome. NOTE: =node --test <dir>= directory-globbing is broken on Node v26 (tries to load the dir as a module) — use the =*.mjs= glob form. -*** 2026-06-08 Mon @ 19:55:53 -0500 Picker OKLCH/APCA readouts landed -Phase 2 shipped in 77c7f126. Second readout row (=.pinfo2=) under the WCAG ratio: OKLCH L/C/H + signed APCA Lc against the ground color, always shown; sign convention in the APCA tooltip + README. Tables unchanged (APCA picker-only per Agreed-decision #3). =pkReadout= drives the spans from the inlined colormath functions. Gate met: =#readouttest= asserts the spans match the live computation AND the known dupre-blue OKLCH reference (L 0.591 / C 0.052 / H 252°, APCA Lc -34 on ground) with WCAG unchanged; =#selftest= + =#cursortest= still PASS; 15 Node tests green. Headless-rendered values verified against a node cross-check. Visual eyeball is the open "Perceptual readouts read well in the picker" item under Manual testing. -*** 2026-06-08 Mon @ 20:44:39 -0500 Palette ΔE warnings landed -Phase 3 shipped in 163d3730. =renderPalette= runs a pairwise OKLab ΔE over PALETTE via the pure =paletteDeltas()= (one pass → sub-threshold pairs + per-color nearest distance); warns on pairs below the named =DELTAE_MIN= (0.02), sorted closest-first, capped at 5 with "and N more"; each chip's tooltip gains its nearest-neighbor ΔE. Names go through =esc= before the warning markup. Gate met: =#deltatest= PASS (near pair fires + names itself; spread palette quiet; 7-color cluster caps at 5 ascending + overflow suffix). #readouttest/#selftest/#cursortest + 15 Node tests still green. Screenshot-verified the warning render (terracotta "too-similar colors" header + "blue / blue2 — ΔE 0.007, hard to distinguish", placed between palette and add-color controls). Pushed below. -*** 2026-06-08 Mon @ 21:05:28 -0500 OKLCH sliders + color-model control landed -Phase 4a shipped in 22605426. Picker gains an edit-model toggle (HSV/OKLCH) in its own =pkModel= state, orthogonal to =pkMode= (AA/AAA mask) — separate handlers, distinct toggle colors (blue vs gold). OKLCH mode shows L/C/H as paired range+number inputs driving =oklch2hex= → hex/swatch/readouts/HSV-cursor; out-of-gamut chroma snaps the dials to the reachable color + shows "chroma clamped to sRGB". HSV stays default; SV square still edits HSV (C×L plane is 4b); SV drag in OKLCH mode refreshes the dials. =openPicker= re-asserts the model via =setPkModel= so the toggle highlight can't drift (caught on screenshot). Gate met: =#oklchtest= PASS (color preserved on model switch; mask toggle leaves pkModel; model switch leaves pkMode; dials drive color to a known OKLCH target; out-of-gamut C raises clamp status). All 5 browser gates + 15 Node tests green; screenshot-verified the dials + toggle highlight. -*** 2026-06-08 Mon @ 21:41:49 -0500 Chroma×Lightness plane landed -Phase 4b shipped in 582d8a6a. OKLCH mode renders the SV square as a C(x)×L(y) plane at the current hue; crosshair maps to (C,L), hue strip selects H. Out-of-gamut region greyed (#15120f), AA/AAA contrast mask overlays the reachable colors. Per-cell gamut test is forward-only (=oklch2oklab=→=oklab2lrgb=→=inGamut=), never the binary search (that stays in =oklch2hex= for committing). colormath.js exports =oklab2lrgb=/=inGamut=/=lrgb2hex= with direct Node tests (one pins inGamut to oklch2hex's clamped flag). Bitmap cached on (hue+dims+mask+bg) so C/L drags reuse it; hue drags ride browser pointermove-to-frame coalescing (synchronous render measured ~7ms math/5600 cells — no explicit rAF defer; flagged if jank appears). HSV path untouched. Gate met: =#planetest= (crosshair at C/L; OOG cell grey; in-gamut cell colored). Screenshot-verified the plane (gamut-boundary shape, crosshair at C=0 for grey). NOTE for Craig: OKLCH_CMAX=0.4 matches the C dial domain, so much of the plane is gamut-grey at low-chroma hues — a tighter max fills more area but desyncs the crosshair scale from the dial; your eyeball call. -*** 2026-06-08 Mon @ 21:41:49 -0500 Test surface green across the feature -Final state: 17 Node unit tests (colormath.js 100% line / 93.75% branch / 100% func), six browser hash gates (=#cursortest=/=#readouttest=/=#deltatest=/=#oklchtest=/=#planetest=/=#selftest=), inline-integrity check, =node --check= on the spliced page, README updated. All green. NOTE: =node --test <dir>= directory-globbing is broken on Node v26 — use =node --test scripts/theme-studio/*.mjs=. ** TODO Manual testing and validation :verify:theme-studio: Exercised once the phases above land. *** TODO Seeded package-face defaults look right @@ -291,35 +220,6 @@ Phase 1. Palette anchors + OKLCH shade generation (reusing colormath.js), the RO Phase 2. Initial state from seed() plus seedPkgmap for the non-org packages; all-tier reseed button with a scope-named overwrite warning, resetting non-org to their APPS defaults; regenerate dupre-revised.json. Gate: #selftest PASS; default-on-open equals seed(); artifact round-trip (regenerated dupre-revised.json imports back to the same seeded state); Chrome eyeball. *** TODO Seeding-engine test surface :solo:tests: Keep #seedtest, #selftest, the default-on-open check, the dupre-revised round-trip, node --check, and Chrome validation green. -** DONE [#B] theme-studio refactor — extract app from generate.py :feature:theme-studio:refactor: -CLOSED: [2026-06-09 Tue] -Examined 2026-06-09. generate.py is 1378 lines, ~1300 of them a single triple-quoted string holding the whole app (CSS + HTML + ~1000+ lines of JS). That string is the root of every refactor here: the app logic can't be unit-tested (only =colormath.js= is, because it is the one extracted module); backslash-doubling in the string caused real bugs this session (the multi-line export strip, the =#deltatest= regex); and there is no lint, highlight, or brace-check until Chrome runs it. The rest of the directory is healthy: =colormath.js= (pure, 100/96 tested) and =build-theme.el= (13 small functions) are the model. - -Run the whole set in NO-APPROVALS mode: TDD per stage (characterization hash tests before each behavior-preserving move; node unit tests as extraction makes logic importable), commit + push at each green stage. Tooling committed at c7518d6f before starting. Order: - -DONE (2026-06-09): Stages 1-5 + 7 landed and pushed (origin/main tip dd90eca9); Stage 6 deliberately skipped (optional, works today). generate.py went 1378→~500 lines; the app now lives in real files (styles.css, app.js, app-core.js) inlined at generate time. The escaping-bug class is gone (str.replace is literal), the dedup is done (unified dropdowns/sort/clear-unlocked, shared crHtml/mkStyleButtons/effFg helpers), and the pure app logic is unit-tested (app-core.js, 18 node tests). Three new permanent gates added along the way: =#locktest=, =#sorttest=, and the app-core integrity + node suite. =make theme-studio-test= = 13 python + 43 node + spliced-check + 8 hash gates, all green. -*** 2026-06-09 Tue @ 05:01:11 -0500 Stage 1 — #locktest net + extracted styles.css/app.js -Added the =#locktest= browser gate first (commit d04f44dd): it pins, across all three tiers, that mkLockCell disables a row's control (syntax swatch div via data-locked, UI select via .disabled) and that clear-unlocked wipes unlocked rows while skipping locked ones. Proved it goes red when a lock guard is removed. - -Then extracted the =<style>= block to =styles.css= and the =<script>= body to =app.js= (commit eaf16904), inlined by =generate.py= through STYLES_CSS / APP_JS placeholders the same way =colormath.js= is. Used =ast= to pull the resolved string value so the escapes (single vs doubled backslashes) survive the move — the generated page is byte-identical to before. =generate.py= dropped 1378 → ~500 lines (the remaining bulk is the package face-data dicts; Stage 6 may data-file those). Two integrity tests guard the splice: styles.css inlines verbatim, app.js reaches the page as =fill_data= renders it; both go red if the wiring is dropped. - -Gate green: 12 python templating tests, 25 node tests, spliced-script =node --check=, all 7 hash gates. =node --check app.js= passes standalone (placeholders are valid JS identifiers). The escaping-bug class is gone — =str.replace= is literal, so the JS no longer lives inside a Python string. -*** 2026-06-09 Tue @ 05:07:10 -0500 Stage 2 — unified color dropdowns on the swatch picker -Deleted native =colorDropdown=; routed UI + package fg/bg through =mkColorDropdown= so all three tiers show real swatches (commit aee14bff). The inherit column stays a select — it picks a face name, not a color. Pulled the option-list build into a shared =ddList= helper (default + palette + "(gone)" entry), replacing the inline copy in the syntax table. Preserved value-based sort: the swatch dropdown now exposes =data-val= and =cellVal= reads it. Updated =#locktest='s UI assertion to the div lock path (data-locked). Verified via headless DOM: legbody 21 cdd / 0 select, uibody 40 cdd / 0 select, pkgbody 176 cdd + 88 inherit selects. All gates green. -*** 2026-06-09 Tue @ 05:12:00 -0500 Stage 3 — extracted crHtml + mkStyleButtons -Extracted =crHtml(r)= (the contrast→ratingColor→rating span, was copy-pasted at 5 sites; now syntax/UI/pkg cells share it — the picker readout renders differently and stays) and =mkStyleButtons(isOn,onToggle)= (the B/I/U/S loop, was near-identical in the UI + pkg tables; returns the button list for mkLockCell). Commit 62b53bc5. - -Deliberately NOT done: the syntax bold/italic buttons (2 buttons, BOLD/ITALIC dicts, in-place refresh closure — poor fit for the same helper), and a shared row scaffold (the three tables differ enough in columns/order that one would leak — premature abstraction). Node-unit-testing the pure pieces deferred to Stage 7, where app.js is made importable. - -Verified behavior-preserving by diffing the runtime-rendered DOM (Stage 2 page vs Stage 3 page in headless Chrome): the only differences are inside the inline =<script>= source, never a built tr/td/button/span — the tables build identically. All hash gates + node + python green. -*** 2026-06-09 Tue @ 05:16:33 -0500 Stage 4 — unified syntax table onto the shared sort -Deleted =srt= + =D{}= (the syntax table's own sort); pointed its headers at =srtTable('legbody',col)= so all three tables share =srtTable=/=cellVal=/=applyTableSort= (commit d947944b). Mapping is exact: the legtable color cell is a swatch dropdown whose =data-val= is the hex (what =srt= sorted on via MAP[kind]); elements cell is text; first-click stays ascending. Syntax sorts on click only — it doesn't opt into the cross-rebuild persistence the UI/pkg tables get, preserving its prior behavior. Added a =#sorttest= gate (sort was untested): syntax sorts by color asc, reverses on re-click, sorts by element name; UI + pkg still sort. asc/desc pair is self-validating. -*** 2026-06-09 Tue @ 05:20:22 -0500 Stage 5 — parameterized clear-unlocked + added effFg/effBg -Collapsed the three clear-unlocked functions into =clearUnlockedRows(items,keyFn,resetFn)= (keyFn returns a row's lock key or null to skip; resetFn does the tier-specific clear) — #locktest already guards clear-unlocked-skips-locked per tier. Replaced the 9x =||MAP['p']= / =||MAP['bg']= effective-fg/bg fallback with =effFg(v)=/=effBg(v)= across syntax/UI/pkg render paths (commit 89d079fe). Behavior-preserving: rendered DOM (script stripped) byte-identical; all gates green. Node-unit-testing the pure pieces (effFg/effBg, clearUnlockedRows) deferred to Stage 7 with the rest of the importable-app-logic suite. -*** 2026-06-09 Tue @ 06:03:04 -0500 Stage 6 — skipped (optional, deferred) -Left undone deliberately. Grouping the free module-level state into a state object is churn with no functional gain (works today), and data-filing the inline face dicts is a generate.py size win unrelated to the refactor's goal (testable logic), which Stage 7 already achieved. Can be revived from this entry + the original plan if the generate.py face dicts ever need to become data. Not blocking anything. -*** 2026-06-09 Tue @ 06:03:04 -0500 Stage 7 — extracted app-core.js + unit-tested the app logic -The coverage payoff. Pulled the pure package-face model + dropdown option list into app-core.js (nameToHex, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList — every dep a parameter, no DOM/globals), inlined like colormath.js (strip + placeholder + integrity). app.js keeps thin wrappers (pname/seedPkgmap/ddList/pkgEffFg/pkgEffBg) passing live PALETTE/APPS/PKGMAP, so no call site changed and the built DOM is byte-identical. Added test-app-core.mjs: 18 Normal/Boundary/Error tests (name resolution, seed/export/merge round trip, inherit chain incl. a cycle terminating at null, "(gone)" entry) + inline-integrity. Node suite 25→43; python +1 integrity. Commit dd90eca9. GOTCHA found+fixed pre-commit: a code comment that contained the literal token "APP_CORE_J" got inlined by str.replace too (placeholder tokens must not appear in prose that gets templated). ** TODO [#C] theme-studio terminal/ANSI colors :feature:theme-studio: theme-studio represents GUI faces only; terminal colors aren't surfaced at all. Scope decided 2026-06-09: GUI-first faces, NOT full per-face display-class fallback. Two pieces: @@ -2766,19 +2666,6 @@ They should have the same UI paradigms and patters for consistency. :END: Display slack.el message and thread buffers in a dedicated popup window (side or bottom) and reuse that one window instead of spawning a new window per buffer. Likely a =display-buffer-alist= rule (or popper integration) in =modules/slack-config.el=. -** DONE [#C] M-F9 ai-vterm close removes the window split :quick:solo: -CLOSED: [2026-06-06 Sat] -:PROPERTIES: -:LAST_REVIEWED: 2026-06-02 -:END: -Closing the ai-vterm with M-F9 while its window is in a split deletes the split too (the sibling window goes away) instead of just closing the vterm and leaving the rest of the layout intact. - -*** 2026-06-02 Tue @ 14:12:48 -0500 Audit: still a bug, distinct from the F9 collapse -The F9 toggle-off rework (38dad92) made F9 collapse the split by design, but that's the toggle path. This is M-F9 close (kills the agent process): close should leave the surrounding layout intact, not delete the sibling window. Craig confirmed it's still a bug. cj/--ai-vterm-close-buffer still calls delete-window. - -*** 2026-06-06 Sat @ 18:18:17 -0500 Fixed: close swaps the window to a non-agent buffer instead of deleting it -=cj/--ai-term-close-buffer= no longer calls =delete-window=; it swaps the agent's window to the working buffer (=cj/--ai-term-most-recent-non-agent-buffer=), then kills the agent buffer, so the split survives. F9 hide still collapses the split by design; close no longer does. Regression test =test-ai-term--close-buffer-keeps-window-split=. Commit =1a097b7e=. - ** TODO [#C] Implement EMMS-free music-config architecture :refactor: :PROPERTIES: :LAST_REVIEWED: 2026-06-01 @@ -7372,3 +7259,111 @@ Expected: F12 excludes agent buffers and keeps saved geometry; the dashboard lau What we're verifying: the aiv- tmux session survives an Emacs crash and reattaches. - with a live agent, kill Emacs (not the tmux session); restart Emacs; F9 → project picker Expected: the project shows "[detached]" and reattaches to the surviving tmux session. +** DONE [#B] Color-family grouping for hue-adjacent warm colors :feature:theme-studio:research: +CLOSED: [2026-06-10 Wed] +Resolved by two independent reviews of =~/color-sorting.org= (=~/color-sorting-codex.org=, =~/color-sorting-fable.org=, Fable's harness at =~/working/color-sorting-fable/=). Both converged on lightness-conditioned complete-linkage clustering + a floored neutral threshold; implemented in commit =04b82bbe= (replacing the hue anchors). Measured F1 0.63→0.96 on the real palette: gold and olive separate, red/blue ramps stay whole, intense-red isolates, all grays/steels consolidate, and the gray+1/gray+2/white neutral-leak bugs are fixed. The only residual (pale yellow+2 lands on the olive ramp) is geometrically irreducible from the hex — see the hint-override task below. +** DONE [#B] theme-studio comprehensive previews (org/magit/elfeed/ghostel/mu4e/dashboard) :feature:theme:theme-studio: +CLOSED: [2026-06-08 Mon] +Expanded the bespoke previews to near-complete face coverage and added three new ones. org now exercises 83/88 faces (document + agenda; the 5 skipped are non-visual: org-hide, org-indent, org-clock-overlay, org-default, org-date-selected). magit 97/98 (status buffer + blame/reflog/sequence/bisect/signature sampler rows). elfeed 13/13. New bespoke previews: ghostel 19/19 (mock terminal, 16 ANSI colors + default + fake cursor), mu4e 37/37 (curated face list, not in the generated inventory; headers list + message view + compose), dashboard 8/8. So clicking a face row flashes a real preview element for nearly every face. Originally filed as just the org preview. +** DONE [#A] theme-studio theme.json -> dupre-*.el converter :feature:theme:theme-studio: +CLOSED: [2026-06-08 Mon] +Built as scripts/theme-studio/build-theme.el (sibling to build-inventory.el), emitting a single self-contained themes/<name>-theme.el deftheme (not the palette/faces/theme trio — a theme.json carries resolved per-face hex, not dupre's semantic layer). All four tiers convert: default from assignments.bg/.p, syntax categories -> font-lock/tree-sitter faces with bold/italic sets, UI passthrough, packages with :inherit/:height/weight/slant. 20 ERT tests in tests/test-build-theme.el (Normal/Boundary/Error + an end-to-end load + a WCAG-AA assertion on the round-tripped result). One mapping limitation documented: the dec (decorator) key has no independent Emacs face (Emacs renders decorators with font-lock-type-face, which ty owns), so dec is omitted and decorators follow the type color. + +The last link in the pipeline: turn a theme.json exported by the theme-studio into a real loadable Emacs theme. Elisp (per Craig), TDD — this is the correctness-sensitive piece. + +Inputs (all on disk; no chat history needed): +- theme.json contract: =scripts/theme-studio/README.md= (theme.json section) and =docs/design/theme-studio-package-faces-spec.org= (State and export policy, Relative height, Inheritance). +- Reference face layout: existing =themes/dupre-palette.el= + =themes/dupre-faces.el= + =themes/dupre-theme.el=, and =tests/test-dupre-theme.el= (WCAG-contrast helper to reuse). +- Conventions: =.claude/rules/elisp.md=, =.claude/rules/elisp-testing.md=. + +Scope: +1. Read theme.json. Set =default= from =assignments.bg= / =assignments.p=. +2. Author the syntax category -> font-lock face map (~21 keys: kw->font-lock-keyword-face, str->font-lock-string-face, fnd->font-lock-function-name-face, fnc->font-lock-function-call-face, op->font-lock-operator-face, punc->font-lock-punctuation-face, etc. incl. the Emacs-29 tree-sitter additions). Apply =bold= / =italic= sets. +3. UI faces: the =ui= keys are already real face names (region, cursor, mode-line, ...) -> near 1:1 passthrough of fg/bg. +4. Package faces: =packages= -> each face spec, writing =:inherit PARENT= for inherited faces + only the overridden attrs, =:height= when != 1.0, weight/slant. +5. Emit a deftheme file (or palette+faces+theme trio mirroring dupre's layout). + +TDD targets: old-JSON (no packages) loads; every category maps; round-trip of fg/bg/bold/italic/inherit/height into valid face specs; WCAG-contrast assertion on the result. Decide whether the converter lives under =scripts/theme-studio/= (emits to =themes/=) or =themes/=. +** DONE [#B] theme-studio tier-3 package faces :feature:theme:theme-studio: +CLOSED: [2026-06-08 Mon] +Package-specific face editing in the theme-studio: org/magit/elfeed bespoke (complete face tables + live previews) plus a generated all-package inventory so every installed package is themeable. Spec is Ready, all opens resolved: [[file:docs/design/theme-studio-package-faces-spec.org][docs/design/theme-studio-package-faces-spec.org]]. Phases below run in dependency order; phases 1-5 deliver the three high-value apps, phase 6 opens the long tail, phase 7 documents. The =theme.json= -> =dupre-*.el= converter (Elisp) is a separate downstream task. + +*** 2026-06-08 Mon @ 00:17:41 -0500 Phase 1 — package state + schema landed +Added =APPS= (org starter) and =PKGMAP= ({app:{face:{fg,bg,bold,italic,inherit,height,source}}}), pure helpers (=seedPkgmap= / =packagesForExport= / =mergePackagesInto=), and wired export/import for the =packages= key with old-JSON compat. The =height= float (relative size, read off the face not cascaded through inherit) and the fixed-pitch inherits are seeded in the org starter. No UI yet (Phase 3). Verified: node-check, plus a guarded =#selftest= harness (headless Chrome) confirming seed->export->import round-trip, old-JSON merge, and inherit/height/source survival — all PASS. + +*** 2026-06-08 Mon @ 02:16:24 -0500 Phase 2 — curated app data (org/magit/elfeed) landed +Filled =APPS= with the complete own-defface sets built from embedded face-name lists + a curated seed-color map: org 88 (85 seeded, incl. org-agenda, heading heights, fixed-pitch inherits), magit 98 (64 seeded), elfeed 13 (all seeded). Long-tail faces seed to default fg. Verified: 199 faces total, no seed typos / no dupes, schema self-test PASS seeding all of them. Seeded-default aesthetics still go to Manual testing once the Phase 3 UI lands. + +*** 2026-06-08 Mon @ 02:23:56 -0500 Phase 3 — package face table UI landed +Added the "package faces" section: app selector (org/magit/elfeed), per-app face table with fg/bg dropdowns, bold/italic toggles, inherit dropdown (base faces + the app's own faces), relative-height stepper, live contrast readout on the effective (inherit-resolved) color, per-face and per-app reset, and a text filter. Refactored the fg/bg dropdown into a shared =colorDropdown= helper the ui-faces table now also uses (no =uiSelect= fork). Palette edits propagate to package faces; import/export carry them. Right pane is the generic preview (face names in their own resolved colors) until the bespoke org/magit/elfeed previews land (phases 4-5). Verified: node, headless screenshot, schema self-test PASS. + +*** 2026-06-08 Mon @ 02:27:51 -0500 Phase 4 — org preview landed +Added =renderOrgPreview()=: a mock org document painted live from the org package faces (title, headings with heights, TODO/DONE, tag, scheduled date, property drawer, inline code/verbatim, link, checkbox, quote, src block, header-row table). The preview pane dispatches on the app's preview key; org-mode gets this, others keep the generic list. Verified: node, headless screenshot, self-test PASS. + +*** 2026-06-08 Mon @ 02:30:42 -0500 Phase 5 — magit + elfeed previews landed +Bespoke =renderMagitPreview()= (status buffer: head/branches, untracked, a diff hunk with context/added/removed, recent commits with hashes/authors/keyword/tag) and =renderElfeedPreview()= (search list: filter, dated entries with feed/unread-title/read-title/tags, log lines by level). The preview label now names the app and notes generic vs bespoke. Verified: node, headless screenshots, self-test PASS. + +*** 2026-06-08 Mon @ 02:32:44 -0500 Phase 6 — generated all-package inventory landed +=build-inventory.el= (loaded into a running Emacs) groups every installed package's faces by the defining package and writes =package-inventory.json=. =generate.py= embeds it and merges each package into the dropdown as an editable generic app, leaving org/magit/elfeed bespoke. 40 apps now (3 bespoke + 37 inventory, 643 faces). Committed data artifact, refreshed by reloading the .el; never browser-side discovery. Verified: node, self-test PASS, app count + bespoke-preserved checks. + +*** 2026-06-08 Mon @ 02:34:01 -0500 Phase 7 — docs landed +Rewrote =README.md= for the full tool: three face tiers + palette, the in-page picker (with the AA/AAA mask), package faces (bespoke vs generic previews), modeled inheritance + relative height (family stays in font-config.el), the packages schema with inherit/height/source, export-vs-save, and the inventory-refresh command (=build-inventory.el=) + its loaded-config dependency. Notes =theme-studio.html= is generated. Test-surface fixtures tracked separately below. + +*** 2026-06-08 Mon @ 02:40:00 -0500 theme-studio tier 3 — test surface landed +Extended the guarded =#selftest= harness (headless Chrome) to assert the acceptance criteria against the real emitted code: old-JSON import (no =packages=), full round-trip (fg/bg/bold/italic/inherit/height/source), cleared-state export, unknown-package/face preservation, and inheritance-cycle termination — all PASS. The two DOM-coupled regressions are handled structurally: =updateColor= remaps =PKGMAP= on a palette-color edit, and =PKGMAP= stores hexes so a deleted palette color leaves package refs in the "(gone)" recoverable state. =generate.py= rebuilds =theme-studio.html= each run. +** DONE [#B] theme-studio perceptual color metrics :feature:theme:theme-studio: +CLOSED: [2026-06-08 Mon] +Spec (Ready, opens confirmed 2026-06-08): [[file:docs/design/theme-studio-perceptual-color-metrics-spec.org][docs/design/theme-studio-perceptual-color-metrics-spec.org]]. OKLCH model + perceptual-L/APCA readouts + pairwise ΔE, for building low-contrast themes by metric rather than by eye. All five phases shipped 2026-06-08 (commits 49342bf5, 78260018, 77c7f126, 163d3730, 22605426, 582d8a6a): colormath.js core inlined + WCAG/HSV helpers migrated; picker OKLCH/APCA readouts; palette ΔE warnings; OKLCH edit-model dials; C×L gamut plane. 17 Node tests (colormath 100/93.75/100), six browser hash gates green, inline-integrity guard. vNext deferrals (low-contrast preset, CIEDE2000) remain the two [#D] tasks below. Manual eyeballs tracked under Manual testing. +*** 2026-06-08 Mon @ 19:43:50 -0500 Color-math foundation + Node tests landed +Pure color core in =scripts/theme-studio/colormath.js= (OKLab/OKLCH, APCA-W3 0.1.9 exact constants, ΔE-OK, binary-search gamut clamp returning ={hex,clamped}=) shipped in 49342bf5; this phase finished the integration in 78260018. =generate.py= now inlines the colormath.js body into the page script (export-stripped, =COLORMATH_J= placeholder), and the page's lin/rl/contrast/rating/hsv2rgb/rgb2hsv/hex2rgb/rgb2hex copies moved into the module — =rl= reuses the canonical =lin= (0.04045 cutoff), byte-identical to the old 0.03928 form on every #rrggbb (no 8-bit channel falls between the cutoffs; verified over 200k pairs, zero contrast change). =test-colormath.mjs= gained Normal/Boundary/Error cases for the migrated helpers, a seeded hsv-rgb round-trip property test, and an inline-integrity check that the generated page carries the module body verbatim. Gate met: =node --test scripts/theme-studio/*.mjs= 15 pass, colormath.js 100% line / 93.75% branch / 100% func; =node --check= on the spliced script clean; =#selftest= + =#cursortest= PASS in headless Chrome. NOTE: =node --test <dir>= directory-globbing is broken on Node v26 (tries to load the dir as a module) — use the =*.mjs= glob form. +*** 2026-06-08 Mon @ 19:55:53 -0500 Picker OKLCH/APCA readouts landed +Phase 2 shipped in 77c7f126. Second readout row (=.pinfo2=) under the WCAG ratio: OKLCH L/C/H + signed APCA Lc against the ground color, always shown; sign convention in the APCA tooltip + README. Tables unchanged (APCA picker-only per Agreed-decision #3). =pkReadout= drives the spans from the inlined colormath functions. Gate met: =#readouttest= asserts the spans match the live computation AND the known dupre-blue OKLCH reference (L 0.591 / C 0.052 / H 252°, APCA Lc -34 on ground) with WCAG unchanged; =#selftest= + =#cursortest= still PASS; 15 Node tests green. Headless-rendered values verified against a node cross-check. Visual eyeball is the open "Perceptual readouts read well in the picker" item under Manual testing. +*** 2026-06-08 Mon @ 20:44:39 -0500 Palette ΔE warnings landed +Phase 3 shipped in 163d3730. =renderPalette= runs a pairwise OKLab ΔE over PALETTE via the pure =paletteDeltas()= (one pass → sub-threshold pairs + per-color nearest distance); warns on pairs below the named =DELTAE_MIN= (0.02), sorted closest-first, capped at 5 with "and N more"; each chip's tooltip gains its nearest-neighbor ΔE. Names go through =esc= before the warning markup. Gate met: =#deltatest= PASS (near pair fires + names itself; spread palette quiet; 7-color cluster caps at 5 ascending + overflow suffix). #readouttest/#selftest/#cursortest + 15 Node tests still green. Screenshot-verified the warning render (terracotta "too-similar colors" header + "blue / blue2 — ΔE 0.007, hard to distinguish", placed between palette and add-color controls). Pushed below. +*** 2026-06-08 Mon @ 21:05:28 -0500 OKLCH sliders + color-model control landed +Phase 4a shipped in 22605426. Picker gains an edit-model toggle (HSV/OKLCH) in its own =pkModel= state, orthogonal to =pkMode= (AA/AAA mask) — separate handlers, distinct toggle colors (blue vs gold). OKLCH mode shows L/C/H as paired range+number inputs driving =oklch2hex= → hex/swatch/readouts/HSV-cursor; out-of-gamut chroma snaps the dials to the reachable color + shows "chroma clamped to sRGB". HSV stays default; SV square still edits HSV (C×L plane is 4b); SV drag in OKLCH mode refreshes the dials. =openPicker= re-asserts the model via =setPkModel= so the toggle highlight can't drift (caught on screenshot). Gate met: =#oklchtest= PASS (color preserved on model switch; mask toggle leaves pkModel; model switch leaves pkMode; dials drive color to a known OKLCH target; out-of-gamut C raises clamp status). All 5 browser gates + 15 Node tests green; screenshot-verified the dials + toggle highlight. +*** 2026-06-08 Mon @ 21:41:49 -0500 Chroma×Lightness plane landed +Phase 4b shipped in 582d8a6a. OKLCH mode renders the SV square as a C(x)×L(y) plane at the current hue; crosshair maps to (C,L), hue strip selects H. Out-of-gamut region greyed (#15120f), AA/AAA contrast mask overlays the reachable colors. Per-cell gamut test is forward-only (=oklch2oklab=→=oklab2lrgb=→=inGamut=), never the binary search (that stays in =oklch2hex= for committing). colormath.js exports =oklab2lrgb=/=inGamut=/=lrgb2hex= with direct Node tests (one pins inGamut to oklch2hex's clamped flag). Bitmap cached on (hue+dims+mask+bg) so C/L drags reuse it; hue drags ride browser pointermove-to-frame coalescing (synchronous render measured ~7ms math/5600 cells — no explicit rAF defer; flagged if jank appears). HSV path untouched. Gate met: =#planetest= (crosshair at C/L; OOG cell grey; in-gamut cell colored). Screenshot-verified the plane (gamut-boundary shape, crosshair at C=0 for grey). NOTE for Craig: OKLCH_CMAX=0.4 matches the C dial domain, so much of the plane is gamut-grey at low-chroma hues — a tighter max fills more area but desyncs the crosshair scale from the dial; your eyeball call. +*** 2026-06-08 Mon @ 21:41:49 -0500 Test surface green across the feature +Final state: 17 Node unit tests (colormath.js 100% line / 93.75% branch / 100% func), six browser hash gates (=#cursortest=/=#readouttest=/=#deltatest=/=#oklchtest=/=#planetest=/=#selftest=), inline-integrity check, =node --check= on the spliced page, README updated. All green. NOTE: =node --test <dir>= directory-globbing is broken on Node v26 — use =node --test scripts/theme-studio/*.mjs=. +** DONE [#B] theme-studio refactor — extract app from generate.py :feature:theme-studio:refactor: +CLOSED: [2026-06-09 Tue] +Examined 2026-06-09. generate.py is 1378 lines, ~1300 of them a single triple-quoted string holding the whole app (CSS + HTML + ~1000+ lines of JS). That string is the root of every refactor here: the app logic can't be unit-tested (only =colormath.js= is, because it is the one extracted module); backslash-doubling in the string caused real bugs this session (the multi-line export strip, the =#deltatest= regex); and there is no lint, highlight, or brace-check until Chrome runs it. The rest of the directory is healthy: =colormath.js= (pure, 100/96 tested) and =build-theme.el= (13 small functions) are the model. + +Run the whole set in NO-APPROVALS mode: TDD per stage (characterization hash tests before each behavior-preserving move; node unit tests as extraction makes logic importable), commit + push at each green stage. Tooling committed at c7518d6f before starting. Order: + +DONE (2026-06-09): Stages 1-5 + 7 landed and pushed (origin/main tip dd90eca9); Stage 6 deliberately skipped (optional, works today). generate.py went 1378→~500 lines; the app now lives in real files (styles.css, app.js, app-core.js) inlined at generate time. The escaping-bug class is gone (str.replace is literal), the dedup is done (unified dropdowns/sort/clear-unlocked, shared crHtml/mkStyleButtons/effFg helpers), and the pure app logic is unit-tested (app-core.js, 18 node tests). Three new permanent gates added along the way: =#locktest=, =#sorttest=, and the app-core integrity + node suite. =make theme-studio-test= = 13 python + 43 node + spliced-check + 8 hash gates, all green. +*** 2026-06-09 Tue @ 05:01:11 -0500 Stage 1 — #locktest net + extracted styles.css/app.js +Added the =#locktest= browser gate first (commit d04f44dd): it pins, across all three tiers, that mkLockCell disables a row's control (syntax swatch div via data-locked, UI select via .disabled) and that clear-unlocked wipes unlocked rows while skipping locked ones. Proved it goes red when a lock guard is removed. + +Then extracted the =<style>= block to =styles.css= and the =<script>= body to =app.js= (commit eaf16904), inlined by =generate.py= through STYLES_CSS / APP_JS placeholders the same way =colormath.js= is. Used =ast= to pull the resolved string value so the escapes (single vs doubled backslashes) survive the move — the generated page is byte-identical to before. =generate.py= dropped 1378 → ~500 lines (the remaining bulk is the package face-data dicts; Stage 6 may data-file those). Two integrity tests guard the splice: styles.css inlines verbatim, app.js reaches the page as =fill_data= renders it; both go red if the wiring is dropped. + +Gate green: 12 python templating tests, 25 node tests, spliced-script =node --check=, all 7 hash gates. =node --check app.js= passes standalone (placeholders are valid JS identifiers). The escaping-bug class is gone — =str.replace= is literal, so the JS no longer lives inside a Python string. +*** 2026-06-09 Tue @ 05:07:10 -0500 Stage 2 — unified color dropdowns on the swatch picker +Deleted native =colorDropdown=; routed UI + package fg/bg through =mkColorDropdown= so all three tiers show real swatches (commit aee14bff). The inherit column stays a select — it picks a face name, not a color. Pulled the option-list build into a shared =ddList= helper (default + palette + "(gone)" entry), replacing the inline copy in the syntax table. Preserved value-based sort: the swatch dropdown now exposes =data-val= and =cellVal= reads it. Updated =#locktest='s UI assertion to the div lock path (data-locked). Verified via headless DOM: legbody 21 cdd / 0 select, uibody 40 cdd / 0 select, pkgbody 176 cdd + 88 inherit selects. All gates green. +*** 2026-06-09 Tue @ 05:12:00 -0500 Stage 3 — extracted crHtml + mkStyleButtons +Extracted =crHtml(r)= (the contrast→ratingColor→rating span, was copy-pasted at 5 sites; now syntax/UI/pkg cells share it — the picker readout renders differently and stays) and =mkStyleButtons(isOn,onToggle)= (the B/I/U/S loop, was near-identical in the UI + pkg tables; returns the button list for mkLockCell). Commit 62b53bc5. + +Deliberately NOT done: the syntax bold/italic buttons (2 buttons, BOLD/ITALIC dicts, in-place refresh closure — poor fit for the same helper), and a shared row scaffold (the three tables differ enough in columns/order that one would leak — premature abstraction). Node-unit-testing the pure pieces deferred to Stage 7, where app.js is made importable. + +Verified behavior-preserving by diffing the runtime-rendered DOM (Stage 2 page vs Stage 3 page in headless Chrome): the only differences are inside the inline =<script>= source, never a built tr/td/button/span — the tables build identically. All hash gates + node + python green. +*** 2026-06-09 Tue @ 05:16:33 -0500 Stage 4 — unified syntax table onto the shared sort +Deleted =srt= + =D{}= (the syntax table's own sort); pointed its headers at =srtTable('legbody',col)= so all three tables share =srtTable=/=cellVal=/=applyTableSort= (commit d947944b). Mapping is exact: the legtable color cell is a swatch dropdown whose =data-val= is the hex (what =srt= sorted on via MAP[kind]); elements cell is text; first-click stays ascending. Syntax sorts on click only — it doesn't opt into the cross-rebuild persistence the UI/pkg tables get, preserving its prior behavior. Added a =#sorttest= gate (sort was untested): syntax sorts by color asc, reverses on re-click, sorts by element name; UI + pkg still sort. asc/desc pair is self-validating. +*** 2026-06-09 Tue @ 05:20:22 -0500 Stage 5 — parameterized clear-unlocked + added effFg/effBg +Collapsed the three clear-unlocked functions into =clearUnlockedRows(items,keyFn,resetFn)= (keyFn returns a row's lock key or null to skip; resetFn does the tier-specific clear) — #locktest already guards clear-unlocked-skips-locked per tier. Replaced the 9x =||MAP['p']= / =||MAP['bg']= effective-fg/bg fallback with =effFg(v)=/=effBg(v)= across syntax/UI/pkg render paths (commit 89d079fe). Behavior-preserving: rendered DOM (script stripped) byte-identical; all gates green. Node-unit-testing the pure pieces (effFg/effBg, clearUnlockedRows) deferred to Stage 7 with the rest of the importable-app-logic suite. +*** 2026-06-09 Tue @ 06:03:04 -0500 Stage 6 — skipped (optional, deferred) +Left undone deliberately. Grouping the free module-level state into a state object is churn with no functional gain (works today), and data-filing the inline face dicts is a generate.py size win unrelated to the refactor's goal (testable logic), which Stage 7 already achieved. Can be revived from this entry + the original plan if the generate.py face dicts ever need to become data. Not blocking anything. +*** 2026-06-09 Tue @ 06:03:04 -0500 Stage 7 — extracted app-core.js + unit-tested the app logic +The coverage payoff. Pulled the pure package-face model + dropdown option list into app-core.js (nameToHex, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList — every dep a parameter, no DOM/globals), inlined like colormath.js (strip + placeholder + integrity). app.js keeps thin wrappers (pname/seedPkgmap/ddList/pkgEffFg/pkgEffBg) passing live PALETTE/APPS/PKGMAP, so no call site changed and the built DOM is byte-identical. Added test-app-core.mjs: 18 Normal/Boundary/Error tests (name resolution, seed/export/merge round trip, inherit chain incl. a cycle terminating at null, "(gone)" entry) + inline-integrity. Node suite 25→43; python +1 integrity. Commit dd90eca9. GOTCHA found+fixed pre-commit: a code comment that contained the literal token "APP_CORE_J" got inlined by str.replace too (placeholder tokens must not appear in prose that gets templated). +** DONE [#C] M-F9 ai-vterm close removes the window split :quick:solo: +CLOSED: [2026-06-06 Sat] +:PROPERTIES: +:LAST_REVIEWED: 2026-06-02 +:END: +Closing the ai-vterm with M-F9 while its window is in a split deletes the split too (the sibling window goes away) instead of just closing the vterm and leaving the rest of the layout intact. + +*** 2026-06-02 Tue @ 14:12:48 -0500 Audit: still a bug, distinct from the F9 collapse +The F9 toggle-off rework (38dad92) made F9 collapse the split by design, but that's the toggle path. This is M-F9 close (kills the agent process): close should leave the surrounding layout intact, not delete the sibling window. Craig confirmed it's still a bug. cj/--ai-vterm-close-buffer still calls delete-window. + +*** 2026-06-06 Sat @ 18:18:17 -0500 Fixed: close swaps the window to a non-agent buffer instead of deleting it +=cj/--ai-term-close-buffer= no longer calls =delete-window=; it swaps the agent's window to the working buffer (=cj/--ai-term-most-recent-non-agent-buffer=), then kills the agent buffer, so the split survives. F9 hide still collapses the split by design; close no longer does. Regression test =test-ai-term--close-buffer-keeps-window-split=. Commit =1a097b7e=. |
