transitions-refine 0.3.13 → 0.3.15
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.
|
@@ -54,6 +54,19 @@ The relay reports the agent as "available" for ~120s after your last poll, so
|
|
|
54
54
|
short pauses keep you live. A successful job always resets idle, so an active
|
|
55
55
|
session never backs off.
|
|
56
56
|
|
|
57
|
+
0. **Announce yourself once, before the first poll.** The relay keeps a *sticky*
|
|
58
|
+
Stop latch: after a panel **Stop** (or the idle auto-stop) it answers every
|
|
59
|
+
`GET /jobs/next` with `{"stop": true}` until a new agent explicitly resumes —
|
|
60
|
+
so a stopped session can't silently come back. Clear the latch a single time
|
|
61
|
+
at startup, then begin polling:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl -s -X POST http://localhost:7331/poller/start
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Do **not** call this again mid-loop (it would defeat a user's Stop). Only on a
|
|
68
|
+
fresh `/refine live`.
|
|
69
|
+
|
|
57
70
|
1. **Claim the next job (long-poll).** This call blocks up to ~25s, then returns.
|
|
58
71
|
|
|
59
72
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
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": {
|
|
@@ -446,6 +446,10 @@ async function pollOnce() {
|
|
|
446
446
|
|
|
447
447
|
async function loop() {
|
|
448
448
|
log(`live refine loop → ${RELAY} (workspace: ${WORKSPACE})`);
|
|
449
|
+
// Announce ourselves so the relay clears any prior Stop latch — otherwise a
|
|
450
|
+
// relay that's still "stopped" from a previous panel Stop would answer our
|
|
451
|
+
// first poll with {stop:true} and we'd exit immediately.
|
|
452
|
+
try { await post("/poller/start", {}); } catch {}
|
|
449
453
|
// eslint-disable-next-line no-constant-condition
|
|
450
454
|
while (true) {
|
|
451
455
|
try {
|
package/server/relay.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { createServer } from "node:http";
|
|
26
26
|
import { randomUUID } from "node:crypto";
|
|
27
27
|
import { spawn } from "node:child_process";
|
|
28
|
-
import { existsSync } from "node:fs";
|
|
28
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { delimiter, join } from "node:path";
|
|
31
31
|
import { refineTimings, DURATION_TOKENS, SCALE_TOKENS, BLUR_TOKENS, SMOOTH_OUT } from "./motion-tokens.mjs";
|
|
@@ -33,6 +33,12 @@ import { buildInjectModule } from "./inject.mjs";
|
|
|
33
33
|
|
|
34
34
|
const PORT = Number(process.env.REFINE_RELAY_PORT) || 7331;
|
|
35
35
|
const AUTO = process.env.REFINE_AUTO !== "0";
|
|
36
|
+
// Own package version, surfaced on /health so you can verify which relay build is
|
|
37
|
+
// actually running (npx caches — a stale relay is the usual "fix didn't work").
|
|
38
|
+
let PKG_VERSION = "0.0.0";
|
|
39
|
+
try {
|
|
40
|
+
PKG_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version || PKG_VERSION;
|
|
41
|
+
} catch {}
|
|
36
42
|
|
|
37
43
|
// A bare `cursor-agent` goes interactive: it prints "⚠ Workspace Trust Required"
|
|
38
44
|
// and exits 1, so every headless refine/scan/apply job fails. Force the headless
|
|
@@ -91,13 +97,13 @@ const PENDING_TIMEOUT_MS = Number(process.env.REFINE_PENDING_TIMEOUT_MS) || 1200
|
|
|
91
97
|
// to stop (it returns {stop:true} from /jobs/next). 0 disables. Only the chat
|
|
92
98
|
// loop is affected — a wired REFINE_AGENT_CMD never polls /jobs/next.
|
|
93
99
|
const POLLER_IDLE_STOP_MS = Number(process.env.REFINE_POLLER_IDLE_STOP_MS) || 600000;
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
const
|
|
100
|
+
// While the Stop latch is held, a genuinely new `/refine live` session can resume
|
|
101
|
+
// itself implicitly: only after polling has been *quiet* this long (the old loop
|
|
102
|
+
// actually died) do we treat the next poll as a fresh session and auto-resume.
|
|
103
|
+
// Must exceed a looping agent's poll gap, so an agent that ignores Stop and keeps
|
|
104
|
+
// polling can never clear the latch — it just keeps getting {stop:true}. (An
|
|
105
|
+
// updated agent also resumes explicitly via POST /poller/start, no wait needed.)
|
|
106
|
+
const STOP_QUIET_MS = Number(process.env.REFINE_STOP_QUIET_MS) || 15000;
|
|
101
107
|
|
|
102
108
|
/** @type {Map<string, Job>} */
|
|
103
109
|
const jobs = new Map();
|
|
@@ -106,10 +112,17 @@ const now = () => Date.now();
|
|
|
106
112
|
// When did a `/refine live` agent last poll? Used to know if LLM mode can be
|
|
107
113
|
// served by a live editor agent (vs. needing REFINE_AGENT_CMD).
|
|
108
114
|
let lastPollAt = 0;
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
// Stop latch. Set by POST /poller/stop (and idle auto-stop); cleared ONLY by an
|
|
116
|
+
// explicit POST /poller/start (a fresh `/refine live` / agent loop announcing
|
|
117
|
+
// itself). While latched the poller reports inactive and every GET /jobs/next
|
|
118
|
+
// answers {stop:true}, so a loop that ignores the stop — or keeps polling /
|
|
119
|
+
// re-polls — can never silently revive the session. This is what makes the
|
|
120
|
+
// panel's Stop button stick instead of flipping "Live" back on seconds later.
|
|
121
|
+
let pollerStopped = false;
|
|
122
|
+
// When did a poll last arrive *while latched*? Used to detect that the old loop
|
|
123
|
+
// went quiet so a genuinely new session (or re-run) can auto-resume.
|
|
124
|
+
let lastStoppedPollAt = 0;
|
|
125
|
+
const pollerActive = () => !pollerStopped && now() - lastPollAt < POLLER_TTL_MS;
|
|
113
126
|
const llmAvailable = () => Boolean(AGENT_CMD) || pollerActive();
|
|
114
127
|
|
|
115
128
|
// Stop signal for the in-chat `/refine live` loop. Set by POST /poller/stop
|
|
@@ -599,9 +612,11 @@ const server = createServer(async (req, res) => {
|
|
|
599
612
|
if (method === "GET" && path === "/health") {
|
|
600
613
|
return send(res, 200, {
|
|
601
614
|
ok: true,
|
|
615
|
+
version: PKG_VERSION,
|
|
602
616
|
auto: AUTO,
|
|
603
617
|
llmAvailable: llmAvailable(),
|
|
604
618
|
pollerActive: pollerActive(),
|
|
619
|
+
pollerStopped,
|
|
605
620
|
agentCmd: Boolean(AGENT_CMD),
|
|
606
621
|
cliInstalled: cursorCliInstalled(),
|
|
607
622
|
jobs: jobs.size,
|
|
@@ -663,32 +678,43 @@ const server = createServer(async (req, res) => {
|
|
|
663
678
|
|
|
664
679
|
// GET /jobs/next — long-poll claimed by a `/refine live` agent (LLM jobs).
|
|
665
680
|
if (method === "GET" && path === "/jobs/next") {
|
|
666
|
-
//
|
|
667
|
-
// exit and
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
681
|
+
// Stopped latch: a Stop was issued and not yet resumed. Keep telling every
|
|
682
|
+
// poller to exit and report inactive until either an explicit POST
|
|
683
|
+
// /poller/start, or polling goes quiet for STOP_QUIET_MS (the old loop died)
|
|
684
|
+
// and a new poll arrives — only then is it a genuinely fresh session.
|
|
685
|
+
// A pending job always wins so we never drop real work on the edge.
|
|
686
|
+
if (pollerStopped && !nextPendingLlm()) {
|
|
687
|
+
if (lastStoppedPollAt && now() - lastStoppedPollAt >= STOP_QUIET_MS) {
|
|
688
|
+
// Quiet gap → the previous loop is gone. This poll is a fresh session.
|
|
689
|
+
pollerStopped = false;
|
|
690
|
+
lastStoppedPollAt = 0;
|
|
691
|
+
// fall through to normal handling below (counts as live again)
|
|
692
|
+
} else {
|
|
693
|
+
lastStoppedPollAt = now(); // a poller is still hammering us → hold the latch
|
|
694
|
+
return send(res, 200, { stop: true });
|
|
695
|
+
}
|
|
672
696
|
}
|
|
673
697
|
lastPollAt = now();
|
|
674
698
|
if (!lastJobAt) lastJobAt = now(); // seed idle window on the loop's first poll
|
|
675
|
-
//
|
|
676
|
-
// A pending job always wins so we never drop real work
|
|
699
|
+
// Idle auto-stop (or a leftover stopRequested) → latch stopped + tell the
|
|
700
|
+
// loop to exit. A pending job always wins so we never drop real work.
|
|
677
701
|
if ((stopRequested || (POLLER_IDLE_STOP_MS && now() - lastJobAt >= POLLER_IDLE_STOP_MS)) && !nextPendingLlm()) {
|
|
678
702
|
stopRequested = false;
|
|
703
|
+
pollerStopped = true; // latch until /poller/start or a quiet gap
|
|
704
|
+
lastStoppedPollAt = 0;
|
|
679
705
|
lastJobAt = 0;
|
|
680
706
|
lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
|
|
681
|
-
stoppedUntil = now() + STOP_GRACE_MS; // hold the stop so re-polls can't revive it
|
|
682
707
|
return send(res, 200, { stop: true });
|
|
683
708
|
}
|
|
684
709
|
const deadline = now() + LONGPOLL_MS;
|
|
685
710
|
const attempt = () => {
|
|
686
711
|
if (res.writableEnded) return;
|
|
687
|
-
if (
|
|
712
|
+
if (stopRequested && !nextPendingLlm()) {
|
|
688
713
|
stopRequested = false;
|
|
714
|
+
pollerStopped = true; // latch until /poller/start or a quiet gap
|
|
715
|
+
lastStoppedPollAt = 0;
|
|
689
716
|
lastJobAt = 0;
|
|
690
717
|
lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
|
|
691
|
-
stoppedUntil = now() + STOP_GRACE_MS; // hold the stop so re-polls can't revive it
|
|
692
718
|
return send(res, 200, { stop: true });
|
|
693
719
|
}
|
|
694
720
|
const job = nextPendingLlm();
|
|
@@ -709,11 +735,25 @@ const server = createServer(async (req, res) => {
|
|
|
709
735
|
if (method === "POST" && path === "/poller/stop") {
|
|
710
736
|
const wasActive = now() - lastPollAt < POLLER_TTL_MS;
|
|
711
737
|
stopRequested = true;
|
|
738
|
+
pollerStopped = true; // latch: stays stopped until /poller/start or a quiet gap
|
|
739
|
+
lastStoppedPollAt = 0;
|
|
712
740
|
lastPollAt = 0; // report inactive immediately; a straggler poll won't revive it
|
|
713
|
-
stoppedUntil = now() + STOP_GRACE_MS; // hold the stop through the grace window
|
|
714
741
|
return send(res, 200, { ok: true, stopping: wasActive });
|
|
715
742
|
}
|
|
716
743
|
|
|
744
|
+
// POST /poller/start — a fresh `/refine live` agent (or loop) announces itself
|
|
745
|
+
// and clears the Stop latch so its polls count as live again. Without this an
|
|
746
|
+
// explicit re-run would have to wait out the quiet window. Call it once at loop
|
|
747
|
+
// startup, before the first GET /jobs/next.
|
|
748
|
+
if (method === "POST" && path === "/poller/start") {
|
|
749
|
+
pollerStopped = false;
|
|
750
|
+
lastStoppedPollAt = 0;
|
|
751
|
+
stopRequested = false;
|
|
752
|
+
lastJobAt = now();
|
|
753
|
+
lastPollAt = now();
|
|
754
|
+
return send(res, 200, { ok: true });
|
|
755
|
+
}
|
|
756
|
+
|
|
717
757
|
const m = path.match(/^\/jobs\/([^/]+)(?:\/(status|result|error))?$/);
|
|
718
758
|
if (m) {
|
|
719
759
|
const job = jobs.get(m[1]);
|