fix(ai): repair custom setting name autocompletion
All checks were successful
CI Code / Check spelling (pull_request) Successful in 13s
CI Code / Check coding style (pull_request) Successful in 23s
CI Code / Code Coverage (pull_request) Successful in 3m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m24s
CI Code / Linux (arch) (pull_request) Successful in 7m7s
CI Code / Linux (debian) (pull_request) Successful in 8m41s

The setting-name branch for /ai set custom never fired: it passed the
prefix "/ai set custom " with a trailing space to
autocomplete_param_with_ac(), which appends a space itself, so the
matched prefix contained a double space no real input ever has. Even
without that, the primitive completes the first token after the command
while the setting name is the second, and the num_args guard only held
before the name was started. The suggestion list (tools/search/memory/
plugins) predated the /ai set custom backend and matched no real
payload parameter.

Replace the branch with token-position completion (arg 5 via
autocomplete_param_no_with_func) fed by a list rebuilt on demand from
common payload parameters plus the keys already set on the provider,
exposed through the new ai_get_provider_setting_keys() (snapshot taken
under settings_lock). Drop the now-unused parse_args() call whose NULL
result was fed to g_strv_length() unguarded, spamming a glib CRITICAL
on TAB at /ai with zero or five-plus arguments.
This commit is contained in:
2026-07-06 11:08:58 +03:00
parent 14f76ab1db
commit b3a269342d
6 changed files with 112 additions and 22 deletions

View File

@@ -990,6 +990,25 @@ ai_get_provider_setting(const gchar* provider_name, const gchar* setting)
return value;
}
GList*
ai_get_provider_setting_keys(const gchar* provider_name)
{
AIProvider* provider = provider_name ? ai_get_provider(provider_name) : NULL;
if (!provider || !provider->settings)
return NULL;
GList* keys = NULL;
pthread_mutex_lock(&provider->settings_lock);
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, provider->settings);
while (g_hash_table_iter_next(&iter, &key, &value)) {
keys = g_list_prepend(keys, g_strdup((gchar*)key));
}
pthread_mutex_unlock(&provider->settings_lock);
return keys;
}
/* ========================================================================
* Generic HTTP request handling
* ======================================================================== */

View File

