{"repo": "accsci/agentsurfer", "files": {"entrypoints/options/App.tsx": {"f5a57dca2a4f201846ef155a0bc3f93fe353261d": {"t": 1785595219.8035371, "n": 21021, "h": "7151673a06839878", "c": "import { useLiveQuery } from 'dexie-react-hooks';\nimport { Plus, Trash2, Star, CheckCircle2, AlertCircle, Pencil, X } from 'lucide-react';\nimport { useState } from 'react';\n\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { ScrollArea } from '@/components/ui/scroll-area';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { Separator } from '@/components/ui/separator';\nimport { db, newId, now } from '@/lib/db';\nimport { useChangeCount } from '@/lib/use-change-count';\nimport { sendToSW } from '@/lib/sw-messenger';\nimport { KnownModels } from '@/lib/llm';\nimport { ToolConfigPanel } from './components/ToolConfigPanel';\nimport {\n DefaultBaseUrl,\n DefaultModelId,\n DEFAULT_REASONING_EFFORT,\n ProviderMeta,\n ProviderSchema,\n REASONING_EFFORT_VALUES,\n type Provider,\n type ReasoningEffort,\n} from '@/types';\nimport { ModelConfigSchema } from '@/types';\n\nasync function dbMsg(message: { type: string; [k: string]: unknown }): Promise {\n const res = await sendToSW(message);\n if (!res.ok) throw new Error(res.error ?? 'db message failed');\n}\n\nexport default function OptionsApp() {\n const configChangeCount = useChangeCount('modelConfigs');\n const configs = useLiveQuery(\n () => db.modelConfigs.orderBy('createdAt').toArray(),\n [configChangeCount],\n [],\n );\n\n return (\n
\n
\n
\n

AgentSurfer

\n

Model providers and API keys

\n
\n v0.1\n
\n\n \n\n \n\n
\n

\n Saved configurations\n

\n \n
\n {configs?.length === 0 && (\n

\n No configurations yet. Add one above to get started.\n

\n )}\n {configs?.map((c) => (\n \n ))}\n
\n
\n
\n\n \n\n

\n API keys are stored locally in this browser via IndexedDB. They never leave your machine\n except to call the provider you configured.\n

\n
\n );\n}\n\n// ---------- Form: add / edit a config ----------\n\nfunction ConfigForm() {\n const [name, setName] = useState('');\n const [provider, setProvider] = useState('openai');\n const [modelId, setModelId] = useState(DefaultModelId.openai);\n const [apiKey, setApiKey] = useState('');\n const [baseUrl, setBaseUrl] = useState('');\n const [maxSteps, setMaxSteps] = useState(99);\n // Reasoning effort — surfaced in the form for StepFun. Other providers\n // silently ignore it (lib/runtime/loop.ts only spreads `providerOptions`\n // when this is set). Reset to DEFAULT_REASONING_EFFORT on provider\n // switch so users don't carry a stale value across providers.\n const [reasoningEffort, setReasoningEffort] = useState(DEFAULT_REASONING_EFFORT);\n const [makeDefault, setMakeDefault] = useState(true);\n const [error, setError] = useState(null);\n const [ok, setOk] = useState(false);\n\n const meta = ProviderMeta[provider];\n const needsBaseUrl = meta.needsBaseUrl;\n const knownModels = KnownModels[provider] ?? [];\n\n async function submit(e: React.FormEvent) {\n e.preventDefault();\n setError(null);\n setOk(false);\n\n const candidate = {\n id: newId(),\n name: name.trim() || `${meta.label} (${modelId})`,\n provider,\n modelId: modelId.trim() || DefaultModelId[provider],\n apiKey: apiKey.trim(),\n baseUrl: baseUrl.trim() || null,\n isDefault: makeDefault,\n // Coerce to number explicitly — returns\n // \"\" when cleared, which Zod rejects; clamp to schema range\n // and let ModelConfigSchema re-validate the final value.\n maxSteps: Number.isFinite(maxSteps) ? maxSteps : 99,\n // Always store reasoningEffort for new configs — the value is\n // provider-agnostic (StepFun honors it; others ignore it via\n // the loop's conditional spread). This avoids schema-vs-form\n // drift when a user re-edits the same config after switching\n // providers.\n reasoningEffort,\n createdAt: now(),\n };\n const parsed = ModelConfigSchema.safeParse(candidate);\n if (!parsed.success) {\n setError(parsed.error.issues[0]?.message ?? 'Invalid configuration');\n return;\n }\n try {\n await dbMsg({ type: 'db:upsert-config', config: parsed.data });\n if (makeDefault) await dbMsg({ type: 'db:set-active-config', id: parsed.data.id });\n // Reset form\n setName('');\n setApiKey('');\n setBaseUrl('');\n setMaxSteps(99);\n setReasoningEffort(DEFAULT_REASONING_EFFORT);\n setOk(true);\n setTimeout(() => setOk(false), 2000);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n }\n }\n\n return (\n \n \n \n Add model configuration\n \n \n Pick a provider, enter your API key, and (optionally) make it the default.\n \n \n \n
\n
\n
\n \n {\n const next = ProviderSchema.parse(v);\n setProvider(next);\n setModelId(DefaultModelId[next]);\n // Reset reasoning effort to the default on provider\n // change so a stale value from a previous provider\n // doesn't leak into the new config. The field is\n // still saved (other providers ignore it) but the\n // initial UI value reflects the recommended default.\n setReasoningEffort(DEFAULT_REASONING_EFFORT);\n }}\n >\n \n \n \n \n {ProviderSchema.options.map((p) => (\n \n {ProviderMeta[p].label}\n \n ))}\n \n \n
\n\n
\n \n setModelId(e.target.value)}\n placeholder={DefaultModelId[provider]}\n list=\"known-models\"\n />\n {knownModels.length > 0 && (\n \n {knownModels.map((m: string) => (\n \n )}\n
\n
\n\n
\n \n setName(e.target.value)}\n placeholder={`${meta.label} · ${modelId}`}\n />\n
\n\n
\n \n setApiKey(e.target.value)}\n placeholder={\n meta.authHeader === 'api-key'\n ? 'MIMO_API_KEY'\n : meta.authHeader === 'x-api-key'\n ? 'sk-ant-...'\n : 'sk-...'\n }\n autoComplete=\"off\"\n />\n

