Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
95888b9
feat(frontend): IndexedDB per-query persistence for playground workfl…
ardaerzin Jul 18, 2026
9bfd2df
perf(frontend): cut cold-load head-of-line and eager revisions-list f…
ardaerzin Jul 18, 2026
0d7d6ba
perf(frontend): remove redundant playground cold-load requests (HAR-d…
ardaerzin Jul 18, 2026
b3a3bcc
fix(frontend): inspect persist key rotated per reload; session record…
ardaerzin Jul 19, 2026
617b351
perf(frontend): persist trace summaries, gate billing on config, fix …
ardaerzin Jul 19, 2026
f9acb0e
docs(design): record HAR rounds 2-3 outcomes in playground-query-pers…
ardaerzin Jul 19, 2026
f5ebc54
perf(frontend): secrets/records paint-from-disk, sidebar list deferrals
ardaerzin Jul 19, 2026
cf34851
docs(design): record increment-4 round in playground-query-persistenc…
ardaerzin Jul 19, 2026
76084c3
perf(frontend): evict the monolithic Fern client from _app again (-22…
ardaerzin Jul 19, 2026
63c63ba
docs(design): app boot analysis - serial gate chain + un-preloaded pl…
ardaerzin Jul 19, 2026
8580515
perf(frontend): warm the serialized boot chunks (playground, layout, …
ardaerzin Jul 19, 2026
8c9df1e
docs(design): correct playground graph size (stale-manifest 10MB -> 3…
ardaerzin Jul 19, 2026
b7a7a4c
perf(frontend): name anonymous forwardRefs; skip tooltip wrapper on p…
ardaerzin Jul 19, 2026
8bf61c7
perf(frontend): tier-2 boot re-render hygiene (profiler-confirmed amp…
ardaerzin Jul 19, 2026
229e952
docs(design): mark tier-2 + profiler extras built in app-boot plan
ardaerzin Jul 19, 2026
ca0449f
perf(frontend): latch the protected-route gate, boot shell, prewarm b…
ardaerzin Jul 19, 2026
bed6d66
perf(frontend): config-accordion render hygiene (profiler-driven)
ardaerzin Jul 19, 2026
fad37e2
docs(design): mark T1.3 + accordion hygiene built in app-boot plan
ardaerzin Jul 19, 2026
b6fa895
perf(frontend): fix warm-reload list race, prewarm current-workflow d…
ardaerzin Jul 20, 2026
99a0711
docs(design): record iteration-3 findings (parse-bound dead zone, lis…
ardaerzin Jul 20, 2026
44e5135
docs(design): playground render-gate ladder audit (bottom-up, with re…
ardaerzin Jul 20, 2026
1c40c34
perf(frontend): relax playground render gates for disk-served warm re…
ardaerzin Jul 20, 2026
0aaf921
perf(frontend): cut session-UI boot render cost (rail latch, hover-ga…
ardaerzin Jul 20, 2026
f62f27d
perf(frontend): dedup + stop cancelling session/workflow boot queries
ardaerzin Jul 20, 2026
622697e
perf(frontend): persist latest-revision query to IndexedDB (paint-fro…
ardaerzin Jul 20, 2026
58e9b3a
test(frontend): pin isFetched-under-restore semantics (Phase 2 gate)
ardaerzin Jul 20, 2026
b028a1c
fix(frontend): remove the persist kill switch — persistence is always on
ardaerzin Jul 20, 2026
d9f6439
docs(design): branch summary for the data & render optimizations work
ardaerzin Jul 20, 2026
ad69fb8
fix(frontend): align agent config skeletons with the real section layout
ardaerzin Jul 20, 2026
2ea9b3e
fix(frontend): keep session hover actions mounted while keyboard-focused
ardaerzin Jul 21, 2026
bc94e92
refactor(frontend): store-scope currentWorkflowContext memoization
ardaerzin Jul 21, 2026
9bfbfb1
fix(frontend): fail-safe vault secret redaction
ardaerzin Jul 21, 2026
238a395
fix(frontend): clear persisted cache on every sign-out path
ardaerzin Jul 21, 2026
59a47cc
Merge remote-tracking branch 'origin/release/v0.105.8' into HEAD
ardaerzin Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/design/app-boot-optimizations/gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Playground render-gate ladder (bottom-up audit, 2026-07-20)

Why: on warm reloads the config panel showed loading states even though its data is
IndexedDB-restorable. Central mechanic: **persister restores are asynchronous** — a
disk-served query still passes through `isPending: true` (and `data === undefined`)
for a frame or more, so any `isPending → skeleton` gate treats instant-disk data like
a cold network fetch. `enabled: false` queries report `isPending: true` forever
(drafts — but see the draft analysis: the molecule's `selectors.query` wrapper
special-cases drafts to `isPending: false`, molecule.ts:164-172, so panel gates never
hit that trap directly).

Legend: [SAFE-RELAX] = gate can key on data presence / synchronous signal;
[RESTRUCTURE] = relaxable with reordering; [KEEP] = correctness latch, leave alone.

## Level 1 — config-section internals (entity-ui)

| Gate | Condition | Verdict |
|---|---|---|
| PlaygroundConfigSection ~1888 config skeleton | `schemaQuery.isPending && !hasRenderableConfigSections(activeData)` | **[KEEP — audit verdict revised]** — dropping `isPending` makes the "No configuration needed" empty state paint during every restore window (data absent + pending is a real loading state, not a false gate); the skeleton already clears the moment restored data lands |
| PCS ~695 revision-switch latch | keep previous render until target data or settled | [KEEP] — swap-correctness latch, prefers data-presence already |
| PCS ~1906 "No configuration needed" | data-null empty state | [RESTRUCTURE] — reached wrongly for drafts pre-hydration; held off by gate 2.1 |
| SchemaPropertyRenderer ~448 Suspense(AgentTemplateControl) | lazy chunk import | **[SAFE-RELAX → RELAXED]** — preload on the synchronous early-agent signal instead of idle |
| useModelHarness vault/capabilities gates | vault + inspect catalogs | [KEEP] — gate a warning / pick a control variant; never hold the body |

## Level 2 — config panel host (PlaygroundVariantConfig)

| Gate | Condition | Verdict |
|---|---|---|
| ~281 `hasPendingHydration` skeleton | pending URL-snapshot hydration; clears when the SOURCE revision query is `!isPending && data` (playground.ts ~900-913) | **[RESTRUCTURE]** — the draft warm-reload skeleton. Must hold (removing it falls through to the wrong "No configuration needed" state), but should resolve off disk-present source *data*; `workflowLocalServerDataAtomFamily` (store.ts ~2361) is in-memory-only and could be reseeded synchronously |
| ~99 `isAgentHeaderMode` incl. `variantQueryPending` disjunct | agent-vs-prompt chrome | **[SAFE-RELAX → RELAXED]** — `earlyAgentState` covers the persisted case synchronously; the pending disjunct mislabels prompt apps as agent for a tick |
| ~341 agent operations skeleton | `hasPendingHydration \|\| (!isAgent && earlyAgentState!=="agent")` | [RESTRUCTURE] — tied to 2.1 |

## Level 3 — MainLayout

| Gate | Condition | Verdict |
|---|---|---|
| ~402 `configEntityIds.length === 0` placeholder | selection not yet applied | **[RESTRUCTURE]** — persisted selection is synchronously readable but applied in `playgroundSyncAtom.onMount` (post-first-commit) → one-frame placeholder |
| ~299 EmptyState (`status === "empty"`) | initialized + empty | [KEEP] — correctly distinct from the idle frame |
| ~519 AgentChatSkeleton | `isAgentConfig && singleEntityQuery.isPending` | **[SAFE-RELAX → RELAXED]** — key on data presence |
| ~183 / ~222 agent host + config latches | isPending-conservative latches | [KEEP] — protect live chat / splitter geometry across swaps |
| ~497 GenerationPanelPlaceholder | same empty-selection frame as 3.1 | [RESTRUCTURE] — same fix |

## Level 4 — Playground root

`playgroundSyncAtom` mounts the sync engine in `onMount` — one commit too late for the
synchronous selection restore ([RESTRUCTURE], the root cause of the Level-3 frames).
Onboarding loader is flag-gated ([KEEP]). No other config-subtree gates.

## Level 5 — PlaygroundRouter

Chunk `loading` shells are warmed (brief). The evaluator-vs-app branch resolves from
the now-persisted + prewarmed detail query ([SAFE-RELAX] residual: avoid an
`isPending`-shaped shell frame; largely mitigated).

## Levels 6-8 — ProtectedRoute → Layout → `_app`

Documented in plan.md (boot model): latched ready-gate + BootShell, warmed chunks,
de-async'd auth init, prewarmed boot query graph. Ready-atom has zero network deps on
warm workspace routes; remaining wait is main-thread parse.

## Draft warm-reload chain (the reported symptom)

Draft selected → `workflowLocalServerDataAtomFamily` is in-memory-null after reload →
panel data null → gate 2.1 holds the skeleton pending hydration → hydration waits for
the SOURCE revision's query to be `!isPending && data` → that query's `initialData`
reads the in-memory detail cache (empty on reload) and fills from the persister
asynchronously. Net: the skeleton's length = source-revision disk-restore latency,
even though the data is on disk. Fix path: hydration resolves on disk-present data +
synchronous reseed of the local-server-data atom (+ gate 1.1 relax so restored
parameters render immediately).

## Status

Built in this round: 1.4 (immediate preload on the early-agent signal), 2.1 (hydration
applies on source-data presence; the `!isPending` term dropped), 2.2/2.4 (agent chrome
keyed on the synchronous early-agent signal, `variantQueryPending` disjunct removed),
3.1/3.6 (module-eval URL sync in `state/url/playground.ts` seeds the selection before
the first React commit), 3.3 (skeleton only when pending AND no data). NOT relaxed:
1.1 — verdict revised to KEEP (see the Level-1 table); with 2.1 + 3.1 the restored data
is present by/near first commit, so the remaining skeleton time equals the IDB restore
latency, which is the correct floor. 2.1's local-server-data reseed idea is moot: the
draft body cannot exist before its source body (IDB reads are async, drafts are
reconstructed from patches by design), and compare-mode clones are already seeded at
hydration-apply time — now the earliest possible moment.
174 changes: 174 additions & 0 deletions docs/design/app-boot-optimizations/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# App boot optimizations — collapsing the serial gate chain

Status: ANALYSIS + PLAN (2026-07-19) — T1.1 + T1.2 BUILT (e423d17708, warmup variant
for T1.2 preserving the recipe code-split). T2 BUILT (33e06521eb — all four items;
ProtectedRoute deviation: its five hooks are side-effectful mounts, moved to a memo'd
null island rather than an atom; snapshot `timestamp` dropped entirely, zero consumers).
T1.3 BUILT (c14e768072 — latch `raw || (latch && sessionExists)`, BootShell sidebar
ghost pre-ready, prewarmBootQueryGraph() overlapping the config-chunk fetch; the 157ms
"ProtectedRoute first read" was the boot query-atom graph's initial evaluation).
Config-accordion hygiene BUILT (620054a6c6 — equality-stable adapter atoms,
narrowed PlaygroundVariantConfig subscriptions, stable DrillInUIContext value; the
accordion agent was cut off by a session limit and two type fixes landed at
integration). Iteration 3 (1a8b541815): warm-reload list race fixed (bindRevisionsReady mounted the
list sub before tryApplyDefaults restored the persisted selection); current-workflow
detail + latest-revision prewarmed in the profile burst (registerBootPrewarmTask);
org detail un-gated from /profile (jwtReady, mirrors projects); workflow detail
persisted (nothing invalidates the key — audited). SETTLED: the 532→1061ms network
dead zone is main-thread PARSE of the warmed chunk graph (T1.1 tradeoff), not a data
gate; protectedRouteReadyAtom has ZERO network deps on warm workspace routes (pure
effect timing); sessionStorage ready-latch REJECTED (renders protected content before
SuperTokens confirms). Remaining ranked: chunk-parse window (Tier-3 split or prod
minification), ready-flip→mount ~400ms (hygiene), session records pre-mount (needs
session id before mount). Remaining: T1.4 /__env.js inlining; profiler re-run.
Profiler-driven extras (25f527a016): seven anonymous forwardRefs named — the profile's
top line "ForwardRef(Anonymous)" ×594 was their aggregate, dominated by EnhancedButton —
and EnhancedButton no longer wraps tooltip-less buttons in Tooltip/Trigger (the
Tooltip ×173 / Trigger ×231 churn). T1.3/T1.4 pending; profiler re-run pending
(expect Context.Provider/Sidebar/PlaygroundHeader render counts to drop).
Scope: initial-load time of the web app (agent playground as the reference route).
Constraints: Next.js pages router stays; no Next upgrade; no app-router migration;
SuperTokens stays.
Companion: docs/design/playground-query-persistence/plan.md (the data-layer work this
plan complements — data is now largely disk-served; boot is the remaining felt cost).

## The boot model (what actually happens)

The server delivers an effectively empty shell (theme pre-paint only — the inline
`_document` script sets `.dark` before paint). Everything else is client-JS, and it
runs as a **serial chain of four null-render gates**, each of which must fully release
before the next is even discovered:

```text
HTML → [BLOCK] /__env.js (beforeInteractive, Cache-Control: no-store)
→ parse _app JS (~3.56 MB uncompressed; page-route chunk 326 KB loads in parallel — fine)
→ GATE 1 AuthProvider renders null
→ effect: await import(frontendConfig) [CHUNK ROUND-TRIP]
→ SuperTokens.init (cookie-only, no network)
→ setIsInitialized → ENTIRE provider tree mounts (double render)
→ GATE 2 Layout chunk (dynamic ssr:false, ~336 KB) [SEQUENTIAL CHUNK ROUND-TRIP, no preload]
→ GATE 3 ProtectedRoute renders null until protectedRouteReadyAtom
(session + profile + project + org resolution; the atom FLIPS MULTIPLE
TIMES during auth resolution — each flip unmounts/remounts the whole
page subtree)
→ GATE 4 Playground chunk (dynamic ssr:false, ~3.2 MB uncompressed, 63 files (an earlier stale manifest overstated this as 10 MB))
[SEQUENTIAL CHUNK ROUND-TRIP, ZERO preload/warmup anywhere]
→ data gates (revision/inspect/records — now largely IndexedDB-served)
```

Key facts with evidence:

- **The Playground graph (~3.2 MB) has no warmup.** It is discovered only after Gates 1–3
release. The only prefetch in the codebase (`VariantsComponents/index.tsx:125`)
warms the 326 KB page-route chunk, not this lazy leaf. `preloadEditorPlugins` runs
*after* Playground mounts.
- **Gate 1 is self-inflicted latency**: the session check is cookie-only; the gate's
cost is an async chunk fetch (`frontendConfig`) plus an effect tick, serialized
before *everything* (state, theme, Layout, queries all wait).
- **Gate 3 is unstable**: `state/url/auth.ts` sets `protectedRouteReadyAtom` false at
6 sites and true at 3 during boot; `ProtectedRoute` swaps `null ↔ children`, so each
early flip can unmount the page subtree mid-boot (and re-trigger the Gate-4 dynamic
mount). It also subscribes 5 boot-volatile sources directly.
- **`/__env.js`** is `beforeInteractive` + `no-store`: a render-blocking, never-cached
script fetch on every single load.
- **Healthy parts (leave alone):** playground URL rewrites use `history.replaceState`
and bypass all router events — zero `_app` churn during editing; `PreloadQueries` is
a correctly isolated warmup leaf; the theme pre-paint prevents wrong-theme flash;
`appStateSnapshotAtom` writes are signature-deduped to real navigations.

### Re-render amplifiers (ranked by breadth × flip-count)

1. `AppWithVariants` subscribes the whole `appStateSnapshotAtom` (`Layout.tsx:174`);
every navigation writes a fresh object (with `timestamp: Date.now()`) →
the widest subtree re-renders per navigation. Inline antd `ConfigProvider`
`theme={{algorithm…}}` object literals at `Layout.tsx:299-306`/`:332-338` compound
it (cssinjs re-eval).
2. `ThemeContext.Provider value={{…}}` is an inline object (`ThemeContextProvider.tsx:247-253`)
→ every provider re-render fans out to all `useAppTheme` consumers (Layout, Sidebar,
ThemeContextBridge). `ThemeContextBridge` additionally rebuilds `{...token, isDark}`
per render.
3. Gate-3 flips (above) — mount/unmount is the most expensive re-render there is.
4. Sidebar: `memo`'d island defeated by internal subscriptions (`useRouter()`, theme
context, session/org/currentApp flips) → ~5+ full menu recomputes during boot.
5. `useSession` writes session atoms in effects; each boot flip cascades to every
session-gated consumer.
6. `currentWorkflowContextAtom` returns a fresh object per query phase (no selectAtom)
→ PlaygroundRouter re-renders per transition; a `workflowKind` change remounts
Playground.
7. PlaygroundHeader/MainLayout inline `useMemo(() => atom(...))` atoms re-subscribe
when input identities change.
8. `AppGlobalWrappers` reconciles ~15 null dynamic children per router event (cheap,
bounded — low priority).

## The plan (ranked; measure between tiers)

### Tier 1 — collapse the serial chain (structural, low-risk, highest impact)

**T1.1 Warm the Playground chunk immediately.** Hoist the `import()` thunk
(`const load = () => import("../Playground/Playground")`), share it with `dynamic()`,
and invoke it at PlaygroundRouter module-eval (or first idle) — the ~3.2 MB download+parse
then runs IN PARALLEL with Gates 1–3 instead of after them. Same pattern for the Layout
chunk from `_app`. This is the single biggest structural win: it converts the two
sequential chunk round-trips into parallel work behind the auth/data gates.

**T1.2 De-async Gate 1.** Import `frontendConfig` statically in AuthProvider (its
recipe deps — supertokens-auth-react — are already in the `_app` vendor bundle, so the
chunk split buys ~nothing) and call `SuperTokens.init` at module scope (it is
synchronous). Gate 1 then collapses to ~zero and the double mount of the entire
provider tree disappears. Verify: no SSR pitfalls (init guarded by `typeof window`),
`fromSupertokens === "needs-refresh"` path preserved.

**T1.3 Latch Gate 3.** `protectedRouteReadyAtom`: once true, stay true for the session
(reset only on real sign-out), so mid-boot auth-resolution flips stop unmounting the
page subtree. Additionally render the page shell (skeleton) instead of `null` while
not-ready, so gate release is a fill-in rather than a mount storm. Collapse
ProtectedRoute's 5 subscriptions into one derived boolean atom.

**T1.4 Unblock `/__env.js`.** The standalone server can inline the env payload into
`_document` at request time (it already templates HTML), removing a render-blocking,
uncacheable round-trip from every load. Fallback option: keep the script but allow a
short max-age + ETag.

### Tier 2 — re-render hygiene (mechanical, low-risk)

**T2.1** Memoize the four unstable identities: ThemeContext provider value,
ThemeContextBridge token object, `AgSWRConfig` config object, both nested
`ConfigProvider theme` literals in Layout.
**T2.2** Narrow `AppWithVariants`'s snapshot subscription to the slices it renders
(selectAtom with equality), and stop stamping `timestamp: Date.now()` into the
snapshot object (or exclude it from equality).
**T2.3** `currentWorkflowContextAtom` → stable-identity selector (selectAtom or
equalityFn) so query-phase transitions don't re-render PlaygroundRouter; same
treatment for ProtectedRoute's derived ready-boolean (T1.3).
**T2.4** Sidebar: replace `useRouter()` with a pathname selector; benefits from T2.1
automatically. Hoist PlaygroundHeader's inline `useMemo(atom)` instances where inputs
churn.

### Tier 3 — flagged, not recommended now

- **Split the Playground graph (~3.2 MB)** (agent vs prompt branches; MainLayout statically
imports ExecutionItems/comparison views). Real but large refactor; T1.1 removes the
serialization pain first — re-measure before considering.
- **SSR/streaming shell** — excluded by constraints (pages router, ssr:false layers,
window-guarded Layout).
- **`_app` entity-cascade trim** (~450 KB) — previously attempted and reverted
(multi-root, registration-race risk). Unchanged verdict.

## Expected effects (directional, verify by measuring)

- T1.1: removes 10 MB of *serialized* download+parse from the critical path — in dev
(uncompressed, unminified) this is the dominant term; in prod (~gzip) still the
largest single structural win.
- T1.2: −1 chunk round-trip, −1 full-tree double mount at the very front of boot.
- T1.3: eliminates mid-boot page-subtree remounts (worst-case re-render class).
- T1.4: −1 blocking uncached request before hydration on every load.
- Tier 2: fewer/narrower re-renders during the boot window in which the main thread is
already contended with chunk parse — the wins compound with Tier 1 rather than
standing alone.

## Measurement protocol

Prod build (`next build && next start`), Performance panel: mark
(1) nav → first shell paint, (2) → PlaygroundLoadingShell, (3) → real config+chat.
Compare before/after per tier. In dev, the same ordering holds with larger absolute
numbers; use the same three marks.
Loading
Loading