Skip to content

Commit e21bd84

Browse files
committed
live sync
1 parent 386c37b commit e21bd84

4 files changed

Lines changed: 47 additions & 18 deletions

File tree

src/Reactor.Cli/Figma/FigmaClient.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,29 @@ public FigmaClient(string apiKey)
2525
/// <summary>
2626
/// Returns the <c>lastModified</c> timestamp and file name for a Figma file,
2727
/// using a lightweight metadata-only request (no document tree).
28+
/// On a 429, <see cref="RetryAfterSeconds"/> is set from the Retry-After header.
2829
/// </summary>
30+
public int? RetryAfterSeconds { get; private set; }
31+
2932
public async Task<FigmaFileInfo?> GetFileInfoAsync(string fileKey, CancellationToken ct = default)
3033
{
34+
RetryAfterSeconds = null;
35+
3136
// depth=1 returns only top-level metadata without traversing the full tree
3237
var response = await _http.GetAsync($"v1/files/{fileKey}?depth=1", ct);
3338
if (!response.IsSuccessStatusCode)
3439
{
3540
var status = (int)response.StatusCode;
3641
if (status == 429)
37-
Console.Error.WriteLine("[mur figma] Figma API rate limit exceeded (429). Wait a minute and retry.");
42+
{
43+
// Respect Retry-After header if present, otherwise default to 60s
44+
RetryAfterSeconds = 60;
45+
if (response.Headers.RetryAfter?.Delta is { } delta)
46+
RetryAfterSeconds = Math.Max(1, (int)Math.Ceiling(delta.TotalSeconds));
47+
else if (response.Headers.RetryAfter?.Date is { } date)
48+
RetryAfterSeconds = Math.Max(1, (int)Math.Ceiling((date - DateTimeOffset.UtcNow).TotalSeconds));
49+
Console.Error.WriteLine($"[mur figma] Rate limited (429). Waiting {RetryAfterSeconds}s before next poll.");
50+
}
3851
else if (status == 403)
3952
Console.Error.WriteLine("[mur figma] Figma API key is invalid or expired (403).");
4053
else

src/Reactor.Cli/Figma/FigmaWatchCommand.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,13 @@ public static async Task<int> RunAsync(string[] args)
136136
Console.Error.WriteLine("[mur figma watch] Too many consecutive errors. Stopping.");
137137
return 1;
138138
}
139-
Console.Error.WriteLine("[mur figma watch] Failed to fetch — retrying...");
139+
140+
// Back off: use Retry-After from the API, or exponential fallback
141+
var backoff = client.RetryAfterSeconds
142+
?? Math.Min(120, intervalSeconds * (1 << consecutiveErrors));
143+
Console.Error.WriteLine($"[mur figma watch] Failed to fetch — retrying in {backoff}s...");
144+
try { await Task.Delay(TimeSpan.FromSeconds(backoff), cts.Token); }
145+
catch (OperationCanceledException) { break; }
140146
continue;
141147
}
142148

tools/figma-plugin/dist/ui.html

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,12 @@
208208
var name = getProjectName();
209209

210210
var skillRef = 'Read skills/figma.md for Figma-to-Reactor control mapping, theme tokens, typography, and layout rules. Read skills/design.md for WinUI design best practices. Match every dimension, gap, and padding value exactly from the Figma design for pixel-accurate output.';
211-
var watchSuffix = ' Then start watching for Figma changes: run mur figma watch "' + url + '" --interval 10 in the background. When a change is detected, re-fetch the design via get_figma_data and update the code accordingly.';
211+
var watchCmd = 'dotnet run --project src/Reactor.Cli -- figma watch "' + url + '" --interval 10';
212+
var watchLoop = ' After the initial sync, start a live watch loop: run `' + watchCmd +
213+
'` in a background shell. It emits JSON events to stdout when the Figma file changes.' +
214+
' When you see an event with "event":"changed", re-fetch the design via get_figma_data MCP tool for file key ' + (frameInfo.fileKey || '') + ' node ' + (frameInfo.nodeId || '') +
215+
', diff it against the current code, and apply only the changes needed.' +
216+
' Keep this loop running — do NOT exit after the first sync. You are a live sync agent.';
212217

