transitions-refine 0.3.15 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/demo.html +211 -44
- package/package.json +1 -1
- package/server/relay.mjs +52 -3
package/README.md
CHANGED
|
@@ -98,6 +98,8 @@ Like Replace, Accept needs the agent — run `/refine live` (or `--llm` / `REFIN
|
|
|
98
98
|
| `REFINE_RELAY_PORT` | `7331` | relay port |
|
|
99
99
|
| `REFINE_AGENT_CMD` | — | one-shot LLM CLI the relay spawns per job |
|
|
100
100
|
| `REFINE_AGENT_TIMEOUT_MS` | `120000` | per-run timeout |
|
|
101
|
+
| `REFINE_SCAN_MODEL` | `composer-2.5-fast` | cursor-agent model for the initial group scan (`""` = agent default) |
|
|
102
|
+
| `REFINE_SCAN_EFFORT` | `low` | Codex reasoning effort for the scan — `minimal\|low\|medium\|high` (`""` = configured effort) |
|
|
101
103
|
| `REFINE_AUTO=0` | — | disable auto-answer and wait for an external poller |
|
|
102
104
|
| `window.REFINE_RELAY_URL` | injected origin | browser override for the relay URL |
|
|
103
105
|
|
package/demo.html
CHANGED
|
@@ -1437,32 +1437,49 @@
|
|
|
1437
1437
|
.tl-scan-note .tl-dotm { width: 14px; height: 14px; }
|
|
1438
1438
|
.tl-scan-error { color: var(--c-warn, #c2410c); }
|
|
1439
1439
|
.tl-scan-error svg { flex: none; }
|
|
1440
|
-
/* /refine live chip —
|
|
1440
|
+
/* /refine live chip — Figma icons 594:22938. Live = green chip + green Stop
|
|
1441
|
+
pill; Live Off = gray chip, no button. Tooltip warns the chat-loop mode
|
|
1442
|
+
bills credits while idle. */
|
|
1441
1443
|
.tl-live-wrap { display: inline-flex; }
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1444
|
+
/* LIVE (active) — node 594:22828 */
|
|
1445
|
+
.tl-live-chip { box-sizing: border-box; display: inline-flex; align-items: center;
|
|
1446
|
+
justify-content: space-between; width: 94px; height: 28px; padding: 0 2px 0 9px;
|
|
1447
|
+
border-radius: 36px; cursor: help;
|
|
1448
|
+
background: rgba(132,211,125,0.1);
|
|
1449
|
+
box-shadow: 0 1px 3px 0 rgba(0,220,37,0.06),
|
|
1450
|
+
inset 0 0 0 1px rgba(48,124,21,0.06), inset 0 -1px 0 0 rgba(0,85,9,0.08);
|
|
1447
1451
|
/* mount: token dropdown-open feel — 250ms Fast, scale 0.97, smooth ease out */
|
|
1448
1452
|
animation: tl-live-in 250ms cubic-bezier(0.22,1,0.36,1) both; }
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
.tl-live-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
.tl-live-dot { width: 7px; height: 7px; border-radius: 50%;
|
|
1455
|
-
background: currentColor; flex: none;
|
|
1453
|
+
.tl-live-main { display: inline-flex; align-items: center; gap: 5px; flex: none; }
|
|
1454
|
+
.tl-live-label { font-size: 12px; font-weight: 400; line-height: 14px; color: #18a32f; white-space: nowrap; }
|
|
1455
|
+
html[data-theme="dark"] .tl-live-label { color: #4ade80; }
|
|
1456
|
+
.tl-live-dot { width: 8px; height: 8px; border-radius: 50%; flex: none;
|
|
1457
|
+
background: #34CC20; box-shadow: inset 0 0 0 0.5px rgba(0,182,30,0.55);
|
|
1456
1458
|
animation: tl-live-pulse 1600ms linear infinite; }
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1459
|
+
/* Stop pill — node 594:22874 (default / hover+active = Variant2/3) */
|
|
1460
|
+
.tl-live-stop { all: unset; box-sizing: border-box; cursor: pointer; flex: none;
|
|
1461
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
1462
|
+
height: 24px; padding: 0 8px; border-radius: 36px;
|
|
1463
|
+
font-size: 12px; font-weight: 500; line-height: 14px; color: #118c25;
|
|
1464
|
+
background: rgba(68,167,84,0.1);
|
|
1465
|
+
box-shadow: inset 0 0 0 1px rgba(18,130,28,0.04);
|
|
1466
|
+
transition: background 150ms cubic-bezier(0.22,1,0.36,1),
|
|
1467
|
+
color 150ms cubic-bezier(0.22,1,0.36,1); }
|
|
1468
|
+
.tl-live-stop:hover,
|
|
1469
|
+
.tl-live-stop:active { background: rgba(68,167,84,0.2); color: #057217; }
|
|
1470
|
+
html[data-theme="dark"] .tl-live-stop { color: #4ade80; }
|
|
1471
|
+
html[data-theme="dark"] .tl-live-stop:hover,
|
|
1472
|
+
html[data-theme="dark"] .tl-live-stop:active { color: #86efac; }
|
|
1473
|
+
.tl-live-stop:focus-visible { outline: 2px solid #118c25; outline-offset: 1px; }
|
|
1474
|
+
/* LIVE OFF — node 594:22912 (gray, no button) */
|
|
1475
|
+
.tl-live-chip.is-stopped { width: auto; justify-content: flex-start; padding: 0 9px;
|
|
1476
|
+
background: rgba(102,102,102,0.04);
|
|
1477
|
+
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.02),
|
|
1478
|
+
inset 0 0 0 1px rgba(77,77,77,0.1), inset 0 -1px 0 0 rgba(0,0,0,0.04); }
|
|
1479
|
+
.tl-live-chip.is-stopped .tl-live-dot { background: #A1A1A1;
|
|
1480
|
+
box-shadow: inset 0 0 0 0.5px rgba(146,146,146,0.6); animation: none; }
|
|
1481
|
+
.tl-live-chip.is-stopped .tl-live-label { color: #525252; }
|
|
1482
|
+
html[data-theme="dark"] .tl-live-chip.is-stopped .tl-live-label { color: #a1a1a1; }
|
|
1466
1483
|
@keyframes tl-live-in { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
|
|
1467
1484
|
@keyframes tl-live-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
1468
1485
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1782,6 +1799,63 @@
|
|
|
1782
1799
|
return out;
|
|
1783
1800
|
}
|
|
1784
1801
|
|
|
1802
|
+
// Resolve a grouped phase MEMBER selector to live element(s), SCOPED to the
|
|
1803
|
+
// component instance(s) named by the phase's `stateTarget`. Agent-produced
|
|
1804
|
+
// member selectors are often broad (e.g. `.panel`, a utility class) and, when
|
|
1805
|
+
// queried globally, match elements in OTHER components — so editing one
|
|
1806
|
+
// group's timing leaked onto unrelated transitions, and claiming hid the
|
|
1807
|
+
// wrong flat entries. Restricting the query to each stateTarget subtree keeps
|
|
1808
|
+
// the match inside the intended component (all instances of it, which is
|
|
1809
|
+
// correct) without touching look-alikes elsewhere. Falls back to a global
|
|
1810
|
+
// query only when scoping yields nothing (no/again-unresolvable stateTarget),
|
|
1811
|
+
// so this can only ever NARROW matches — never regress to "applies nothing".
|
|
1812
|
+
function txMemberEls(memberSel, stateTargetSel){
|
|
1813
|
+
if(!memberSel) return [];
|
|
1814
|
+
const out=[];
|
|
1815
|
+
if(stateTargetSel){
|
|
1816
|
+
let roots=[];try{roots=Array.from(document.querySelectorAll(stateTargetSel));}catch{}
|
|
1817
|
+
for(const root of roots){
|
|
1818
|
+
try{ if(root.matches&&root.matches(memberSel)) out.push(root); }catch{}
|
|
1819
|
+
try{ for(const el of root.querySelectorAll(memberSel)) out.push(el); }catch{}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const scoped=Array.from(new Set(out));
|
|
1823
|
+
if(scoped.length) return scoped;
|
|
1824
|
+
let all=[];try{all=Array.from(document.querySelectorAll(memberSel));}catch{}
|
|
1825
|
+
return all;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Normalize a phase state token (the class/attribute toggled on `stateTarget`)
|
|
1829
|
+
// into a CSS selector fragment that can be concatenated onto a selector:
|
|
1830
|
+
// ".is-open" → ".is-open" (class, already a fragment)
|
|
1831
|
+
// "[data-open=\"true\"]"→ "[data-open=\"true\"]" (attribute, already a fragment)
|
|
1832
|
+
// "is-open" → ".is-open" (bare class name)
|
|
1833
|
+
// "data-open" → "[data-open]" (bare attribute name)
|
|
1834
|
+
// null / "" / "base" → "" (the no-class base state)
|
|
1835
|
+
function stateSuffix(tok){
|
|
1836
|
+
if(tok==null) return "";
|
|
1837
|
+
tok=String(tok).trim();
|
|
1838
|
+
if(!tok||/^base$/i.test(tok)) return "";
|
|
1839
|
+
// already a selector fragment: .class, [attr], :pseudo (e.g. :hover)
|
|
1840
|
+
if(tok[0]==="."||tok[0]==="["||tok[0]===":") return tok;
|
|
1841
|
+
if(/^[\w-]+$/.test(tok)) return "."+tok;
|
|
1842
|
+
return "["+tok+"]";
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Tear down a state-gated phase live-preview store: stop observers/listeners
|
|
1846
|
+
// and restore every element's original inline transition + custom props.
|
|
1847
|
+
function txTeardownPhase(store){
|
|
1848
|
+
if(!store) return;
|
|
1849
|
+
if(store) store.pending=false;
|
|
1850
|
+
for(const mo of store.observers||[]){try{mo.disconnect();}catch{}}
|
|
1851
|
+
if(store.ptr){try{document.removeEventListener("pointerover",store.ptr,true);document.removeEventListener("pointerout",store.ptr,true);}catch{}}
|
|
1852
|
+
for(const [el,rec] of store.applied||new Map()){
|
|
1853
|
+
try{el.style.transition=rec.prevInline;}catch{}
|
|
1854
|
+
for(const n of Object.keys(rec.prevVars||{})){try{const pv=rec.prevVars[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}
|
|
1855
|
+
}
|
|
1856
|
+
store.applied&&store.applied.clear();
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1785
1859
|
// ── registry (per-lane overrides) ──
|
|
1786
1860
|
// A "lane" is one animated property of one element. For flat DOM transitions
|
|
1787
1861
|
// a lane's id is just the property; for agent-grouped phases it is
|
|
@@ -1819,8 +1893,7 @@
|
|
|
1819
1893
|
const memberEls=new Set();
|
|
1820
1894
|
for(const g of this.groups)for(const ph of (g.phases||[]))for(const m of (ph.members||[])){
|
|
1821
1895
|
if(!m.selector)continue;
|
|
1822
|
-
|
|
1823
|
-
for(const el of els)memberEls.add(el);
|
|
1896
|
+
for(const el of txMemberEls(m.selector,ph.stateTarget))memberEls.add(el);
|
|
1824
1897
|
}
|
|
1825
1898
|
if(!memberEls.size)return;
|
|
1826
1899
|
for(const[id,entry]of this.entries){
|
|
@@ -2906,8 +2979,9 @@
|
|
|
2906
2979
|
chatLoop
|
|
2907
2980
|
? h("span",{className:"t-tt-wrap tl-live-wrap"},
|
|
2908
2981
|
h("span",{className:"tl-live-chip",tabIndex:0},
|
|
2909
|
-
h("span",{className:"tl-live-
|
|
2910
|
-
|
|
2982
|
+
h("span",{className:"tl-live-main"},
|
|
2983
|
+
h("span",{className:"tl-live-dot"}),
|
|
2984
|
+
h("span",{className:"tl-live-label"},"Live")),
|
|
2911
2985
|
h("button",{className:"tl-live-stop",title:"Stop /refine live",
|
|
2912
2986
|
onClick:(e)=>{e.stopPropagation();onStopLive&&onStopLive();}},"Stop")),
|
|
2913
2987
|
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
@@ -2915,8 +2989,9 @@
|
|
|
2915
2989
|
: liveStopped
|
|
2916
2990
|
? h("span",{className:"t-tt-wrap tl-live-wrap"},
|
|
2917
2991
|
h("span",{className:"tl-live-chip is-stopped",tabIndex:0},
|
|
2918
|
-
h(
|
|
2919
|
-
|
|
2992
|
+
h("span",{className:"tl-live-main"},
|
|
2993
|
+
h("span",{className:"tl-live-dot"}),
|
|
2994
|
+
h("span",{className:"tl-live-label"},"Live Off"))),
|
|
2920
2995
|
h("span",{className:"t-tt tl-tt-below tl-tt-msg",role:"tooltip"},
|
|
2921
2996
|
"The `/refine live` loop was asked to stop and will exit on its next poll. Run `/refine live` again to resume."))
|
|
2922
2997
|
: null,
|
|
@@ -3936,8 +4011,14 @@
|
|
|
3936
4011
|
// ── live styling ──────────────────────────────────────────────────────
|
|
3937
4012
|
// With playback gone, the real component IS the preview. So whenever a
|
|
3938
4013
|
// transition has edits, mirror its effective timings onto the live
|
|
3939
|
-
// element(s)
|
|
3940
|
-
//
|
|
4014
|
+
// element(s) and revert when edits are cleared. Interacting with the
|
|
4015
|
+
// component then animates with the edited values.
|
|
4016
|
+
//
|
|
4017
|
+
// FLAT entries (single-state DOM transitions) are mirrored as an inline
|
|
4018
|
+
// `transition` on their exact bound elements — precise and simple. GROUPED
|
|
4019
|
+
// phases are NOT handled here: open and close share the same base element,
|
|
4020
|
+
// so a single inline slot can't tell them apart (editing open would also
|
|
4021
|
+
// change close). Phases use a state-scoped <style> instead (effect below).
|
|
3941
4022
|
const liveAppliedRef=useRef(new Map()); // el → {prevInline, css}
|
|
3942
4023
|
useEffect(()=>{
|
|
3943
4024
|
if(typeof document==="undefined")return;
|
|
@@ -3950,20 +4031,13 @@
|
|
|
3950
4031
|
if(l.blur!=null)v[l.varName]=l.blur+"px";}return v;};
|
|
3951
4032
|
const add=(els,css,vars)=>{ for(const el of els){ if(!el||(el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]")))continue; desired.set(el,{css,vars:vars||{}}); } };
|
|
3952
4033
|
for(const item of entries){
|
|
4034
|
+
if(item.kind==="phase")continue; // phases handled by the state-scoped effect
|
|
3953
4035
|
const ov=registry.getPropOverrides(item.id);
|
|
3954
4036
|
if(!ov||!Object.keys(ov).length)continue; // only edited transitions go live
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
add(els,transitionLanesToCss(m.lanes),lanesToVars(m.lanes));
|
|
3960
|
-
}
|
|
3961
|
-
}else{
|
|
3962
|
-
const lanes=item.effectiveTimings||[];
|
|
3963
|
-
if(!lanes.length)continue;
|
|
3964
|
-
const els=((item.bindings&&item.bindings.elements)||[]).map(w=>w.deref&&w.deref()).filter(Boolean);
|
|
3965
|
-
add(els,transitionLanesToCss(lanes),lanesToVars(lanes));
|
|
3966
|
-
}
|
|
4037
|
+
const lanes=item.effectiveTimings||[];
|
|
4038
|
+
if(!lanes.length)continue;
|
|
4039
|
+
const els=((item.bindings&&item.bindings.elements)||[]).map(w=>w.deref&&w.deref()).filter(Boolean);
|
|
4040
|
+
add(els,transitionLanesToCss(lanes),lanesToVars(lanes));
|
|
3967
4041
|
}
|
|
3968
4042
|
const varsKey=v=>Object.keys(v).sort().map(k=>k+":"+v[k]).join(";");
|
|
3969
4043
|
// apply new / changed
|
|
@@ -4000,6 +4074,97 @@
|
|
|
4000
4074
|
for(const [el,rec] of applied){ try{el.style.transition=rec.prevInline;}catch{} for(const n of Object.keys(rec.vars||{})){try{const pv=(rec.prevVars||{})[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}} _TX_LIVE_BASE.delete(el); }
|
|
4001
4075
|
applied.clear();
|
|
4002
4076
|
},[]);
|
|
4077
|
+
// ── live styling: grouped phases (state-gated) ──────────────────────────
|
|
4078
|
+
// Open and close of one component share the SAME element(s), so a single
|
|
4079
|
+
// inline `transition` can't separate them (editing open would bend close
|
|
4080
|
+
// too). And a member often lives OUTSIDE its stateTarget (a caret in the
|
|
4081
|
+
// trigger, a backdrop portal'd to <body>), so a CSS descendant rule can't
|
|
4082
|
+
// reach it. So we GATE on state instead: watch each phase's `stateTarget`
|
|
4083
|
+
// for its destination state and apply the member's edited timing inline
|
|
4084
|
+
// ONLY while that state is active, reverting otherwise:
|
|
4085
|
+
// • OPEN (base→toState): apply while stateTarget IS in toState.
|
|
4086
|
+
// • CLOSE (fromState→base): apply while stateTarget is NOT in fromState.
|
|
4087
|
+
// This works regardless of where the member sits in the DOM and keeps the
|
|
4088
|
+
// two directions independent. A MutationObserver on each stateTarget (class
|
|
4089
|
+
// + state data-attrs, never `style`, so no feedback) re-runs the gate;
|
|
4090
|
+
// hover phases also watch pointer events. Everything reverts on teardown.
|
|
4091
|
+
const livePhaseRef=useRef(null);
|
|
4092
|
+
useEffect(()=>{
|
|
4093
|
+
if(typeof document==="undefined")return;
|
|
4094
|
+
if(livePhaseRef.current) txTeardownPhase(livePhaseRef.current);
|
|
4095
|
+
const store={observers:[],ptr:null,applied:new Map(),pending:false};
|
|
4096
|
+
livePhaseRef.current=store;
|
|
4097
|
+
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){if(!l.varName)continue;
|
|
4098
|
+
if(l.scale!=null)v[l.varName]=String(l.scale);
|
|
4099
|
+
if(l.blur!=null)v[l.varName]=l.blur+"px";}return v;};
|
|
4100
|
+
const inUI=el=>el&&el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]");
|
|
4101
|
+
const safeMatches=(el,sel)=>{ if(!sel)return false; try{return el.matches(sel);}catch{return false;} };
|
|
4102
|
+
const fresh=sel=>{ if(!sel)return []; try{return Array.from(document.querySelectorAll(sel)).filter(e=>!inUI(e));}catch{return [];} };
|
|
4103
|
+
// Specs hold SELECTORS, not resolved elements: the stateTarget panel and
|
|
4104
|
+
// members frequently unmount/remount on toggle, so we re-resolve them on
|
|
4105
|
+
// every recompute to stay correct across that churn.
|
|
4106
|
+
const specs=[]; const attrNames=new Set(["class"]); let anyHover=false;
|
|
4107
|
+
for(const item of entries){
|
|
4108
|
+
if(item.kind!=="phase")continue;
|
|
4109
|
+
const ov=registry.getPropOverrides(item.id);
|
|
4110
|
+
if(!ov||!Object.keys(ov).length)continue; // only edited phases go live
|
|
4111
|
+
const toSuf=stateSuffix(item.toState), fromSuf=stateSuffix(item.fromState);
|
|
4112
|
+
// collect attribute names referenced by the state tokens so the observer
|
|
4113
|
+
// fires on e.g. data-open / aria-expanded toggles, not just class.
|
|
4114
|
+
for(const suf of [toSuf,fromSuf]){const am=suf&&suf.match(/^\[([\w-]+)/);if(am)attrNames.add(am[1]);}
|
|
4115
|
+
if(/:hover/.test(toSuf)||/:hover/.test(fromSuf))anyHover=true;
|
|
4116
|
+
const members=[];
|
|
4117
|
+
for(const m of (item.members||[])){
|
|
4118
|
+
if(!m.selector||!m.lanes||!m.lanes.length)continue;
|
|
4119
|
+
members.push({selector:m.selector,css:transitionLanesToCss(m.lanes),vars:lanesToVars(m.lanes)});
|
|
4120
|
+
}
|
|
4121
|
+
if(!members.length)continue;
|
|
4122
|
+
specs.push({stateTarget:item.stateTarget||null,toSuf,fromSuf,members});
|
|
4123
|
+
}
|
|
4124
|
+
if(!specs.length) return ()=>txTeardownPhase(store);
|
|
4125
|
+
const varsKey=v=>Object.keys(v).sort().map(k=>k+":"+v[k]).join(";");
|
|
4126
|
+
const recompute=()=>{
|
|
4127
|
+
const desired=new Map(); // el → {css,vars}
|
|
4128
|
+
for(const sp of specs){
|
|
4129
|
+
const stEls=fresh(sp.stateTarget);
|
|
4130
|
+
// is THIS phase's destination state active on the stateTarget RIGHT NOW?
|
|
4131
|
+
let active;
|
|
4132
|
+
if(sp.toSuf) active=stEls.length?stEls.some(e=>safeMatches(e,sp.toSuf)):true; // open
|
|
4133
|
+
else if(sp.fromSuf) active=stEls.length?stEls.some(e=>!safeMatches(e,sp.fromSuf)):true; // close = not in fromState
|
|
4134
|
+
else active=true; // no state info → always reflect (degrades to old behavior)
|
|
4135
|
+
if(!active)continue;
|
|
4136
|
+
for(const mm of sp.members) for(const el of txMemberEls(mm.selector,sp.stateTarget)){ if(inUI(el))continue; desired.set(el,{css:mm.css,vars:mm.vars}); }
|
|
4137
|
+
}
|
|
4138
|
+
for(const [el,d] of desired){
|
|
4139
|
+
const vars=d.vars||{},vk=varsKey(vars),cur=store.applied.get(el);
|
|
4140
|
+
if(cur&&cur.css===d.css&&cur.vk===vk)continue;
|
|
4141
|
+
if(!cur) store.applied.set(el,{prevInline:el.style.transition||"",prevVars:Object.fromEntries(Object.keys(vars).map(n=>[n,el.style.getPropertyValue(n)])),css:d.css,vk});
|
|
4142
|
+
else store.applied.set(el,{prevInline:cur.prevInline,prevVars:cur.prevVars,css:d.css,vk});
|
|
4143
|
+
try{el.style.transition=d.css;}catch{}
|
|
4144
|
+
for(const n of Object.keys(vars)){try{el.style.setProperty(n,vars[n]);}catch{}}
|
|
4145
|
+
}
|
|
4146
|
+
for(const [el,rec] of Array.from(store.applied)){
|
|
4147
|
+
if(desired.has(el))continue;
|
|
4148
|
+
try{el.style.transition=rec.prevInline;}catch{}
|
|
4149
|
+
for(const n of Object.keys(rec.prevVars||{})){try{const pv=rec.prevVars[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}
|
|
4150
|
+
store.applied.delete(el);
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
// Coalesce via microtask (NOT rAF): the gate must update the moment the
|
|
4154
|
+
// state class/attr flips — before the browser styles the next frame — so
|
|
4155
|
+
// the very first interaction animates with the edited timing, and reverts
|
|
4156
|
+
// are instant even when the tab is backgrounded (rAF pauses there).
|
|
4157
|
+
const queueMt=typeof queueMicrotask==="function"?queueMicrotask:(f)=>Promise.resolve().then(f);
|
|
4158
|
+
const schedule=()=>{ if(store.pending)return; store.pending=true; queueMt(()=>{store.pending=false;recompute();}); };
|
|
4159
|
+
// ONE observer on the document subtree: state lives on elements that may
|
|
4160
|
+
// mount/unmount (panels) or just toggle a class/attr, so we watch class +
|
|
4161
|
+
// the state attrs across the tree, plus childList for remounts. Never
|
|
4162
|
+
// `style` → our own inline writes can't feed back. rAF-coalesced.
|
|
4163
|
+
try{ const mo=new MutationObserver(schedule); mo.observe(document.body,{attributes:true,attributeFilter:Array.from(attrNames),subtree:true,childList:true}); store.observers.push(mo); }catch{}
|
|
4164
|
+
if(anyHover){ const onPtr=()=>schedule(); document.addEventListener("pointerover",onPtr,true); document.addEventListener("pointerout",onPtr,true); store.ptr=onPtr; }
|
|
4165
|
+
recompute(); // initial pass
|
|
4166
|
+
return ()=>txTeardownPhase(store);
|
|
4167
|
+
},[entries,registry]);
|
|
4003
4168
|
const startResize=useCallback(e=>{
|
|
4004
4169
|
e.preventDefault();
|
|
4005
4170
|
const startY=e.clientY; const startH=panelHeight;
|
|
@@ -4159,9 +4324,11 @@
|
|
|
4159
4324
|
cssRules:harvestCssHints(e)}));
|
|
4160
4325
|
try{
|
|
4161
4326
|
const{id}=await relayCreateJob({kind:"scan",url:location.href,raw});
|
|
4162
|
-
for(let i=0;i<
|
|
4327
|
+
for(let i=0;i<520;i++){
|
|
4163
4328
|
if(scanTokenRef.current!==token)return; // superseded / unmounted
|
|
4164
|
-
|
|
4329
|
+
// Ramp: poll fast (120ms) for the first ~2.4s so a quick scan returns
|
|
4330
|
+
// almost immediately, then settle to 300ms to keep the request rate low.
|
|
4331
|
+
await new Promise(r=>setTimeout(r,i<20?120:300));
|
|
4165
4332
|
if(scanTokenRef.current!==token)return;
|
|
4166
4333
|
const job=await relayGetJob(id);
|
|
4167
4334
|
if(job.status==="done"){
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "Live, agent-driven Refine panel for CSS/Motion transitions — injects a timeline + Refine UI and runs transitions.dev suggestions via your coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server/relay.mjs
CHANGED
|
@@ -47,7 +47,15 @@ try {
|
|
|
47
47
|
// --print), and --force (auto-allow tool calls so apply/scan don't hang on
|
|
48
48
|
// approval). Append only the missing flags; leave non-cursor-agent commands alone.
|
|
49
49
|
function augmentAgentCmd(cmd) {
|
|
50
|
-
if (!cmd
|
|
50
|
+
if (!cmd) return cmd;
|
|
51
|
+
// `codex exec` reads the prompt from stdin ONLY when given a trailing `-`
|
|
52
|
+
// (the stdin marker). The relay always pipes the prompt on stdin, so without
|
|
53
|
+
// it Codex gets no prompt and exits 1. Append `-` when it's missing (a lone
|
|
54
|
+
// `-` token, not the dashes inside flags like `--sandbox`).
|
|
55
|
+
if (/(^|\s|\/)codex(\s|$)/.test(cmd) && /(^|\s)exec(\s|$)/.test(cmd)) {
|
|
56
|
+
return /(^|\s)-(\s|$)/.test(cmd) ? cmd : `${cmd} -`;
|
|
57
|
+
}
|
|
58
|
+
if (!/(^|\s|\/)cursor-agent(\s|$)/.test(cmd)) return cmd;
|
|
51
59
|
const has = (...flags) => flags.some((f) => new RegExp(`(^|\\s)${f.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`).test(cmd));
|
|
52
60
|
const extra = [];
|
|
53
61
|
if (!has("-p", "--print")) extra.push("-p");
|
|
@@ -61,6 +69,13 @@ const AGENT_CMD = augmentAgentCmd(process.env.REFINE_AGENT_CMD || null);
|
|
|
61
69
|
// (Opus / GPT-5.5) — forcing a fast model here keeps the initial scan snappy.
|
|
62
70
|
// Override with REFINE_SCAN_MODEL="" to fall back to the agent's default.
|
|
63
71
|
const SCAN_MODEL = process.env.REFINE_SCAN_MODEL ?? "composer-2.5-fast";
|
|
72
|
+
// Reasoning effort for scan jobs on Codex (cursor-agent uses SCAN_MODEL instead).
|
|
73
|
+
// Grouping is a structured, near-mechanical task — the cssRules payload already
|
|
74
|
+
// hands the agent the timings + state selectors — so it does NOT need deep
|
|
75
|
+
// reasoning. A default-effort Codex run can take tens of seconds; "low" cuts that
|
|
76
|
+
// dramatically with no grouping/naming loss. Override with REFINE_SCAN_EFFORT
|
|
77
|
+
// (minimal|low|medium|high) or "" to use the agent's configured effort.
|
|
78
|
+
const SCAN_EFFORT = process.env.REFINE_SCAN_EFFORT ?? "low";
|
|
64
79
|
// Pin a fast model for refine (suggestion) jobs too. The motion-token vocabulary
|
|
65
80
|
// is inlined into the prompt (see MOTION_TOKENS_BLOCK) so a fast model has every
|
|
66
81
|
// fact it needs for the common token-tweak path — keeping suggestions snappy
|
|
@@ -83,6 +98,28 @@ function withModel(cmd, model) {
|
|
|
83
98
|
if (/(^|\s)--model(\s|=)/.test(cmd)) return cmd; // respect an explicit choice
|
|
84
99
|
return cmd.replace(/^(\s*\S+)/, `$1 --model ${model}`);
|
|
85
100
|
}
|
|
101
|
+
|
|
102
|
+
// Speed up the SCAN job per agent — grouping is structured and doesn't need a
|
|
103
|
+
// heavy model / deep reasoning:
|
|
104
|
+
// • cursor-agent → pin SCAN_MODEL (a fast model) via --model.
|
|
105
|
+
// • codex exec → drop reasoning to SCAN_EFFORT ("low") and silence reasoning
|
|
106
|
+
// summaries (less work AND cleaner stdout for JSON parsing) via `-c` config
|
|
107
|
+
// overrides, inserted right after `exec` (before the trailing `-` stdin marker).
|
|
108
|
+
// No-ops if the user already pinned a model / reasoning effort, or disabled it
|
|
109
|
+
// (REFINE_SCAN_MODEL="" / REFINE_SCAN_EFFORT=""). Other CLIs are left untouched.
|
|
110
|
+
function withScanSpeed(cmd) {
|
|
111
|
+
if (!cmd) return cmd;
|
|
112
|
+
let out = withModel(cmd, SCAN_MODEL); // cursor-agent fast model
|
|
113
|
+
const isCodexExec =
|
|
114
|
+
/(^|\s|\/)codex(\s|$)/.test(out) && /(^|\s)exec(\s|$)/.test(out);
|
|
115
|
+
if (isCodexExec && SCAN_EFFORT && !/model_reasoning_effort/.test(out)) {
|
|
116
|
+
out = out.replace(
|
|
117
|
+
/(^|\s)exec(\s|$)/,
|
|
118
|
+
`$1exec -c model_reasoning_effort="${SCAN_EFFORT}" -c model_reasoning_summary="none"$2`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
86
123
|
const LONGPOLL_MS = Number(process.env.REFINE_LONGPOLL_MS) || 25000;
|
|
87
124
|
// Grace window after a `/refine live` agent's last poll during which LLM mode is
|
|
88
125
|
// still reported "available". Kept well above LONGPOLL_MS so the normal gaps
|
|
@@ -369,6 +406,18 @@ const AGENT_RETRIES = Number.isFinite(Number(process.env.REFINE_AGENT_RETRIES))
|
|
|
369
406
|
? Number(process.env.REFINE_AGENT_RETRIES)
|
|
370
407
|
: 2;
|
|
371
408
|
|
|
409
|
+
// CLIs (Codex especially) print verbose startup WARNINGs to stderr even on
|
|
410
|
+
// success — e.g. "could not create PATH aliases" and "failed to open state db"
|
|
411
|
+
// (Codex literally logs "proceeding" right after). Those lines used to eat the
|
|
412
|
+
// 300-char error budget, truncating away the REAL failure. Strip the known
|
|
413
|
+
// noise so the genuine error surfaces; keep a generous cap.
|
|
414
|
+
function cleanAgentErr(err) {
|
|
415
|
+
const noise = /could not create PATH aliases|failed to open state db|state DB at|codex_state::runtime|^\s*WARNING:|(^|\s)WARN(\s|:)/i;
|
|
416
|
+
const lines = (err || "").split(/\r?\n/).filter((l) => l.trim() && !noise.test(l));
|
|
417
|
+
const msg = lines.join("\n").trim() || (err || "").trim() || "(no stderr)";
|
|
418
|
+
return msg.slice(0, 1500);
|
|
419
|
+
}
|
|
420
|
+
|
|
372
421
|
function runAgentOnce(cmd, prompt, parse) {
|
|
373
422
|
return new Promise((resolve, reject) => {
|
|
374
423
|
const child = spawn("sh", ["-c", cmd], { stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -386,7 +435,7 @@ function runAgentOnce(cmd, prompt, parse) {
|
|
|
386
435
|
});
|
|
387
436
|
child.on("close", (code) => {
|
|
388
437
|
clearTimeout(timer);
|
|
389
|
-
if (code !== 0) return reject(new Error(`agent exited ${code}: ${err
|
|
438
|
+
if (code !== 0) return reject(new Error(`agent exited ${code}: ${cleanAgentErr(err)}`));
|
|
390
439
|
try {
|
|
391
440
|
resolve(parse(out));
|
|
392
441
|
} catch (e) {
|
|
@@ -535,7 +584,7 @@ async function answerJob(job) {
|
|
|
535
584
|
);
|
|
536
585
|
}
|
|
537
586
|
job.statusLog.push({ message: "Reading components from source…", at: now() });
|
|
538
|
-
result = await runAgentCmd(
|
|
587
|
+
result = await runAgentCmd(withScanSpeed(AGENT_CMD), buildScanPrompt(job), parseScanOutput);
|
|
539
588
|
job.result = { groups: result.groups, summary: result.summary };
|
|
540
589
|
job.status = "done";
|
|
541
590
|
job.updatedAt = now();
|