fix: guard against malformed stored settings (crash → blank window); add ErrorBoundary to surface render errors

This commit is contained in:
Hermes Agent committed 2026-09-19 16:36:24 +02:00
1 parent bfd3638093
commit 895b254c2a
3 files changed
+36 -7

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "figma-ai-plugin",
"version": "0.1.3",
"version": "0.1.4",
"private": true,
"dependencies": {
"@create-figma-plugin/ui": "^4.0.3",
+9 -3
View File
@@ -52,12 +52,18 @@ export function createApinexProvider (): ProviderConfig {
}
export async function loadSettings (): Promise<Settings> {
const defaults = defaultSettings()
try {
const stored = await figma.clientStorage.getAsync(SETTINGS_KEY)
if (stored == null || stored.version !== 1) return defaultSettings()
return stored as Settings
if (stored == null || stored.version !== 1) return defaults
const merged: Settings = { ...defaults, ...(stored as Settings) }
if (!Array.isArray(merged.providers) || merged.providers.length === 0) {
merged.providers = defaults.providers
merged.activeProviderId = defaults.providers[0].id
}
return merged
} catch (e) {
return defaultSettings()
return defaults
}
}
+26 -3
View File
@@ -18,7 +18,7 @@ import {
IconComponent24
} from '@create-figma-plugin/ui'
import { emit, on } from '@create-figma-plugin/utilities'
import { h, Fragment } from 'preact'
import { h, Fragment, Component } from 'preact'
import { useState, useEffect, useRef } from 'preact/hooks'
import MenuItem from './MenuItem'
import { type Settings, type ProviderConfig, defaultSettings, loadSettings, saveSettings, settingsToJSON, settingsFromJSON, APINEX_BASE_URL } from './settings'
@@ -29,7 +29,7 @@ const MIN_WINDOW_SIZE = { width: 320, height: 200 }
// very high — actual clamp is done by main via clampWindowSize (bounds*zoom).
// Full plugin version shown in the footer status bar. Keep in sync with package.json "version".
const PLUGIN_VERSION = '0.1.3'
const PLUGIN_VERSION = '0.1.4'
const fieldStyle = {
display: 'flex',
@@ -55,6 +55,26 @@ function Separator () {
)
}
class ErrorBoundary extends Component<{ children: any }, { error: Error | null }> {
constructor (props: { children: any }) {
super(props)
this.state = { error: null }
}
componentDidCatch (error: Error) {
this.setState({ error: error })
}
render (props: { children: any }, state: { error: Error | null }) {
if (state.error != null) {
return (
<pre style={{ margin: 0, padding: 8, color: 'var(--figma-color-text-danger)', whiteSpace: 'pre-wrap', fontSize: '10px', overflow: 'auto' }}>
{String(state.error && state.error.stack ? state.error.stack : state.error)}
</pre>
)
}
return props.children
}
}
function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) => void }) {
const { settings, setSettings } = props
const [showKey, setShowKey] = useState(false)
@@ -65,7 +85,8 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings)
settingsRef.current = settings
// Static single provider (APInex). Always edit providers[0].
const provider = settings.providers[0] != null ? settings.providers[0] : null
const providers = Array.isArray(settings.providers) ? settings.providers : []
const provider = providers.length > 0 ? providers[0] : null
useEffect(function () {
const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string }) {
@@ -488,6 +509,7 @@ function Plugin () {
return (
<div className="plugin-root" style={{ position: 'relative', width: '100vw', height: '100vh' }}>
<style>{'html,body{height:100%;overflow:hidden}'}</style>
<ErrorBoundary>
<Container space="medium" style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: 0 }}>
{/* Header — navigation bar */}
<header style={{ display: 'flex', alignItems: 'center', flex: '0 0 auto', padding: 'var(--space-8)' }}>
@@ -523,6 +545,7 @@ function Plugin () {
<Muted style={{ fontSize: '11px' }}>Version {PLUGIN_VERSION}</Muted>
</footer>
</Container>
</ErrorBoundary>
{/* More-menu modal — old-plugin style: library Modal + Container + Stack of MenuItem */}
<Modal open={moreOpen} onClose={function () { setMoreOpen(false) }} onOverlayClick={function () { setMoreOpen(false) }}>
<Container space="extraSmall">