@fias/arche-sdk 1.6.0 → 1.8.0
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/dist/cli/create-plugin.js +0 -0
- package/dist/hooks.d.ts +24 -11
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +142 -29
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.test.js +191 -0
- package/dist/hooks.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +92 -16
- package/dist/types.d.ts +20 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +103 -3
- package/templates/default/CLAUDE.md +102 -2
- package/templates/multi-step/README.md +13 -0
- package/templates/multi-step/fias-plugin.json +11 -0
- package/templates/multi-step/index.html +12 -0
- package/templates/multi-step/package.json +26 -0
- package/templates/multi-step/src/App.tsx +38 -0
- package/templates/multi-step/src/context/AppContext.tsx +36 -0
- package/templates/multi-step/src/index.tsx +12 -0
- package/templates/multi-step/src/steps/step-one/StepOne.tsx +43 -0
- package/templates/multi-step/src/steps/step-two/StepTwo.tsx +40 -0
- package/templates/multi-step/tsconfig.json +17 -0
- package/templates/multi-step/vite.config.ts +18 -0
package/dist/index.mjs
CHANGED
|
@@ -353,6 +353,36 @@ function useBridge() {
|
|
|
353
353
|
}
|
|
354
354
|
return bridge;
|
|
355
355
|
}
|
|
356
|
+
var PERSIST_DEBOUNCE_WAIT_MS = 250;
|
|
357
|
+
var PERSIST_DEBOUNCE_MAX_WAIT_MS = 1e3;
|
|
358
|
+
function createDebouncedWriter(bridge) {
|
|
359
|
+
let pending = null;
|
|
360
|
+
function flush() {
|
|
361
|
+
if (!pending) return;
|
|
362
|
+
const { path, content, timerId } = pending;
|
|
363
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
364
|
+
pending = null;
|
|
365
|
+
bridge.request("storage_write", { path, content }).catch(() => {
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function schedule(path, content) {
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
if (pending && pending.path !== path) {
|
|
371
|
+
flush();
|
|
372
|
+
}
|
|
373
|
+
if (!pending) {
|
|
374
|
+
pending = { path, content, timerId: null, firstScheduledAt: now };
|
|
375
|
+
} else {
|
|
376
|
+
pending.content = content;
|
|
377
|
+
}
|
|
378
|
+
if (pending.timerId !== null) clearTimeout(pending.timerId);
|
|
379
|
+
const elapsed = now - pending.firstScheduledAt;
|
|
380
|
+
const remainingMax = Math.max(0, PERSIST_DEBOUNCE_MAX_WAIT_MS - elapsed);
|
|
381
|
+
const delay = Math.min(PERSIST_DEBOUNCE_WAIT_MS, remainingMax);
|
|
382
|
+
pending.timerId = setTimeout(flush, delay);
|
|
383
|
+
}
|
|
384
|
+
return { schedule, flush };
|
|
385
|
+
}
|
|
356
386
|
function useFiasUser() {
|
|
357
387
|
const bridge = useBridge();
|
|
358
388
|
const [user, setUser] = useState2(null);
|
|
@@ -482,41 +512,87 @@ function usePersistentState(key, initialValue) {
|
|
|
482
512
|
const bridge = useBridge();
|
|
483
513
|
const [value, setValueInternal] = useState2(initialValue);
|
|
484
514
|
const initializedRef = useRef(false);
|
|
515
|
+
const valueRef = useRef(initialValue);
|
|
516
|
+
const writer = useMemo2(() => createDebouncedWriter(bridge), [bridge]);
|
|
485
517
|
useEffect2(() => {
|
|
486
518
|
bridge.request("storage_read", { path: `__state/${key}` }).then((result) => {
|
|
487
519
|
if (result.exists && result.content !== null) {
|
|
488
520
|
try {
|
|
489
|
-
|
|
521
|
+
const parsed = JSON.parse(result.content);
|
|
522
|
+
valueRef.current = parsed;
|
|
523
|
+
setValueInternal(parsed);
|
|
490
524
|
} catch {
|
|
491
525
|
}
|
|
492
526
|
}
|
|
493
527
|
initializedRef.current = true;
|
|
494
528
|
});
|
|
495
|
-
|
|
529
|
+
return () => {
|
|
530
|
+
writer.flush();
|
|
531
|
+
};
|
|
532
|
+
}, [bridge, key, writer]);
|
|
496
533
|
const setValue = useCallback(
|
|
497
534
|
(next) => {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
content: JSON.stringify(resolved)
|
|
503
|
-
}).catch(() => {
|
|
504
|
-
});
|
|
505
|
-
return resolved;
|
|
506
|
-
});
|
|
535
|
+
const resolved = typeof next === "function" ? next(valueRef.current) : next;
|
|
536
|
+
valueRef.current = resolved;
|
|
537
|
+
setValueInternal(resolved);
|
|
538
|
+
writer.schedule(`__state/${key}`, JSON.stringify(resolved));
|
|
507
539
|
},
|
|
508
|
-
[
|
|
540
|
+
[key, writer]
|
|
509
541
|
);
|
|
510
542
|
return [value, setValue];
|
|
511
543
|
}
|
|
512
|
-
function useStepNavigation(initialStep) {
|
|
544
|
+
function useStepNavigation(initialStep, options) {
|
|
513
545
|
const bridge = useBridge();
|
|
514
|
-
const
|
|
546
|
+
const persistKey = options?.persistKey;
|
|
547
|
+
const [currentStep, setCurrentStepInternal] = useState2(initialStep ?? null);
|
|
548
|
+
const initializedRef = useRef(false);
|
|
549
|
+
const writer = useMemo2(() => createDebouncedWriter(bridge), [bridge]);
|
|
550
|
+
useEffect2(() => {
|
|
551
|
+
if (!persistKey) {
|
|
552
|
+
initializedRef.current = true;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
let cancelled = false;
|
|
556
|
+
bridge.request("storage_read", {
|
|
557
|
+
path: `__state/${persistKey}`
|
|
558
|
+
}).then((result) => {
|
|
559
|
+
if (cancelled || initializedRef.current) return;
|
|
560
|
+
if (result.exists && result.content !== null) {
|
|
561
|
+
try {
|
|
562
|
+
const parsed = JSON.parse(result.content);
|
|
563
|
+
if (typeof parsed === "string" || parsed === null) {
|
|
564
|
+
setCurrentStepInternal(parsed);
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
initializedRef.current = true;
|
|
570
|
+
}).catch(() => {
|
|
571
|
+
initializedRef.current = true;
|
|
572
|
+
});
|
|
573
|
+
return () => {
|
|
574
|
+
cancelled = true;
|
|
575
|
+
writer.flush();
|
|
576
|
+
};
|
|
577
|
+
}, [bridge, persistKey, writer]);
|
|
515
578
|
useEffect2(() => {
|
|
516
579
|
return bridge.onStepNavigationUpdate((nodeId) => {
|
|
517
|
-
|
|
580
|
+
setCurrentStepInternal(nodeId);
|
|
581
|
+
if (persistKey) {
|
|
582
|
+
writer.schedule(`__state/${persistKey}`, JSON.stringify(nodeId));
|
|
583
|
+
}
|
|
518
584
|
});
|
|
519
|
-
}, [bridge]);
|
|
585
|
+
}, [bridge, persistKey, writer]);
|
|
586
|
+
const setCurrentStep = useCallback(
|
|
587
|
+
(nodeId) => {
|
|
588
|
+
initializedRef.current = true;
|
|
589
|
+
setCurrentStepInternal(nodeId);
|
|
590
|
+
if (persistKey) {
|
|
591
|
+
writer.schedule(`__state/${persistKey}`, JSON.stringify(nodeId));
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
[persistKey, writer]
|
|
595
|
+
);
|
|
520
596
|
return { currentStep, setCurrentStep };
|
|
521
597
|
}
|
|
522
598
|
function useFiasDataStore() {
|
package/dist/types.d.ts
CHANGED
|
@@ -81,7 +81,11 @@ export interface EntityInvocationApi {
|
|
|
81
81
|
isLoading: boolean;
|
|
82
82
|
result: EntityInvocationResult | null;
|
|
83
83
|
error: Error | null;
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Tokens accumulated during a streaming invocation. Persists after the
|
|
86
|
+
* call completes (not reset to ''), so render either `streamingText` OR
|
|
87
|
+
* `result.output` — never both, or you'll show the response twice.
|
|
88
|
+
*/
|
|
85
89
|
streamingText: string;
|
|
86
90
|
}
|
|
87
91
|
export interface EntityInvocationParams {
|
|
@@ -152,6 +156,21 @@ export interface StepNavigationApi {
|
|
|
152
156
|
currentStep: string | null;
|
|
153
157
|
setCurrentStep: (step: string | null) => void;
|
|
154
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Options for useStepNavigation().
|
|
161
|
+
*/
|
|
162
|
+
export interface StepNavigationOptions {
|
|
163
|
+
/**
|
|
164
|
+
* When set, persists currentStep to bridge storage under `__state/<persistKey>`.
|
|
165
|
+
* The persisted value is read on mount (overriding initialStep if present) and
|
|
166
|
+
* written on every setCurrentStep call and incoming host step_navigate message.
|
|
167
|
+
*
|
|
168
|
+
* Use this instead of wrapping currentStep in usePersistentState separately —
|
|
169
|
+
* two independent step-state sources cause a bidirectional useEffect sync loop
|
|
170
|
+
* that spams storage and flashes the UI.
|
|
171
|
+
*/
|
|
172
|
+
persistKey?: string;
|
|
173
|
+
}
|
|
155
174
|
/**
|
|
156
175
|
* Message types sent from the parent frame to the plugin iframe.
|
|
157
176
|
*/
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,mBAAmB,GACnB,iBAAiB,GACjB,iBAAiB,GACjB,YAAY,GACZ,YAAY,GACZ,gBAAgB,CAAC;AAErB;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;IACF,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,UAAU,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACtC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This project is a FIAS platform plugin — a React application that runs in a sandboxed iframe within the FIAS marketplace. This file provides the context AI coding assistants need to build, test, and submit plugins effectively.
|
|
4
4
|
|
|
5
|
-
For other AI tool instruction files, see `
|
|
5
|
+
For other AI tool instruction files, see `CLAUDE.md` (identical content).
|
|
6
6
|
|
|
7
7
|
## Project Structure
|
|
8
8
|
|
|
@@ -183,13 +183,17 @@ function AISummarizer() {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// streamingText holds the accumulated tokens during the call AND continues to
|
|
187
|
+
// hold the full text after the call completes — it is NOT cleared. result.output
|
|
188
|
+
// also contains the full text once invoke() resolves. Render ONE slot only —
|
|
189
|
+
// streamingText while loading, result.output afterwards. Showing both renders
|
|
190
|
+
// the response twice.
|
|
186
191
|
return (
|
|
187
192
|
<div>
|
|
188
193
|
<button onClick={() => summarize('...')} disabled={isLoading}>
|
|
189
194
|
Summarize
|
|
190
195
|
</button>
|
|
191
|
-
{isLoading
|
|
192
|
-
{result && <p>{result.output}</p>}
|
|
196
|
+
<p>{isLoading ? streamingText : result?.output}</p>
|
|
193
197
|
{error && <p>Error: {error.message}</p>}
|
|
194
198
|
</div>
|
|
195
199
|
);
|
|
@@ -249,12 +253,22 @@ navigateTo('/settings');
|
|
|
249
253
|
|
|
250
254
|
**Returns:** `StepNavigationApi`
|
|
251
255
|
|
|
256
|
+
Call **once** in the top-level App component. Share `currentStep` and `setCurrentStep` with step components via React context — do not call `useStepNavigation` from step components (each call creates an independent state).
|
|
257
|
+
|
|
252
258
|
```tsx
|
|
253
259
|
import { useStepNavigation } from '@fias/arche-sdk';
|
|
254
260
|
|
|
261
|
+
// Without persistence — currentStep resets on preview rebuild
|
|
255
262
|
const { currentStep, setCurrentStep } = useStepNavigation('step-1');
|
|
263
|
+
|
|
264
|
+
// With persistence — currentStep survives preview rebuilds (SDK ≥ 1.7.0)
|
|
265
|
+
const { currentStep, setCurrentStep } = useStepNavigation('step-1', {
|
|
266
|
+
persistKey: 'currentStep',
|
|
267
|
+
});
|
|
256
268
|
```
|
|
257
269
|
|
|
270
|
+
**Do NOT wrap `currentStep` in `usePersistentState`.** Two independent step-state sources create a bidirectional `useEffect` sync loop that spams `storage_write` and flashes the UI between steps. `persistKey` is the only correct way to persist the current step.
|
|
271
|
+
|
|
258
272
|
### `usePersistentState()` — Auto-saving state
|
|
259
273
|
|
|
260
274
|
**Permission:** `storage:sandbox`
|
|
@@ -268,6 +282,12 @@ const [count, setCount] = usePersistentState<number>('counter', 0);
|
|
|
268
282
|
// Automatically persists to storage on change
|
|
269
283
|
```
|
|
270
284
|
|
|
285
|
+
Use for domain data (form inputs, selections, AI results). **Do not use for `currentStep`** — use `useStepNavigation({ persistKey })` instead.
|
|
286
|
+
|
|
287
|
+
**Writes are debounced (SDK ≥ 1.8.0).** The in-memory value updates synchronously on every setter call, but the underlying `storage_write` is coalesced with a 250 ms trailing-edge debounce and a 1 s max-wait, and flushed on unmount. This makes the hook safe to call from `requestAnimationFrame`, `mousemove`, and other high-frequency handlers — bursts no longer trip the `storage_write` rate limit.
|
|
288
|
+
|
|
289
|
+
Trade-off: reading the same key via `useFiasStorage().readFile('__state/foo')` immediately after a `setValue` can see a stale value for up to ~250 ms. Read through the hook instead of bypassing it.
|
|
290
|
+
|
|
271
291
|
### `fias` — Imperative utilities
|
|
272
292
|
|
|
273
293
|
```tsx
|
|
@@ -338,6 +358,8 @@ These are hard limits enforced by the platform. Code that violates these will fa
|
|
|
338
358
|
- `storage_read`: 300/minute
|
|
339
359
|
- `storage_list`, `storage_delete`: 60/minute
|
|
340
360
|
|
|
361
|
+
A separate **runaway-loop detector** also fires if any method is called >50 times in 5 s and blocks that method for 10 s. Errors include the target for diagnostics, e.g. `Runaway loop detected: "storage_write" (path: __state/gameState) called 51 times in 5s.` — if you see this, look at the identified path and debounce the updates at the source.
|
|
362
|
+
|
|
341
363
|
### Security Rules (enforced during review)
|
|
342
364
|
|
|
343
365
|
- No `eval()`, `Function()`, `innerHTML`, or dynamic code execution
|
|
@@ -418,6 +440,84 @@ npm run submit
|
|
|
418
440
|
# First listing: 5000 credits ($50). Re-submissions: 100 credits ($1).
|
|
419
441
|
```
|
|
420
442
|
|
|
443
|
+
## Common Pitfalls
|
|
444
|
+
|
|
445
|
+
These are real bugs that have shipped from AI-generated plugins. Avoid them.
|
|
446
|
+
|
|
447
|
+
### Don't navigate from a `useEffect` that watches derived state
|
|
448
|
+
|
|
449
|
+
If a step component derives a value from context state and uses `useEffect` to redirect when that derived value is missing, the redirect can fire mid-update and yank the user away from a working screen. Common shape:
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
// ❌ BUG: redirects whenever conversations changes between renders
|
|
453
|
+
const currentConversation = conversations.find((c) => c.id === currentConversationId);
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (!currentConversation) {
|
|
457
|
+
setCurrentStep('list');
|
|
458
|
+
}
|
|
459
|
+
}, [currentConversation, setCurrentStep]); // fires on every conversations change
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`currentConversation` is a _derived_ value — its reference changes every time `conversations` changes. The effect re-runs and may redirect during a state transition (e.g., right after the user sends a message and the component is mid-update). The user reports "I sent a message and got bounced back" or "the response never showed up".
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
// ✅ FIX: gate on the *id* (which is durable) and render a loading state when
|
|
466
|
+
// the conversation can't be found yet — let the user navigate back manually
|
|
467
|
+
// instead of forcing it.
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
if (!currentConversationId) setCurrentStep('list');
|
|
470
|
+
}, [currentConversationId, setCurrentStep]);
|
|
471
|
+
|
|
472
|
+
if (!currentConversation) return <div>Loading conversation…</div>;
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Use functional updaters when an `await` sits between reads and writes
|
|
476
|
+
|
|
477
|
+
When you read state, `await` something, then write back, the closure captures the _stale_ state. Use the functional form so the setter receives the latest value at the time of the update:
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
// ❌ BUG: `messages` here is the snapshot from before await
|
|
481
|
+
const messages = currentConversation.messages;
|
|
482
|
+
const response = await invoke({ entityId, input });
|
|
483
|
+
setConversations(
|
|
484
|
+
conversations.map((c) => (c.id === id ? { ...c, messages: [...messages, response] } : c)),
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// ✅ FIX: functional updater reads fresh state at update time
|
|
488
|
+
await invoke({ entityId, input });
|
|
489
|
+
setConversations((prev) =>
|
|
490
|
+
prev.map((c) => (c.id === id ? { ...c, messages: [...c.messages, response] } : c)),
|
|
491
|
+
);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
This matters most for `usePersistentState` on lists: the user can fire many updates quickly, and lost-update bugs are common with snapshot-style updates.
|
|
495
|
+
|
|
496
|
+
### Render `streamingText` OR `result.output` — never both
|
|
497
|
+
|
|
498
|
+
Already covered in the `useEntityInvocation` section above, but worth repeating: `streamingText` is _not_ cleared after the call completes — it keeps holding the full final text. Showing `{streamingText && ...}` AND `{result && ...}` in separate JSX nodes prints the response twice as soon as `result` arrives. Use one slot: `{isLoading ? streamingText : result?.output}`.
|
|
499
|
+
|
|
500
|
+
### Verify visually when the user reports a UI bug
|
|
501
|
+
|
|
502
|
+
If a user says "I don't see the response" or "the layout's broken", don't rely solely on `console.log` and `useFiasStorage` reads. Inspect the actual rendered output. The platform exposes `get_preview_screenshot` to AI builders for exactly this reason.
|
|
503
|
+
|
|
504
|
+
### Don't persist per-frame state
|
|
505
|
+
|
|
506
|
+
For state that updates every frame (game position, drag coordinates, animated values), use plain `useState` — not `usePersistentState`. Persisting every ball-position tick writes unbounded data to durable storage and doesn't survive reloads in any useful way anyway (the ball will be somewhere different when the player resumes).
|
|
507
|
+
|
|
508
|
+
Hook-level debouncing (SDK ≥ 1.8.0) makes `usePersistentState` safe under bursts, but you still shouldn't persist what you'll discard on reload. Keep ephemeral state in `useState` and persist only meaningful checkpoints (final score, selected difficulty, high-score list).
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
// ❌ Persisting ball position every frame — meaningless on reload
|
|
512
|
+
const [gameState, setGameState] = usePersistentState<GameState>('gameState', initial);
|
|
513
|
+
requestAnimationFrame(() => setGameState(advance(gameState)));
|
|
514
|
+
|
|
515
|
+
// ✅ Ephemeral for live gameplay, persisted for results
|
|
516
|
+
const [gameState, setGameState] = useState<GameState>(initial);
|
|
517
|
+
const [highScores, setHighScores] = usePersistentState<Score[]>('highScores', []);
|
|
518
|
+
// persist highScores only when a run completes
|
|
519
|
+
```
|
|
520
|
+
|
|
421
521
|
## Common Patterns
|
|
422
522
|
|
|
423
523
|
### Theme-Aware Card Component
|
|
@@ -183,13 +183,17 @@ function AISummarizer() {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// streamingText holds the accumulated tokens during the call AND continues to
|
|
187
|
+
// hold the full text after the call completes — it is NOT cleared. result.output
|
|
188
|
+
// also contains the full text once invoke() resolves. Render ONE slot only —
|
|
189
|
+
// streamingText while loading, result.output afterwards. Showing both renders
|
|
190
|
+
// the response twice.
|
|
186
191
|
return (
|
|
187
192
|
<div>
|
|
188
193
|
<button onClick={() => summarize('...')} disabled={isLoading}>
|
|
189
194
|
Summarize
|
|
190
195
|
</button>
|
|
191
|
-
{isLoading
|
|
192
|
-
{result && <p>{result.output}</p>}
|
|
196
|
+
<p>{isLoading ? streamingText : result?.output}</p>
|
|
193
197
|
{error && <p>Error: {error.message}</p>}
|
|
194
198
|
</div>
|
|
195
199
|
);
|
|
@@ -249,12 +253,22 @@ navigateTo('/settings');
|
|
|
249
253
|
|
|
250
254
|
**Returns:** `StepNavigationApi`
|
|
251
255
|
|
|
256
|
+
Call **once** in the top-level App component. Share `currentStep` and `setCurrentStep` with step components via React context — do not call `useStepNavigation` from step components (each call creates an independent state).
|
|
257
|
+
|
|
252
258
|
```tsx
|
|
253
259
|
import { useStepNavigation } from '@fias/arche-sdk';
|
|
254
260
|
|
|
261
|
+
// Without persistence — currentStep resets on preview rebuild
|
|
255
262
|
const { currentStep, setCurrentStep } = useStepNavigation('step-1');
|
|
263
|
+
|
|
264
|
+
// With persistence — currentStep survives preview rebuilds (SDK ≥ 1.7.0)
|
|
265
|
+
const { currentStep, setCurrentStep } = useStepNavigation('step-1', {
|
|
266
|
+
persistKey: 'currentStep',
|
|
267
|
+
});
|
|
256
268
|
```
|
|
257
269
|
|
|
270
|
+
**Do NOT wrap `currentStep` in `usePersistentState`.** Two independent step-state sources create a bidirectional `useEffect` sync loop that spams `storage_write` and flashes the UI between steps. `persistKey` is the only correct way to persist the current step.
|
|
271
|
+
|
|
258
272
|
### `usePersistentState()` — Auto-saving state
|
|
259
273
|
|
|
260
274
|
**Permission:** `storage:sandbox`
|
|
@@ -268,6 +282,12 @@ const [count, setCount] = usePersistentState<number>('counter', 0);
|
|
|
268
282
|
// Automatically persists to storage on change
|
|
269
283
|
```
|
|
270
284
|
|
|
285
|
+
Use for domain data (form inputs, selections, AI results). **Do not use for `currentStep`** — use `useStepNavigation({ persistKey })` instead.
|
|
286
|
+
|
|
287
|
+
**Writes are debounced (SDK ≥ 1.8.0).** The in-memory value updates synchronously on every setter call, but the underlying `storage_write` is coalesced with a 250 ms trailing-edge debounce and a 1 s max-wait, and flushed on unmount. This makes the hook safe to call from `requestAnimationFrame`, `mousemove`, and other high-frequency handlers — bursts no longer trip the `storage_write` rate limit.
|
|
288
|
+
|
|
289
|
+
Trade-off: reading the same key via `useFiasStorage().readFile('__state/foo')` immediately after a `setValue` can see a stale value for up to ~250 ms. Read through the hook instead of bypassing it.
|
|
290
|
+
|
|
271
291
|
### `fias` — Imperative utilities
|
|
272
292
|
|
|
273
293
|
```tsx
|
|
@@ -338,6 +358,8 @@ These are hard limits enforced by the platform. Code that violates these will fa
|
|
|
338
358
|
- `storage_read`: 300/minute
|
|
339
359
|
- `storage_list`, `storage_delete`: 60/minute
|
|
340
360
|
|
|
361
|
+
A separate **runaway-loop detector** also fires if any method is called >50 times in 5 s and blocks that method for 10 s. Errors include the target for diagnostics, e.g. `Runaway loop detected: "storage_write" (path: __state/gameState) called 51 times in 5s.` — if you see this, look at the identified path and debounce the updates at the source.
|
|
362
|
+
|
|
341
363
|
### Security Rules (enforced during review)
|
|
342
364
|
|
|
343
365
|
- No `eval()`, `Function()`, `innerHTML`, or dynamic code execution
|
|
@@ -418,6 +440,84 @@ npm run submit
|
|
|
418
440
|
# First listing: 5000 credits ($50). Re-submissions: 100 credits ($1).
|
|
419
441
|
```
|
|
420
442
|
|
|
443
|
+
## Common Pitfalls
|
|
444
|
+
|
|
445
|
+
These are real bugs that have shipped from AI-generated plugins. Avoid them.
|
|
446
|
+
|
|
447
|
+
### Don't navigate from a `useEffect` that watches derived state
|
|
448
|
+
|
|
449
|
+
If a step component derives a value from context state and uses `useEffect` to redirect when that derived value is missing, the redirect can fire mid-update and yank the user away from a working screen. Common shape:
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
// ❌ BUG: redirects whenever conversations changes between renders
|
|
453
|
+
const currentConversation = conversations.find((c) => c.id === currentConversationId);
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (!currentConversation) {
|
|
457
|
+
setCurrentStep('list');
|
|
458
|
+
}
|
|
459
|
+
}, [currentConversation, setCurrentStep]); // fires on every conversations change
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`currentConversation` is a _derived_ value — its reference changes every time `conversations` changes. The effect re-runs and may redirect during a state transition (e.g., right after the user sends a message and the component is mid-update). The user reports "I sent a message and got bounced back" or "the response never showed up".
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
// ✅ FIX: gate on the *id* (which is durable) and render a loading state when
|
|
466
|
+
// the conversation can't be found yet — let the user navigate back manually
|
|
467
|
+
// instead of forcing it.
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
if (!currentConversationId) setCurrentStep('list');
|
|
470
|
+
}, [currentConversationId, setCurrentStep]);
|
|
471
|
+
|
|
472
|
+
if (!currentConversation) return <div>Loading conversation…</div>;
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Use functional updaters when an `await` sits between reads and writes
|
|
476
|
+
|
|
477
|
+
When you read state, `await` something, then write back, the closure captures the _stale_ state. Use the functional form so the setter receives the latest value at the time of the update:
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
// ❌ BUG: `messages` here is the snapshot from before await
|
|
481
|
+
const messages = currentConversation.messages;
|
|
482
|
+
const response = await invoke({ entityId, input });
|
|
483
|
+
setConversations(
|
|
484
|
+
conversations.map((c) => (c.id === id ? { ...c, messages: [...messages, response] } : c)),
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// ✅ FIX: functional updater reads fresh state at update time
|
|
488
|
+
await invoke({ entityId, input });
|
|
489
|
+
setConversations((prev) =>
|
|
490
|
+
prev.map((c) => (c.id === id ? { ...c, messages: [...c.messages, response] } : c)),
|
|
491
|
+
);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
This matters most for `usePersistentState` on lists: the user can fire many updates quickly, and lost-update bugs are common with snapshot-style updates.
|
|
495
|
+
|
|
496
|
+
### Render `streamingText` OR `result.output` — never both
|
|
497
|
+
|
|
498
|
+
Already covered in the `useEntityInvocation` section above, but worth repeating: `streamingText` is _not_ cleared after the call completes — it keeps holding the full final text. Showing `{streamingText && ...}` AND `{result && ...}` in separate JSX nodes prints the response twice as soon as `result` arrives. Use one slot: `{isLoading ? streamingText : result?.output}`.
|
|
499
|
+
|
|
500
|
+
### Verify visually when the user reports a UI bug
|
|
501
|
+
|
|
502
|
+
If a user says "I don't see the response" or "the layout's broken", don't rely solely on `console.log` and `useFiasStorage` reads. Inspect the actual rendered output. The platform exposes `get_preview_screenshot` to AI builders for exactly this reason.
|
|
503
|
+
|
|
504
|
+
### Don't persist per-frame state
|
|
505
|
+
|
|
506
|
+
For state that updates every frame (game position, drag coordinates, animated values), use plain `useState` — not `usePersistentState`. Persisting every ball-position tick writes unbounded data to durable storage and doesn't survive reloads in any useful way anyway (the ball will be somewhere different when the player resumes).
|
|
507
|
+
|
|
508
|
+
Hook-level debouncing (SDK ≥ 1.8.0) makes `usePersistentState` safe under bursts, but you still shouldn't persist what you'll discard on reload. Keep ephemeral state in `useState` and persist only meaningful checkpoints (final score, selected difficulty, high-score list).
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
// ❌ Persisting ball position every frame — meaningless on reload
|
|
512
|
+
const [gameState, setGameState] = usePersistentState<GameState>('gameState', initial);
|
|
513
|
+
requestAnimationFrame(() => setGameState(advance(gameState)));
|
|
514
|
+
|
|
515
|
+
// ✅ Ephemeral for live gameplay, persisted for results
|
|
516
|
+
const [gameState, setGameState] = useState<GameState>(initial);
|
|
517
|
+
const [highScores, setHighScores] = usePersistentState<Score[]>('highScores', []);
|
|
518
|
+
// persist highScores only when a run completes
|
|
519
|
+
```
|
|
520
|
+
|
|
421
521
|
## Common Patterns
|
|
422
522
|
|
|
423
523
|
### Theme-Aware Card Component
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Multi-Step FIAS Plugin Template
|
|
2
|
+
|
|
3
|
+
Canonical pattern for multi-step plugins. The important bits live in:
|
|
4
|
+
|
|
5
|
+
- `src/App.tsx` — calls `useStepNavigation('step-one', { persistKey: 'currentStep' })` **once**. Single source of truth for the current step.
|
|
6
|
+
- `src/context/AppContext.tsx` — exposes `currentStep` and `setCurrentStep` to step components, plus any domain data via `usePersistentState`.
|
|
7
|
+
- `src/steps/<step-id>/<StepName>.tsx` — step components read/write via context. They do not call `useStepNavigation` themselves.
|
|
8
|
+
|
|
9
|
+
## Why not `usePersistentState('currentStep', ...)`?
|
|
10
|
+
|
|
11
|
+
That used to look tempting, but it creates two independent step-state sources (one from `useStepNavigation`, one from `usePersistentState`). Any `useEffect` that syncs them turns into a bidirectional ping-pong loop that spams `storage_write` and flashes the UI between steps.
|
|
12
|
+
|
|
13
|
+
Since SDK 1.7.0, `useStepNavigation` handles persistence directly via `{ persistKey }`. That is the only correct way to persist step state. Domain data (form inputs, AI-generated content, user selections) can still use `usePersistentState` — just not `currentStep`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A multi-step FIAS plugin arche",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"archeType": "tool",
|
|
7
|
+
"tags": [],
|
|
8
|
+
"pricing": { "model": "free", "currency": "usd" },
|
|
9
|
+
"permissions": ["theme:read", "storage:sandbox"],
|
|
10
|
+
"sdk": "^1.7.0"
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>FIAS Plugin</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/index.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "vite & sleep 2 && fias-dev",
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"validate": "tsc --noEmit",
|
|
10
|
+
"dev:harness": "fias-dev",
|
|
11
|
+
"submit": "fias-dev submit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@fias/arche-sdk": "^1.7.0",
|
|
15
|
+
"react": "^19.0.0",
|
|
16
|
+
"react-dom": "^19.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@fias/plugin-dev-harness": "^1.0.0",
|
|
20
|
+
"@types/react": "^19.0.0",
|
|
21
|
+
"@types/react-dom": "^19.0.0",
|
|
22
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
23
|
+
"typescript": "^5.3.3",
|
|
24
|
+
"vite": "^6.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useFiasTheme, useStepNavigation } from '@fias/arche-sdk';
|
|
2
|
+
import { AppProvider } from './context/AppContext';
|
|
3
|
+
import { StepOne } from './steps/step-one/StepOne';
|
|
4
|
+
import { StepTwo } from './steps/step-two/StepTwo';
|
|
5
|
+
|
|
6
|
+
function AppContent() {
|
|
7
|
+
const theme = useFiasTheme();
|
|
8
|
+
|
|
9
|
+
// Single source of truth for step navigation.
|
|
10
|
+
// persistKey makes currentStep survive preview rebuilds via bridge storage.
|
|
11
|
+
// Do NOT also wrap currentStep in usePersistentState — two sources create
|
|
12
|
+
// a bidirectional useEffect sync loop that spams storage and flashes the UI.
|
|
13
|
+
const { currentStep, setCurrentStep } = useStepNavigation('step-one', {
|
|
14
|
+
persistKey: 'currentStep',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (!theme) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<AppProvider currentStep={currentStep} setCurrentStep={setCurrentStep}>
|
|
21
|
+
<div
|
|
22
|
+
style={{
|
|
23
|
+
minHeight: '100vh',
|
|
24
|
+
backgroundColor: theme.colors.background,
|
|
25
|
+
color: theme.colors.text,
|
|
26
|
+
fontFamily: theme.fonts.body,
|
|
27
|
+
padding: theme.spacing.lg,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{currentStep === 'step-two' ? <StepTwo /> : <StepOne />}
|
|
31
|
+
</div>
|
|
32
|
+
</AppProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function App() {
|
|
37
|
+
return <AppContent />;
|
|
38
|
+
}
|