transitions-refine 0.3.14 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server/relay.mjs +41 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.3.14",
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": {
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,6 +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;
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;
94
107
 
95
108
  /** @type {Map<string, Job>} */
96
109
  const jobs = new Map();
@@ -106,6 +119,9 @@ let lastPollAt = 0;
106
119
  // re-polls — can never silently revive the session. This is what makes the
107
120
  // panel's Stop button stick instead of flipping "Live" back on seconds later.
108
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;
109
125
  const pollerActive = () => !pollerStopped && now() - lastPollAt < POLLER_TTL_MS;
110
126
  const llmAvailable = () => Boolean(AGENT_CMD) || pollerActive();
111
127
 
@@ -596,9 +612,11 @@ const server = createServer(async (req, res) => {
596
612
  if (method === "GET" && path === "/health") {
597
613
  return send(res, 200, {
598
614
  ok: true,
615
+ version: PKG_VERSION,
599
616
  auto: AUTO,
600
617
  llmAvailable: llmAvailable(),
601
618
  pollerActive: pollerActive(),
619
+ pollerStopped,
602
620
  agentCmd: Boolean(AGENT_CMD),
603
621
  cliInstalled: cursorCliInstalled(),
604
622
  jobs: jobs.size,
@@ -661,10 +679,20 @@ const server = createServer(async (req, res) => {
661
679
  // GET /jobs/next — long-poll claimed by a `/refine live` agent (LLM jobs).
662
680
  if (method === "GET" && path === "/jobs/next") {
663
681
  // Stopped latch: a Stop was issued and not yet resumed. Keep telling every
664
- // poller to exit and report inactive (don't touch lastPollAt) until a fresh
665
- // POST /poller/start. A pending job still wins so we never drop real work.
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.
666
686
  if (pollerStopped && !nextPendingLlm()) {
667
- return send(res, 200, { stop: true });
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
+ }
668
696
  }
669
697
  lastPollAt = now();
670
698
  if (!lastJobAt) lastJobAt = now(); // seed idle window on the loop's first poll
@@ -672,7 +700,8 @@ const server = createServer(async (req, res) => {
672
700
  // loop to exit. A pending job always wins so we never drop real work.
673
701
  if ((stopRequested || (POLLER_IDLE_STOP_MS && now() - lastJobAt >= POLLER_IDLE_STOP_MS)) && !nextPendingLlm()) {
674
702
  stopRequested = false;
675
- pollerStopped = true; // latch until an explicit /poller/start
703
+ pollerStopped = true; // latch until /poller/start or a quiet gap
704
+ lastStoppedPollAt = 0;
676
705
  lastJobAt = 0;
677
706
  lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
678
707
  return send(res, 200, { stop: true });
@@ -682,7 +711,8 @@ const server = createServer(async (req, res) => {
682
711
  if (res.writableEnded) return;
683
712
  if (stopRequested && !nextPendingLlm()) {
684
713
  stopRequested = false;
685
- pollerStopped = true; // latch until an explicit /poller/start
714
+ pollerStopped = true; // latch until /poller/start or a quiet gap
715
+ lastStoppedPollAt = 0;
686
716
  lastJobAt = 0;
687
717
  lastPollAt = 0; // loop is exiting → report it inactive immediately on /health
688
718
  return send(res, 200, { stop: true });
@@ -705,17 +735,19 @@ const server = createServer(async (req, res) => {
705
735
  if (method === "POST" && path === "/poller/stop") {
706
736
  const wasActive = now() - lastPollAt < POLLER_TTL_MS;
707
737
  stopRequested = true;
708
- pollerStopped = true; // latch: stays stopped until POST /poller/start
738
+ pollerStopped = true; // latch: stays stopped until /poller/start or a quiet gap
739
+ lastStoppedPollAt = 0;
709
740
  lastPollAt = 0; // report inactive immediately; a straggler poll won't revive it
710
741
  return send(res, 200, { ok: true, stopping: wasActive });
711
742
  }
712
743
 
713
744
  // POST /poller/start — a fresh `/refine live` agent (or loop) announces itself
714
745
  // and clears the Stop latch so its polls count as live again. Without this an
715
- // explicit re-run couldn't resume, since a latched relay tells every poll to
716
- // stop. Call it once at loop startup, before the first GET /jobs/next.
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.
717
748
  if (method === "POST" && path === "/poller/start") {
718
749
  pollerStopped = false;
750
+ lastStoppedPollAt = 0;
719
751
  stopRequested = false;
720
752
  lastJobAt = now();
721
753
  lastPollAt = now();