({ plugin, pluginId, onDone }: Props)
| 62 | }; |
| 63 | |
| 64 | export function PluginOptionsFlow({ plugin, pluginId, onDone }: Props): React.ReactNode { |
| 65 | // Build the step list once at mount. Re-calling after a save would drop the |
| 66 | // item we just configured. |
| 67 | const [steps] = React.useState<ConfigStep[]>(() => { |
| 68 | const result: ConfigStep[] = []; |
| 69 | |
| 70 | // Top-level manifest.userConfig |
| 71 | const unconfigured = getUnconfiguredOptions(plugin); |
| 72 | if (Object.keys(unconfigured).length > 0) { |
| 73 | result.push({ |
| 74 | key: 'top-level', |
| 75 | title: `Configure ${plugin.name}`, |
| 76 | subtitle: 'Plugin options', |
| 77 | schema: unconfigured, |
| 78 | load: () => loadPluginOptions(pluginId), |
| 79 | save: values => savePluginOptions(pluginId, values, plugin.manifest.userConfig!), |
| 80 | }); |
| 81 | } |
| 82 | |
| 83 | // Per-channel userConfig (assistant-mode channels) |
| 84 | const channels: UnconfiguredChannel[] = getUnconfiguredChannels(plugin); |
| 85 | for (const channel of channels) { |
| 86 | result.push({ |
| 87 | key: `channel:${channel.server}`, |
| 88 | title: `Configure ${channel.displayName}`, |
| 89 | subtitle: `Plugin: ${plugin.name}`, |
| 90 | schema: channel.configSchema, |
| 91 | load: () => loadMcpServerUserConfig(pluginId, channel.server) ?? undefined, |
| 92 | save: values => saveMcpServerUserConfig(pluginId, channel.server, values, channel.configSchema), |
| 93 | }); |
| 94 | } |
| 95 | |
| 96 | return result; |
| 97 | }); |
| 98 | |
| 99 | const [index, setIndex] = React.useState(0); |
| 100 | |
| 101 | // Latest-ref: lets the effect close over the current onDone without |
| 102 | // re-running when the parent re-renders. |
| 103 | const onDoneRef = React.useRef(onDone); |
| 104 | onDoneRef.current = onDone; |
| 105 | |
| 106 | // Nothing to configure → tell the caller and render nothing. Effect, |
| 107 | // not inline call: calling setState in the parent during our render |
| 108 | // is a React rules-of-hooks violation. |
| 109 | React.useEffect(() => { |
| 110 | if (steps.length === 0) { |
| 111 | onDoneRef.current('skipped'); |
| 112 | } |
| 113 | }, [steps.length]); |
| 114 | |
| 115 | if (steps.length === 0) { |
| 116 | return null; |
| 117 | } |
| 118 | |
| 119 | const current = steps[index]!; |
| 120 | |
| 121 | function handleSave(values: PluginOptionValues): void { |
nothing calls this directly
no test coverage detected