@@ -207,6 +207,14 @@ gboolean ai_set_provider_setting(const gchar* provider_name, const gchar* settin
*/
gchar* ai_get_provider_setting(const gchar* provider_name, const gchar* setting);
/**
* List the custom setting keys currently set on a provider.
* @param provider_name The provider name
* @return GList of gchar* (free with g_list_free_full(keys, g_free)),
* or NULL if the provider is unknown or has no settings
*/
GList* ai_get_provider_setting_keys(const gchar* provider_name);
/**
* Fetch available models from a provider's API.
* @param provider_name The provider name

View File

@@ -1192,11 +1192,6 @@ cmd_ac_init(void)
autocomplete_add_unsorted(ai_api_types_ac, "responses", FALSE);
autocomplete_add_unsorted(ai_api_types_ac, "auto", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "tools", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "search", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "memory", FALSE);
autocomplete_add_unsorted(ai_set_custom_subcommands_ac, "plugins", FALSE);
autocomplete_add_unsorted(ai_remove_subcommands_ac, "provider", FALSE);
autocomplete_add(avatar_ac, "set");
@@ -4545,17 +4540,61 @@ _ai_api_type_find(const char* const search_str, gboolean previous, void* context
return autocomplete_complete(ai_api_types_ac, search_str, FALSE, previous);
}
static char*
_ai_custom_setting_find(const char* const search_str, gboolean previous, void* context)
{
return autocomplete_complete(ai_set_custom_subcommands_ac, search_str, FALSE, previous);
}
/**
* Autocomplete the setting name for /ai set custom <provider> <setting>.
* Offers a few common payload parameters merged with the keys already set
* on the provider, so existing settings can be retyped or overridden.
*/
static char*
_ai_custom_setting_autocomplete(const char* const input, gboolean previous)
{
if (!g_str_has_prefix(input, "/ai set custom ")) {
return NULL;
}
/* Quote-aware parse: args[2] is the provider, args[3] the setting being
* typed; skip the rebuild when the caret is not at the setting position */
gboolean parse_result = FALSE;
auto_gcharv gchar** args = parse_args(input, 1, 5, &parse_result);
if (!parse_result) {
return NULL;
}
guint num_args = g_strv_length(args);
if (num_args < 3 || num_args > 4 || !ai_get_provider(args[2])) {
return NULL;
}
/* Rebuild the completion list: common parameters + the provider's keys */
gchar* suggestions[] = { "temperature", "max_tokens", "top_p" };
GList* keys = ai_get_provider_setting_keys(args[2]);
char** items = g_new0(char*, ARRAY_SIZE(suggestions) + g_list_length(keys) + 1);
guint i = 0;
for (; i < ARRAY_SIZE(suggestions); i++) {
items[i] = suggestions[i];
}
for (GList* curr = keys; curr; curr = g_list_next(curr)) {
items[i++] = curr->data;
}
autocomplete_update(ai_set_custom_subcommands_ac, items);
g_free(items);
g_list_free_full(keys, g_free);
/* /ai(1) set(2) custom(3) <provider>(4) <setting>(5) */
return autocomplete_param_no_with_func(input, "/ai set custom", 5, _ai_custom_setting_find, previous, NULL);
}
static char*
_ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
/* Parse once for reuse - /ai <subcommand> [<arg1>] [<arg2>] */
gboolean parse_result = FALSE;
auto_gcharv gchar** args = parse_args(input, 1, 4, &parse_result);
int num_args = g_strv_length(args);
/* Top-level /ai <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
// /ai set provider <name> <url> - autocomplete provider names (for updating)
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
if (result) {
return result;
@@ -4591,18 +4630,12 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
return result;
}
// /ai set custom <provider> <setting> - autocomplete settings
/* args[0]="set", args[1]="custom", args[2]=provider (if typed), args[3]=setting (if typed) */
if (num_args == 3 && g_strcmp0(args[1], "custom") == 0) {
/* /ai set custom <provider> - check if provider is valid */
if (ai_get_provider(args[2])) {
/* Valid provider, try settings autocomplete */
result = autocomplete_param_with_ac(input, "/ai set custom ", ai_set_custom_subcommands_ac, TRUE, previous);
if (result) {
return result;
}
}
// /ai set custom <provider> <setting> - autocomplete setting names
result = _ai_custom_setting_autocomplete(input, previous);
if (result) {
return result;
}
// /ai set <subcommand> - autocomplete subcommands
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
if (result) {

View File

@@ -637,6 +637,34 @@ test_ai_set_provider_setting_reserved_key(void** state)
assert_string_equal("0.7", temp);
}
void
test_ai_get_provider_setting_keys(void** state)
{
ai_add_provider("p_setting_keys", "https://example.test/skeys/");
/* No settings yet, unknown provider, NULL — all yield NULL */
assert_null(ai_get_provider_setting_keys("p_setting_keys"));
assert_null(ai_get_provider_setting_keys("nonexistent"));
assert_null(ai_get_provider_setting_keys(NULL));
ai_set_provider_setting("p_setting_keys", "temperature", "0.7");
ai_set_provider_setting("p_setting_keys", "reasoning_effort", "high");
/* Both keys listed (order is hash-defined, assert membership) */
GList* keys = ai_get_provider_setting_keys("p_setting_keys");
assert_int_equal(2, g_list_length(keys));
assert_non_null(g_list_find_custom(keys, "temperature", (GCompareFunc)g_strcmp0));
assert_non_null(g_list_find_custom(keys, "reasoning_effort", (GCompareFunc)g_strcmp0));
g_list_free_full(keys, g_free);
/* Removing a setting removes its key */
ai_set_provider_setting("p_setting_keys", "temperature", NULL);
keys = ai_get_provider_setting_keys("p_setting_keys");
assert_int_equal(1, g_list_length(keys));
assert_string_equal("reasoning_effort", keys->data);
g_list_free_full(keys, g_free);
}
void
test_ai_set_provider_api_type(void** state)
{

View File

@@ -44,6 +44,7 @@ void test_ai_set_provider_default_model(void** state);
void test_ai_get_provider_default_model(void** state);
void test_ai_set_provider_setting(void** state);
void test_ai_set_provider_setting_reserved_key(void** state);
void test_ai_get_provider_setting_keys(void** state);
void test_ai_set_provider_api_type(void** state);
void test_ai_api_type_string_round_trip(void** state);
void test_ai_get_provider_setting(void** state);

View File

@@ -753,6 +753,7 @@ main(int argc, char* argv[])
cmocka_unit_test_setup_teardown(test_ai_get_provider_default_model, ai_client_setup, ai_client_teardown),
cmocka_unit_test_setup_teardown(test_ai_set_provider_setting, ai_client_setup, ai_client_teardown),
cmocka_unit_test_setup_teardown(test_ai_set_provider_setting_reserved_key, ai_client_setup, ai_client_teardown),
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting_keys, ai_client_setup, ai_client_teardown),
cmocka_unit_test_setup_teardown(test_ai_set_provider_api_type, ai_client_setup, ai_client_teardown),
cmocka_unit_test(test_ai_api_type_string_round_trip),
cmocka_unit_test_setup_teardown(test_ai_get_provider_setting, ai_client_setup, ai_client_teardown),