From 79e9ec112858fd80f45c0e81e96f033e588a1bb8 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 24 Sep 2026 11:22:43 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20persist=20settings=20(API=20key)=20via?= =?UTF-8?q?=20main-thread=20RPC=20=E2=80=94=20clientStorage=20is=20unavail?= =?UTF-8?q?able=20in=20UI=20iframe,=20so=20direct=20save/load=20silently?= =?UTF-8?q?=20failed;=20bump=200.1.19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/main.ts | 12 +++++++++++ src/settings.ts | 54 +++++++++++++++++++++++++++++++------------------ src/ui.tsx | 5 +++-- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index b490ae3..02d444a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "figma-ai-plugin", - "version": "0.1.18", + "version": "0.1.19", "private": true, "dependencies": { "@create-figma-plugin/ui": "^4.0.3", diff --git a/src/main.ts b/src/main.ts index 485f624..f525190 100644 --- a/src/main.ts +++ b/src/main.ts @@ -572,6 +572,18 @@ export default function () { } }) + // Settings persistence RPC: clientStorage is only available here (main thread), not in the UI iframe. + on('LOAD_SETTINGS', async function (payload: { key: string }) { + let stored: any = null + try { stored = await figma.clientStorage.getAsync(payload.key) } catch (e) { stored = null } + emit('SETTINGS_LOADED', { settings: stored }) + }) + + on('SAVE_SETTINGS', async function (payload: { key: string, settings: any }) { + try { await figma.clientStorage.setAsync(payload.key, payload.settings) } catch (e) { /* ignore */ } + emit('SAVE_SETTINGS_DONE', {}) + }) + showUI({ width: 380, height: 480, themeColors: true, title: 'AI assistant (MVP)' }) // On open, proactively probe connectivity so the user sees network status without clicking Fetch. diff --git a/src/settings.ts b/src/settings.ts index 22ca1cf..8d16e04 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,8 +1,11 @@ // Local settings model + persistence (figma.clientStorage). // -// No server auth yet — everything lives on the Figma file locally. Backup/restore -// is just a JSON export/import of this same document. (Server auth comes later.) +// IMPORTANT: figma.clientStorage is ONLY available in the plugin MAIN thread (main.ts), +// NOT in the UI iframe. So load/save must round-trip through main via emit/on. +// These helpers only build/parse the data; the actual storage calls live in main.ts +// (SAVE_SETTINGS / LOAD_SETTINGS RPC). Backup/restore is a JSON export/import of this same model. +import { emit, on } from '@create-figma-plugin/utilities' import { APINEX_MODELS } from './apinex-models' export type ProviderType = 'apinex' @@ -53,26 +56,37 @@ export function createApinexProvider (): ProviderConfig { } } -export async function loadSettings (): Promise { - const defaults = defaultSettings() - try { - const stored = await figma.clientStorage.getAsync(SETTINGS_KEY) - 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 defaults - } +// Request main thread to load persisted settings; resolves with merged Settings. +export function loadSettings (): Promise { + return new Promise(function (resolve) { + const dispose = on('SETTINGS_LOADED', function (payload: { settings: Settings | null }) { + dispose() + const stored = payload.settings + if (stored == null || stored.version !== 1) { + resolve(defaultSettings()) + return + } + const defaults = defaultSettings() + const merged: Settings = { ...defaults, ...stored } + if (!Array.isArray(merged.providers) || merged.providers.length === 0) { + merged.providers = defaults.providers + merged.activeProviderId = defaults.providers[0].id + } + resolve(merged) + }) + emit('LOAD_SETTINGS', { key: SETTINGS_KEY }) + }) } -export async function saveSettings (settings: Settings): Promise { - try { - await figma.clientStorage.setAsync(SETTINGS_KEY, settings) - } catch (e) { /* storage unavailable — ignore */ } +// Request main thread to persist settings. +export function saveSettings (settings: Settings): Promise { + return new Promise(function (resolve) { + const dispose = on('SAVE_SETTINGS_DONE', function () { + dispose() + resolve() + }) + emit('SAVE_SETTINGS', { key: SETTINGS_KEY, settings: settings }) + }) } export function settingsToJSON (settings: Settings): string { diff --git a/src/ui.tsx b/src/ui.tsx index 68d40de..f8c0610 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -30,7 +30,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.18' +const PLUGIN_VERSION = '0.1.19' const fieldStyle = { display: 'flex', @@ -164,6 +164,7 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) function save () { saveSettings(settings).then(function () { setSavedNote(true) + logEvent('Settings saved (manual)') setTimeout(function () { setSavedNote(false) }, 1500) }) } @@ -286,7 +287,7 @@ function Plugin () { // Gated on `loaded` so we don't overwrite stored settings with defaults before load completes. useEffect(function () { if (!loaded) return - const t = setTimeout(function () { saveSettings(settings) }, 400) + const t = setTimeout(function () { saveSettings(settings).then(function () { logEvent('Settings saved') }) }, 400) return function () { clearTimeout(t) } }, [settings, loaded]) useEffect(function () { try { logEvent('Plugin loaded') } catch (e) {} }, [])