213218
var prompt;
214219
if (isNewProject()) {
@@ -217,23 +222,23 @@
217222
', then translate this Figma design into it: ' + url +
218223
'. ' + skillRef +
219224
' After generating the code, run: dotnet watch run --project ' + csproj + '.' +
220-
watchSuffix;
225+
watchLoop;
221226
} else {
222227
if (!csproj) {
223228
showToast('Set the project path first');
224229
return;
225230
}
226-
prompt = 'I have an existing Reactor app at ' + csproj +
227-
'. Fetch this Figma design via get_figma_data: ' + url +
228-
'. Then read the current Program.cs in the project and diff it against the Figma frame.' +
231+
prompt = 'You are a live Figma-to-code sync agent for the Reactor app at ' + csproj + '.' +
232+
' First, fetch this Figma design via get_figma_data: ' + url +
233+
'. Read the current Program.cs in the project and diff it against the Figma frame.' +
229234
' Apply only the changes needed to bring the code in sync with the design — update text, spacing, sizing, colors, and layout to match.' +
230235
' Do not regenerate from scratch unless the structure has fundamentally changed.' +
231236
' ' + skillRef +
232-
' After updating the code, run: dotnet watch run --project ' + csproj + '.' +
233-
watchSuffix;
237+
' Make sure dotnet watch run --project ' + csproj + ' is running.' +
238+
watchLoop;
234239
}
235240

236-
var cmd = "copilot -p '" + prompt.replace(/'/g, "''") + "'";
241+
var cmd = "copilot --yolo -p '" + prompt.replace(/'/g, "''") + "'";
237242
copyToClipboard(cmd);
238243

239244
if (isNewProject()) {

tools/figma-plugin/src/ui.html

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,12 @@
208208
var name = getProjectName();
209209

210210
var skillRef = 'Read skills/figma.md for Figma-to-Reactor control mapping, theme tokens, typography, and layout rules. Read skills/design.md for WinUI design best practices. Match every dimension, gap, and padding value exactly from the Figma design for pixel-accurate output.';
211-
var watchSuffix = ' Then start watching for Figma changes: run mur figma watch "' + url + '" --interval 10 in the background. When a change is detected, re-fetch the design via get_figma_data and update the code accordingly.';
211+
var watchCmd = 'dotnet run --project src/Reactor.Cli -- figma watch "' + url + '" --interval 10';
212+
var watchLoop = ' After the initial sync, start a live watch loop: run `' + watchCmd +
213+
'` in a background shell. It emits JSON events to stdout when the Figma file changes.' +
214+
' When you see an event with "event":"changed", re-fetch the design via get_figma_data MCP tool for file key ' + (frameInfo.fileKey || '') + ' node ' + (frameInfo.nodeId || '') +
215+
', diff it against the current code, and apply only the changes needed.' +
216+
' Keep this loop running — do NOT exit after the first sync. You are a live sync agent.';
212217

213218
var prompt;
214219
if (isNewProject()) {
@@ -217,23 +222,23 @@
217222
', then translate this Figma design into it: ' + url +
218223
'. ' + skillRef +
219224
' After generating the code, run: dotnet watch run --project ' + csproj + '.' +
220-
watchSuffix;
225+
watchLoop;
221226
} else {
222227
if (!csproj) {
223228
showToast('Set the project path first');
224229
return;
225230
}
226-
prompt = 'I have an existing Reactor app at ' + csproj +
227-
'. Fetch this Figma design via get_figma_data: ' + url +
228-
'. Then read the current Program.cs in the project and diff it against the Figma frame.' +
231+
prompt = 'You are a live Figma-to-code sync agent for the Reactor app at ' + csproj + '.' +
232+
' First, fetch this Figma design via get_figma_data: ' + url +
233+
'. Read the current Program.cs in the project and diff it against the Figma frame.' +
229234
' Apply only the changes needed to bring the code in sync with the design — update text, spacing, sizing, colors, and layout to match.' +
230235
' Do not regenerate from scratch unless the structure has fundamentally changed.' +
231236
' ' + skillRef +
232-
' After updating the code, run: dotnet watch run --project ' + csproj + '.' +
233-
watchSuffix;
237+
' Make sure dotnet watch run --project ' + csproj + ' is running.' +
238+
watchLoop;
234239
}
235240

236-
var cmd = "copilot -p '" + prompt.replace(/'/g, "''") + "'";
241+
var cmd = "copilot --yolo -p '" + prompt.replace(/'/g, "''") + "'";
237242
copyToClipboard(cmd);
238243

239244
if (isNewProject()) {

0 commit comments

Comments
 (0)