From everything-evenhub
Implements background state persistence for Even Hub G2 plugins: analyzes code, identifies state for background/foreground survival, inserts setBackgroundState and onBackgroundRestore calls. Fixes resets on phone background return.
npx claudepluginhub even-realities/everything-evenhub --plugin everything-evenhubThis skill is limited to using the following tools:
Add background state persistence to an Even Hub plugin by analyzing its code and inserting `setBackgroundState` + `onBackgroundRestore` calls from `@evenrealities/even_hub_sdk`.
Integrates G2 hardware features in Even Hub apps: microphone audio capture, IMU motion data, device info, battery status, wearing detection, local storage. Use for audio processing, motion sensing, persistence.
Implements Android app lifecycle patterns for process death handling with SavedStateHandle, ViewModel restoration, rememberSaveable in Compose, and lifecycle-aware components.
Provides state management patterns for Makepad Rust apps including AppState with serde persistence, theme switching, global registries, and navigation.
Share bugs, ideas, or general feedback.
Add background state persistence to an Even Hub plugin by analyzing its code and inserting setBackgroundState + onBackgroundRestore calls from @evenrealities/even_hub_sdk.
The Even Hub host app uses a Headless WebView migration strategy when the phone goes to the background:
inactive — host snapshots current JS state via window.__getStateSnapshot()paused — host creates a new HeadlessInAppWebView, loads the same plugin URL, and calls window.__restoreState(snapshot) to replay the saved stateresumed — snapshot is injected into the foreground WebView before it wakes up, then the headless WebView is destroyedIf a plugin does not register any state exporters, __getStateSnapshot() returns '{}' and the headless WebView starts from scratch — causing the plugin to reset to its initial state every time the phone comes back from the background.
Apply this skill when a plugin:
import { setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
// Register state exporter — called by host before going to background
setBackgroundState('myKey', () => ({ ...myState }))
// Register state restorer — called by host after headless WebView loads
onBackgroundRestore('myKey', (saved) => {
const s = saved as typeof myState
myState = { ...myState, ...s }
})
Rules:
key must be the same string in both callsRecord<string, unknown> — always cast before usebridge.onEvenHubEvent)setBackgroundState before any code that modifies the state variables, so the initial snapshot is always validIf $ARGUMENTS points to a file, read it directly. If it points to a directory, glob for *.ts and *.tsx files and read the most relevant ones (typically main.ts, index.ts, or the file containing waitForEvenAppBridge).
Look for variables that:
| Pattern | Example | Snapshot? |
|---|---|---|
| Primitive counter or timer | let counter = 0 / frameIndex++ | ✅ Yes |
| Selected index or active mode | let selectedIdx = 0 / let mode: 'A'|'B' = 'A' | ✅ Yes |
| Data buffer or history array | const history: string[] = [] | ✅ Yes |
| Config set at startup | let speed = 1 then later speed = userChoice | ✅ Yes |
| Derived / computed value | const label = items[selectedIdx].name | ❌ No — re-derive after restore |
| WebView lifecycle flag | let isReady = false | ❌ No — will re-initialize |
| Bridge / controller reference | let bridge: EvenAppBridge | ❌ No — not serializable |
| Ephemeral UI flag | let isAnimating = false | ❌ No — transient |
Key heuristic: a variable is snapshot-worthy if losing it on background entry would cause the plugin to behave differently than if it had never gone to the background.
Use one key per logical state group. Avoid one key per variable (too granular) and one key for everything (too coarse). A single plugin typically needs 1–2 keys.
Examples:
'counter' for a frame counter'appState' for the main mutable state object'selection' for the currently selected item index + related dataFind the correct insertion point — after state variable declarations, before bridge.onEvenHubEvent(...). Insert:
// Background state persistence — survives host background/foreground migration
setBackgroundState('myKey', () => ({ /* snapshot of mutable state */ }))
onBackgroundRestore('myKey', (saved) => {
const s = saved as { /* type annotation */ }
// restore each field with nullish fallback
})
Also add the import if not already present:
import { setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
After inserting, confirm:
{ ...state }, not a live reference)saved)?? fallback to its current valueBefore (loses all state on background transition):
import { waitForEvenAppBridge } from '@evenrealities/even_hub_sdk'
let counter = 0
let lastEvent = ''
const bridge = await waitForEvenAppBridge()
await bridge.createStartUpPageContainer(container)
setInterval(() => {
counter++
bridge.rebuildPageContainer(buildPage(counter))
}, 500)
bridge.onEvenHubEvent(event => {
if (event.sysEvent) {
lastEvent = `sys:${event.sysEvent.eventType ?? 0}`
}
})
After (state survives background):
import { waitForEvenAppBridge, setBackgroundState, onBackgroundRestore } from '@evenrealities/even_hub_sdk'
let counter = 0
let lastEvent = ''
// Background state persistence — survives host background/foreground migration
setBackgroundState('appState', () => ({ counter, lastEvent }))
onBackgroundRestore('appState', (saved) => {
const s = saved as { counter: number; lastEvent: string }
counter = s.counter ?? counter
lastEvent = s.lastEvent ?? lastEvent
})
const bridge = await waitForEvenAppBridge()
await bridge.createStartUpPageContainer(container)
setInterval(() => {
counter++
bridge.rebuildPageContainer(buildPage(counter))
}, 500)
bridge.onEvenHubEvent(event => {
if (event.sysEvent) {
lastEvent = `sys:${event.sysEvent.eventType ?? 0}`
}
})
// ❌ Wrong — exporter returns the live object reference; value changes by the time host reads it
setBackgroundState('state', () => myState)
// ✅ Correct — spread creates a snapshot copy at the moment of export
setBackgroundState('state', () => ({ ...myState }))
// ❌ Wrong — local const, live variable unchanged
onBackgroundRestore('state', (saved) => {
const s = saved as typeof myState
console.log(s.counter) // myState.counter still 0
})
// ✅ Correct — reassign the live variable
onBackgroundRestore('state', (saved) => {
const s = saved as typeof myState
myState = { ...myState, ...s }
})
// ❌ Wrong — Map and Date do not survive JSON round-trip
setBackgroundState('state', () => ({
items: new Map([['a', 1]]),
timestamp: new Date(),
}))
// ✅ Correct — convert to plain types
setBackgroundState('state', () => ({
items: Object.fromEntries(itemsMap),
timestamp: Date.now(),
}))
// ❌ Wrong — crashes or resets to 0 if field missing in snapshot
onBackgroundRestore('state', (saved) => {
const s = saved as { counter: number }
counter = s.counter
})
// ✅ Correct — keep current value as fallback when field absent
onBackgroundRestore('state', (saved) => {
const s = saved as { counter: number }
counter = s.counter ?? counter
})
// ❌ Wrong — if this code path isn't hit before background, state is not saved
bridge.onEvenHubEvent(event => {
if (event.sysEvent?.eventType === 4) {
setBackgroundState('state', () => ({ counter })) // too late
}
})
// ✅ Correct — always register at module init time
setBackgroundState('state', () => ({ counter }))
bridge.onEvenHubEvent(event => { /* ... */ })
Analyze the plugin code at $ARGUMENTS. Identify all snapshot-worthy state variables following the table above. Then insert setBackgroundState + onBackgroundRestore at the correct location. Report a summary of: