feat(ai): add model caching, settings, and commands

- Introduce model caching with persistence to preferences
- Add provider default model and custom settings management
- Implement `/ai switch`, `/ai models`, and improve `/ai start`
- Add model name autocomplete for chat commands
- Update command definitions and help text
- Add unit tests for new functionality
This commit is contained in:
2026-05-09 13:15:58 +00:00
parent 731b55fa19
commit acae543057
11 changed files with 1255 additions and 98 deletions

View File

@@ -70,6 +70,7 @@
static char* _sub_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _ai_model_autocomplete(ProfWin* window, const char* const input, gboolean previous, const char* prefix);
static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _autoaway_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _autoconnect_autocomplete(ProfWin* window, const char* const input, gboolean previous);
@@ -285,12 +286,14 @@ static Autocomplete correction_ac;
static Autocomplete avatar_ac;
static Autocomplete ai_subcommands_ac;
static Autocomplete ai_set_subcommands_ac;
static Autocomplete ai_set_custom_subcommands_ac;
static Autocomplete ai_remove_subcommands_ac;
static Autocomplete url_ac;
static Autocomplete executable_ac;
static Autocomplete executable_param_ac;
static Autocomplete intype_ac;
static Autocomplete mood_ac;
static Autocomplete ai_models_ac;
static Autocomplete mood_type_ac;
static Autocomplete strophe_ac;
static Autocomplete strophe_sm_ac;
@@ -465,6 +468,7 @@ static Autocomplete* all_acs[] = {
&force_encryption_policy_ac,
&ai_subcommands_ac,
&ai_set_subcommands_ac,
&ai_set_custom_subcommands_ac,
&ai_remove_subcommands_ac
};
@@ -1178,18 +1182,26 @@ cmd_ac_init(void)
autocomplete_add(correction_ac, "off");
autocomplete_add(correction_ac, "char");
autocomplete_add(ai_subcommands_ac, "set");
autocomplete_add(ai_subcommands_ac, "remove");
autocomplete_add(ai_subcommands_ac, "start");
autocomplete_add(ai_subcommands_ac, "clear");
autocomplete_add(ai_subcommands_ac, "correct");
autocomplete_add(ai_subcommands_ac, "providers");
autocomplete_add_unsorted(ai_subcommands_ac, "set", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "remove", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "start", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "clear", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "providers", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "switch", FALSE);
autocomplete_add_unsorted(ai_subcommands_ac, "models", FALSE);
autocomplete_add(ai_set_subcommands_ac, "provider");
autocomplete_add(ai_set_subcommands_ac, "token");
autocomplete_add(ai_set_subcommands_ac, "org");
autocomplete_add_unsorted(ai_set_subcommands_ac, "provider", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "token", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "default-provider", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "default-model", FALSE);
autocomplete_add_unsorted(ai_set_subcommands_ac, "custom", FALSE);
autocomplete_add(ai_remove_subcommands_ac, "provider");
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");
autocomplete_add(avatar_ac, "disable");
@@ -4480,6 +4492,12 @@ _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);
gboolean space_at_end = g_str_has_suffix(input, " ");
int num_args = g_strv_length(args);
/* Top-level /ai <subcommand> autocomplete (e.g., /ai s<tab> -> /ai set) */
result = autocomplete_param_with_func(input, "/ai set provider", ai_providers_find, previous, NULL);
if (result) {
@@ -4492,12 +4510,36 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
return result;
}
// /ai set org <provider> <org_id> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai set org", ai_providers_find, previous, NULL);
// /ai set default-provider <provider> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai set default-provider", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai set default-model <provider> <model> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai set default-model", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai set custom <provider> <setting> <value> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai set custom", ai_providers_find, previous, NULL);
if (result) {
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 <subcommand> - autocomplete subcommands
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
if (result) {
@@ -4515,25 +4557,95 @@ _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
return result;
}
// /ai start [<provider>/<model>] - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai start", ai_providers_find, previous, NULL);
// /ai start [<provider>] [<model>] - use shared parse_args
if (parse_result && args && args[0] != NULL && g_strcmp0(args[0], "start") == 0) {
/* args[0]="start", args[1]=provider (if typed), args[2]=model (if typed) */
if (num_args == 1 || (num_args == 2 && !space_at_end)) {
result = autocomplete_param_with_func(input, "/ai start", ai_providers_find, previous, NULL);
} else {
/* /ai start <provider> <model> - model autocomplete */
result = _ai_model_autocomplete(window, input, previous, "/ai start ");
}
if (result) {
return result;
}
}
// /ai switch <provider> <model> - autocomplete model names for specified provider
result = _ai_model_autocomplete(window, input, previous, "/ai switch");
if (result) {
return result;
}
// /ai clear [<provider>] - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai clear", ai_providers_find, previous, NULL);
result = autocomplete_param_with_func(input, "/ai switch", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai correct <provider> <text>
result = autocomplete_param_with_func(input, "/ai correct", ai_providers_find, previous, NULL);
// /ai switch <model> - autocomplete model names from current session's provider
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
result = autocomplete_param_with_func(input, "/ai switch", ai_models_find, previous, aiwin);
if (result) {
return result;
}
// /ai models <provider> - autocomplete provider names
result = autocomplete_param_with_func(input, "/ai models", ai_providers_find, previous, NULL);
if (result) {
return result;
}
// /ai clear - no autocomplete
// /ai providers - no autocomplete
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, FALSE, previous);
return result;
}
}
/**
* Autocomplete model names for /ai <cmd> <provider> <model> patterns.
* Extracts provider from input and provides model completions from that provider's cached models.
* Uses static ai_models_ac to preserve cycling state (last_found) across calls.
*/
static char*
_ai_model_autocomplete(ProfWin* window, const char* const input, gboolean previous, const char* cmd_prefix)
{
if (!g_str_has_prefix(input, cmd_prefix)) {
return NULL;
}
/* Extract provider name from input (after cmd_prefix) */
auto_gchar gchar* rest = g_strdup(input + strlen(cmd_prefix));
char* space = strchr(rest, ' ');
if (space) {
*space = '\0';
}
/* Look up the provider by name and get its models */
AIProvider* prov = ai_get_provider(rest);
if (!prov || !prov->models) {
return NULL;
}
if (!ai_models_ac) {
ai_models_ac = autocomplete_new();
}
/* Convert GList* to char** for autocomplete_update */
int model_count = g_list_length(prov->models);
char** model_array = g_new0(char*, model_count + 1);
GList* curr = prov->models;
int i = 0;
while (curr) {
model_array[i++] = curr->data;
curr = g_list_next(curr);
}
autocomplete_update(ai_models_ac, model_array);
g_free(model_array);
auto_gchar gchar* full_prefix = g_strdup_printf("%s%s", cmd_prefix, rest);
char* result = autocomplete_param_with_ac(input, full_prefix, ai_models_ac, TRUE, previous);
return result;
}