\n Auth header: {meta.authHeader}\n {DefaultBaseUrl[provider] && (\n <>\n {' · '}default URL: {DefaultBaseUrl[provider]}\n \n )}\n

\n
\n\n {needsBaseUrl && (\n
\n \n setBaseUrl(e.target.value)}\n placeholder=\"https://api.example.com/v1\"\n />\n
\n )}\n\n
\n \n {\n // yields '' when the user clears\n // the field. Treat that as \"leave it alone for now\" and\n // let Zod catch any out-of-range value on submit.\n const n = e.target.value === '' ? 99 : Number.parseInt(e.target.value, 10);\n setMaxSteps(Number.isFinite(n) ? n : 99);\n }}\n placeholder=\"99\"\n />\n

\n Cap on how many tool-call steps the agent can take in a single run\n (1-999, default 99). Raise for long multi-step tasks like\n \"search → click N results → summarize\".\n

\n
\n\n {/* Reasoning effort — exposed for StepFun's step-3.7-flash. Other\n providers silently ignore it (see lib/runtime/loop.ts). We\n keep the field visible for all providers so users have one\n mental model; the loop only forwards it to OpenAI-compat\n when set. step-3.5-flash-2603 supports low/high only —\n selecting `medium` for that model will return a 400 from\n the API. */}\n
\n \n {\n // Cast: Zod-enum produces ReasoningEffort; the Select\n // value comes from REASONING_EFFORT_VALUES which is the\n // exact tuple Zod was built from, so this is safe.\n setReasoningEffort(v as ReasoningEffort);\n }}\n >\n \n \n \n \n {REASONING_EFFORT_VALUES.map((v) => (\n \n {v}\n \n ))}\n \n \n

\n low 简单问答/摘要 · medium 默认推荐 ·\n high 复杂推理/代码. Honored by StepFun step-3.7-flash;\n ignored by other providers.\n

\n
\n\n \n\n {error && (\n

\n {error}\n

\n )}\n {ok && (\n

\n Saved\n

\n )}\n\n \n
\n
\n
\n );\n}\n\n// ---------- Row: saved config ----------\n\nfunction ConfigRow({ config }: { config: import('@/types').ModelConfig }) {\n const [editing, setEditing] = useState(false);\n\n if (editing) {\n return setEditing(false)} />;\n }\n\n return (\n \n \n
\n
\n {config.name}\n {config.isDefault && (\n \n active\n \n )}\n
\n

