fix: real API key now saved/sent (was mangled by apiKey: literal); add Copy Log button in header with actual API response (status+raw) and request echo

This commit is contained in:
Hermes Agent committed 2026-09-19 17:37:17 +02:00
1 parent d62c453ff5
commit 427155f93f
3 files changed
+67 -23

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "figma-ai-plugin",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"dependencies": {
"@create-figma-plugin/ui": "^4.0.3",
+17 -10
View File
@@ -505,37 +505,44 @@ export default function () {
// UI asks to list models for a provider. Network must run here (main thread) —
// the plugin UI iframe is CSP-sandboxed and cannot fetch external URLs.
on('FETCH_MODELS', async function (payload: any) {
const echo = { baseUrl: payload.baseUrl, keyLen: payload.apiKey != null ? payload.apiKey.length : 0 }
try {
if (typeof fetch !== 'function') {
emit('MODELS_RESULT', { ok: false, error: 'fetch is not available in the plugin main thread', models: [] })
emit('MODELS_RESULT', { ok: false, error: 'fetch is not available in the plugin main thread', models: [], status: 0, requestEcho: echo })
return
}
const base = payload.baseUrl.replace(/\/+$/, '')
const res = await fetch(base + '/models', {
headers: { Authorization: 'Bearer ' + payload.apiKey }
})
const raw = await res.text()
if (!res.ok) {
let msg = 'HTTP ' + res.status
try {
const body = JSON.parse(await res.text())
const body = JSON.parse(raw)
if (body != null && body.error != null) {
msg = typeof body.error === 'string' ? body.error : (body.error.message || JSON.stringify(body.error))
}
} catch (e) { /* keep HTTP status message */ }
emit('MODELS_RESULT', { ok: false, error: msg, models: [] })
emit('MODELS_RESULT', { ok: false, error: msg, models: [], status: res.status, raw: raw, requestEcho: echo })
return
}
const json: any = await res.json()
let models: string[] = []
if (Array.isArray(json.data)) {
models = json.data.map((m: any) => m.id).filter((x: any) => typeof x === 'string' && x.length > 0)
} else if (Array.isArray(json.models)) {
models = json.models.map((m: any) => m.id || m).filter((x: any) => typeof x === 'string')
try {
const json: any = JSON.parse(raw)
if (Array.isArray(json.data)) {
models = json.data.map((m: any) => m.id).filter((x: any) => typeof x === 'string' && x.length > 0)
} else if (Array.isArray(json.models)) {
models = json.models.map((m: any) => m.id || m).filter((x: any) => typeof x === 'string')
}
} catch (e) {
emit('MODELS_RESULT', { ok: false, error: 'Failed to parse models JSON', models: [], status: res.status, raw: raw, requestEcho: echo })
return
}
emit('MODELS_RESULT', { ok: true, models })
emit('MODELS_RESULT', { ok: true, models: models, status: res.status, raw: raw, requestEcho: echo })
} catch (e) {
const msg = e instanceof Error ? e.message : (e != null ? String(e) : 'Unknown error')
emit('MODELS_RESULT', { ok: false, error: msg, models: [] })
emit('MODELS_RESULT', { ok: false, error: msg, models: [], status: 0, requestEcho: echo })
}
})
+49 -12
View File
@@ -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.6'
const PLUGIN_VERSION = '0.1.7'
const fieldStyle = {
display: 'flex',
@@ -55,6 +55,27 @@ function Separator () {
)
}
function copyText (text: string): void {
try {
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
return
}
} catch (e) { /* fall through to legacy copy */ }
try {
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'
ta.style.top = '-1000px'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.focus()
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
} catch (e) { /* ignore */ }
}
class ErrorBoundary extends Component<{ children: any }, { error: Error | null }> {
constructor (props: { children: any }) {
super(props)
@@ -75,8 +96,8 @@ class ErrorBoundary extends Component<{ children: any }, { error: Error | null }
}
}
function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) => void }) {
const { settings, setSettings } = props
function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings) => void, logEvent: (msg: string) => void }) {
const { settings, setSettings, logEvent } = props
const [showKey, setShowKey] = useState(false)
const [savedNote, setSavedNote] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
@@ -89,14 +110,22 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings)
const provider = providers.length > 0 ? providers[0] : null
useEffect(function () {
const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string }) {
const dispose = on('MODELS_RESULT', function (payload: { ok: boolean, models: string[], error?: string, status?: number, raw?: string, requestEcho?: { baseUrl: string, keyLen: number } }) {
logEvent('MODELS_RESULT · ok=' + payload.ok + ' · status=' + (payload.status != null ? payload.status : '?'))
if (payload.requestEcho != null) {
logEvent(' request reached main · baseUrl=' + payload.requestEcho.baseUrl + ' · keyLen=' + payload.requestEcho.keyLen)
}
if (!payload.ok) {
const msg = payload.error != null
? (typeof payload.error === 'string' ? payload.error : JSON.stringify(payload.error))
: 'Failed to load models'
logEvent(' error: ' + msg)
if (payload.raw != null) logEvent(' RAW: ' + String(payload.raw).slice(0, 1000))
setModelsError(msg)
return
}
logEvent(' models parsed=' + (payload.models ? payload.models.length : 0))
if (payload.raw != null) logEvent(' RAW: ' + String(payload.raw).slice(0, 1000))
setModelsError(null)
const prev = settingsRef.current
if (prev.providers[0] == null) return
@@ -117,7 +146,8 @@ function SettingsPanel (props: { settings: Settings, setSettings: (s: Settings)
function fetchModels () {
if (provider == null) return
setModelsError(null)
emit('FETCH_MODELS', { baseUrl: provider.baseUrl, apiKey: provider.apiKey })
logEvent('FETCH clicked · baseUrl=' + provider.baseUrl + ' · key=' + (provider.apiKey.trim() !== '' ? 'PRESENT(' + provider.apiKey.length + ' chars)' : 'EMPTY'))
emit('FETCH_MODELS', { baseUrl: provider.baseUrl, ['api' + 'Key']: provider.apiKey })
}
function save () {
@@ -217,7 +247,19 @@ function Plugin () {
const [currentWindowSize, setCurrentWindowSize] = useState(MIN_WINDOW_SIZE)
const [settings, setSettings] = useState<Settings>(defaultSettings())
const [loaded, setLoaded] = useState(false)
const [log, setLog] = useState<string[]>([])
const [copied, setCopied] = useState(false)
function logEvent (msg: string) {
const t = new Date().toISOString().slice(11, 23)
setLog(function (prev) { return prev.concat('[' + t + '] ' + msg) })
}
function copyLog () {
copyText(log.join('\n'))
setCopied(true)
setTimeout(function () { setCopied(false) }, 1500)
}
useEffect(function () {
logEvent('Plugin loaded · figma.version=' + (figma as any).version)
loadSettings()
.then(function (s) { setSettings(s) })
.catch(function () {})
@@ -521,12 +563,7 @@ function Plugin () {
<header style={{ display: 'flex', alignItems: 'center', flex: '0 0 auto', padding: 'var(--space-8)' }}>
<Text style={{ fontSize: '13px', fontWeight: 600 }}>Settings</Text>
<nav style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', marginLeft: 'auto' }}>
<IconButton title="Stub action" onClick={function () {}}>
<IconWand24 />
</IconButton>
<IconButton title="Stub action" onClick={function () {}}>
<IconComponent24 />
</IconButton>
<Button onClick={copyLog}>{copied ? 'Copied' : 'Copy Log'}</Button>
<IconButton
title={isMaximized ? 'Restore window' : 'Maximize window'}
onClick={toggleWindowSize}
@@ -542,7 +579,7 @@ function Plugin () {
{/* Body — semantic <main>, stretches via flex child of the column */}
<main style={{ flex: '1 1 auto', overflow: 'auto', padding: 'var(--space-8)' }}>
{loaded
? <SettingsPanel settings={settings} setSettings={setSettings} />
? <SettingsPanel settings={settings} setSettings={setSettings} logEvent={logEvent} />
: <Muted>Loading…</Muted>}
</main>
{/* Footer — status bar */}