feat(opencode): add extra options editor for SDK configuration

Add key-value pair editor for configuring additional SDK options like
timeout, setCacheKey, etc. Values are automatically parsed to appropriate
types (number, boolean, object) on save.

- Add `extra` field with serde flatten in Rust backend
- Add index signature to OpenCodeProviderOptions type
- Create ExtraOptionKeyInput component with local state pattern
- Place extra options section above models configuration
This commit is contained in:
Jason
2026-01-19 11:42:24 +08:00
parent 73013c10af
commit b0d0a2c466
7 changed files with 259 additions and 3 deletions
@@ -52,6 +52,45 @@ function ModelIdInput({
);
}
/**
* Extra option key input with local state to prevent focus loss.
* Same pattern as ModelIdInput - use local state during editing,
* only commit changes on blur.
*/
function ExtraOptionKeyInput({
optionKey,
onChange,
placeholder,
}: {
optionKey: string;
onChange: (newKey: string) => void;
placeholder?: string;
}) {
// For new options with placeholder keys like "option-123", show empty string
const displayValue = optionKey.startsWith("option-") ? "" : optionKey;
const [localValue, setLocalValue] = useState(displayValue);
// Sync when external key changes
useEffect(() => {
setLocalValue(optionKey.startsWith("option-") ? "" : optionKey);
}, [optionKey]);
return (
<Input
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
onBlur={() => {
const trimmed = localValue.trim();
if (trimmed && trimmed !== optionKey) {
onChange(trimmed);
}
}}
placeholder={placeholder}
className="flex-1"
/>
);
}
interface OpenCodeFormFieldsProps {
// NPM Package
npm: string;
@@ -71,6 +110,10 @@ interface OpenCodeFormFieldsProps {
// Models
models: Record<string, OpenCodeModel>;
onModelsChange: (models: Record<string, OpenCodeModel>) => void;
// Extra Options
extraOptions: Record<string, string>;
onExtraOptionsChange: (options: Record<string, string>) => void;
}
export function OpenCodeFormFields({
@@ -85,6 +128,8 @@ export function OpenCodeFormFields({
onBaseUrlChange,
models,
onModelsChange,
extraOptions,
onExtraOptionsChange,
}: OpenCodeFormFieldsProps) {
const { t } = useTranslation();
@@ -126,6 +171,41 @@ export function OpenCodeFormFields({
});
};
// Extra Options handlers
const handleAddExtraOption = () => {
const newKey = `option-${Date.now()}`;
onExtraOptionsChange({
...extraOptions,
[newKey]: "",
});
};
const handleRemoveExtraOption = (key: string) => {
const newOptions = { ...extraOptions };
delete newOptions[key];
onExtraOptionsChange(newOptions);
};
const handleExtraOptionKeyChange = (oldKey: string, newKey: string) => {
if (oldKey === newKey) return;
const newOptions: Record<string, string> = {};
for (const [k, v] of Object.entries(extraOptions)) {
if (k === oldKey) {
newOptions[newKey.trim() || oldKey] = v;
} else {
newOptions[k] = v;
}
}
onExtraOptionsChange(newOptions);
};
const handleExtraOptionValueChange = (key: string, value: string) => {
onExtraOptionsChange({
...extraOptions,
[key]: value,
});
};
return (
<>
{/* NPM Package Selector */}
@@ -187,6 +267,80 @@ export function OpenCodeFormFields({
</p>
</div>
{/* Extra Options Editor */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<FormLabel>
{t("opencode.extraOptions", { defaultValue: "额外选项" })}
</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddExtraOption}
className="h-7 gap-1"
>
<Plus className="h-3.5 w-3.5" />
{t("opencode.addExtraOption", { defaultValue: "添加" })}
</Button>
</div>
{Object.keys(extraOptions).length === 0 ? (
<p className="text-sm text-muted-foreground py-2">
{t("opencode.noExtraOptions", {
defaultValue: "暂无额外选项",
})}
</p>
) : (
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs text-muted-foreground px-1 mb-1">
<span className="flex-1">
{t("opencode.extraOptionKey", { defaultValue: "键名" })}
</span>
<span className="flex-1">
{t("opencode.extraOptionValue", { defaultValue: "值" })}
</span>
<span className="w-9" />
</div>
{Object.entries(extraOptions).map(([key, value]) => (
<div key={key} className="flex items-center gap-2">
<ExtraOptionKeyInput
optionKey={key}
onChange={(newKey) => handleExtraOptionKeyChange(key, newKey)}
placeholder={t("opencode.extraOptionKeyPlaceholder", {
defaultValue: "timeout",
})}
/>
<Input
value={value}
onChange={(e) => handleExtraOptionValueChange(key, e.target.value)}
placeholder={t("opencode.extraOptionValuePlaceholder", {
defaultValue: "600000",
})}
className="flex-1"
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveExtraOption(key)}
className="h-9 w-9 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
<p className="text-xs text-muted-foreground">
{t("opencode.extraOptionsHint", {
defaultValue:
"配置额外的 SDK 选项,如 timeout、setCacheKey 等。值会自动解析类型(数字、布尔值等)。",
})}
</p>
</div>
{/* Models Editor */}
<div className="space-y-3">
<div className="flex items-center justify-between">