\n {ProviderMeta[config.provider].label} · {config.modelId}\n {config.baseUrl && (\n <>\n {' · '}\n {config.baseUrl}\n \n )}\n {' · max steps: '}\n {config.maxSteps ?? 99}\n

\n
\n
\n {!config.isDefault && (\n dbMsg({ type: 'db:set-active-config', id: config.id })}\n title=\"Set as default\"\n >\n \n \n )}\n setEditing(true)}\n title=\"Edit\"\n >\n \n \n dbMsg({ type: 'db:delete-config', id: config.id })}\n title=\"Delete\"\n >\n \n \n
\n
\n
\n );\n}\n\n// ---------- Row: inline-edit an existing config ----------\n\nfunction ConfigEditRow({\n config,\n onClose,\n}: {\n config: import('@/types').ModelConfig;\n onClose: () => void;\n}) {\n const meta = ProviderMeta[config.provider];\n const knownModels = KnownModels[config.provider] ?? [];\n\n const [name, setName] = useState(config.name);\n const [modelId, setModelId] = useState(config.modelId);\n const [apiKey, setApiKey] = useState(config.apiKey);\n const [baseUrl, setBaseUrl] = useState(config.baseUrl ?? '');\n const [maxSteps, setMaxSteps] = useState(config.maxSteps ?? 99);\n const [reasoningEffort, setReasoningEffort] = useState(\n config.reasoningEffort ?? DEFAULT_REASONING_EFFORT,\n );\n const [error, setError] = useState(null);\n const [saving, setSaving] = useState(false);\n\n async function save() {\n setError(null);\n // Re-validate against the same schema the add-form uses. Preserve the\n // config's id/provider/isDefault/createdAt so this is an update, not a\n // new row — `db:upsert-config` is a put keyed on `id`.\n const candidate = {\n ...config,\n name: name.trim() || `${meta.label} (${modelId})`,\n modelId: modelId.trim() || config.modelId,\n apiKey: apiKey.trim(),\n baseUrl: baseUrl.trim() || null,\n maxSteps: Number.isFinite(maxSteps) ? maxSteps : 99,\n reasoningEffort,\n };\n const parsed = ModelConfigSchema.safeParse(candidate);\n if (!parsed.success) {\n setError(parsed.error.issues[0]?.message ?? 'Invalid configuration');\n return;\n }\n setSaving(true);\n try {\n await dbMsg({ type: 'db:upsert-config', config: parsed.data });\n onClose();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n setSaving(false);\n }\n }\n\n return (\n \n \n
\n \n Editing · {meta.label}\n \n \n
\n\n
\n \n setName(e.target.value)}\n placeholder={`${meta.label} · ${modelId}`}\n />\n
\n\n
\n \n setModelId(e.target.value)}\n list={`edit-known-models-${config.id}`}\n />\n {knownModels.length > 0 && (\n \n {knownModels.map((m: string) => (\n \n )}\n
\n\n
\n \n setApiKey(e.target.value)}\n autoComplete=\"off\"\n />\n
\n\n {meta.needsBaseUrl && (\n
\n \n setBaseUrl(e.target.value)}\n placeholder=\"https://api.example.com/v1\"\n />\n
\n )}\n\n
\n \n {\n const n = e.target.value === '' ? 99 : Number.parseInt(e.target.value, 10);\n setMaxSteps(Number.isFinite(n) ? n : 99);\n }}\n placeholder=\"99\"\n />\n

\n 一次回复中 agent 最多允许的工具调用步数 (1-999, 默认 99).\n

\n
\n\n
\n \n setReasoningEffort(v as ReasoningEffort)}\n >\n \n \n \n \n {REASONING_EFFORT_VALUES.map((v) => (\n \n {v}\n \n ))}\n \n \n
\n\n {error && (\n

\n {error}\n

\n )}\n\n
\n \n \n
\n
\n
\n );\n}\n"}}}}