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
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:
@@ -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
|
||||
* ======================================================================== */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user