Compare commits
54 Commits
fix/scroll
...
test/ai-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
65c6012677
|
|||
| befdd0ba10 | |||
| d676f3b087 | |||
|
30ad6e2a6e
|
|||
|
7cf82c99f0
|
|||
|
f93f7cdf2c
|
|||
|
a9f0153c0f
|
|||
|
5a074b280d
|
|||
|
fba6a040ee
|
|||
|
89a224b017
|
|||
|
b28b719d11
|
|||
|
b21d537e80
|
|||
|
2f1637ca8e
|
|||
|
277c82fb5d
|
|||
|
1ecaceb59f
|
|||
|
18fb7fa733
|
|||
|
a75a06b4a2
|
|||
|
9f4621883a
|
|||
|
6fe8c370df
|
|||
|
b1dbe714e8
|
|||
|
107f7778e2
|
|||
|
b634eed5af
|
|||
|
010b2062a4
|
|||
|
ecb07fd00c
|
|||
|
a898cb212d
|
|||
|
07d267b5bc
|
|||
|
acae543057
|
|||
|
731b55fa19
|
|||
|
e229ed1281
|
|||
|
f4221e27ac
|
|||
|
cdcc45b7c6
|
|||
|
f133d81a05
|
|||
|
a16237d16b
|
|||
|
cfe6ea46e2
|
|||
|
2fc7f3d672
|
|||
|
fe0a46da58
|
|||
|
cead417e78
|
|||
|
00f11eb704
|
|||
|
4913a3d5a4
|
|||
|
634fb7d7eb
|
|||
|
461c0c32dd
|
|||
|
002a6ed15b
|
|||
|
dc75f16221
|
|||
|
da0bf43d73
|
|||
|
9e1f9b666e
|
|||
|
93ad7379e2
|
|||
|
caa0b3ccba
|
|||
|
c9a5239117
|
|||
|
25e0459979
|
|||
|
bccd3ecded
|
|||
|
cff05ca802
|
|||
|
9c8ad57b59
|
|||
|
1f1770bd58
|
|||
|
e43e8378b0
|
@@ -35,8 +35,6 @@ static Autocomplete providers_ac = NULL;
|
|||||||
static AIProvider* _ai_add_provider_nopersist(const gchar* name, const gchar* api_url);
|
static AIProvider* _ai_add_provider_nopersist(const gchar* name, const gchar* api_url);
|
||||||
static void _ai_load_models_for_provider(AIProvider* provider);
|
static void _ai_load_models_for_provider(AIProvider* provider);
|
||||||
static void _ai_save_models_for_provider(AIProvider* provider);
|
static void _ai_save_models_for_provider(AIProvider* provider);
|
||||||
static const gchar* _find_json_field(const gchar* json, const gchar* field);
|
|
||||||
static gchar* _extract_json_string(const gchar* json, const gchar* field);
|
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
* Curl helpers
|
* Curl helpers
|
||||||
@@ -186,85 +184,6 @@ ai_json_escape(const gchar* str)
|
|||||||
return g_string_free(result, FALSE);
|
return g_string_free(result, FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Decode JSON string escapes in [start, end) into a freshly allocated buffer.
|
|
||||||
* Handles \" \\ \/ \n \t \r \b \f. \uXXXX sequences are passed through verbatim
|
|
||||||
* since the rest of the AI pipeline already deals in UTF-8 byte streams. */
|
|
||||||
static gchar*
|
|
||||||
_json_unescape_substring(const gchar* start, const gchar* end)
|
|
||||||
{
|
|
||||||
gchar* out = g_new0(gchar, (end - start) + 1);
|
|
||||||
gchar* w = out;
|
|
||||||
for (const gchar* in = start; in < end;) {
|
|
||||||
if (*in == '\\' && in + 1 < end) {
|
|
||||||
switch (in[1]) {
|
|
||||||
case '"':
|
|
||||||
*w++ = '"';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
case '\\':
|
|
||||||
*w++ = '\\';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
case '/':
|
|
||||||
*w++ = '/';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
*w++ = '\n';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
case 't':
|
|
||||||
*w++ = '\t';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
case 'r':
|
|
||||||
*w++ = '\r';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
case 'b':
|
|
||||||
*w++ = '\b';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
case 'f':
|
|
||||||
*w++ = '\f';
|
|
||||||
in += 2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
*w++ = *in++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*w++ = *in++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*w = '\0';
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Locate "field":"...value..." in json and return the unescaped value.
|
|
||||||
* Skips JSON whitespace around the colon and treats any \X (X != \0) inside
|
|
||||||
* the string body as an opaque escape so an embedded \" cannot be misread as
|
|
||||||
* the closing quote. */
|
|
||||||
static gchar*
|
|
||||||
_extract_json_string(const gchar* json, const gchar* field)
|
|
||||||
{
|
|
||||||
const gchar* val = _find_json_field(json, field);
|
|
||||||
if (!val || *val != '"')
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
const gchar* start = val + 1;
|
|
||||||
for (const gchar* p = start; *p; p++) {
|
|
||||||
if (*p == '\\' && *(p + 1) != '\0') {
|
|
||||||
p++; /* skip the escaped character; loop ++ advances past it */
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (*p == '"') {
|
|
||||||
return _json_unescape_substring(start, p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
* Provider Management
|
* Provider Management
|
||||||
* ======================================================================== */
|
* ======================================================================== */
|
||||||
@@ -377,31 +296,13 @@ ai_client_init(void)
|
|||||||
/* Load saved API keys from config */
|
/* Load saved API keys from config */
|
||||||
ai_load_keys();
|
ai_load_keys();
|
||||||
|
|
||||||
/* Load cached models, persisted default-model and custom settings for
|
/* Load cached models for all providers */
|
||||||
* all providers. */
|
|
||||||
GList* provider_list = ai_list_providers();
|
GList* provider_list = ai_list_providers();
|
||||||
for (GList* curr = provider_list; curr; curr = g_list_next(curr)) {
|
GList* models_curr = provider_list;
|
||||||
AIProvider* provider = (AIProvider*)curr->data;
|
while (models_curr) {
|
||||||
|
AIProvider* provider = (AIProvider*)models_curr->data;
|
||||||
_ai_load_models_for_provider(provider);
|
_ai_load_models_for_provider(provider);
|
||||||
|
models_curr = g_list_next(models_curr);
|
||||||
auto_gchar gchar* dm = prefs_ai_get_default_model(provider->name);
|
|
||||||
if (dm && dm[0] != '\0') {
|
|
||||||
g_free(provider->default_model);
|
|
||||||
provider->default_model = g_strdup(dm);
|
|
||||||
}
|
|
||||||
|
|
||||||
GList* settings = prefs_ai_list_settings(provider->name);
|
|
||||||
for (GList* s = settings; s; s = g_list_next(s)) {
|
|
||||||
const gchar* name = (const gchar*)s->data;
|
|
||||||
auto_gchar gchar* val = prefs_ai_get_setting(provider->name, name);
|
|
||||||
if (val) {
|
|
||||||
if (!provider->settings) {
|
|
||||||
provider->settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
|
||||||
}
|
|
||||||
g_hash_table_insert(provider->settings, g_strdup(name), g_strdup(val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prefs_free_ai_settings(settings);
|
|
||||||
}
|
}
|
||||||
g_list_free(provider_list);
|
g_list_free(provider_list);
|
||||||
|
|
||||||
@@ -497,9 +398,8 @@ ai_remove_provider(const gchar* name)
|
|||||||
if (!name || !providers)
|
if (!name || !providers)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
/* Wipe the whole [ai/<name>] subsection from config — url, key,
|
/* Remove from config before removing from hash table */
|
||||||
* default_model, models, custom settings — in one call. */
|
prefs_ai_remove_provider(name);
|
||||||
prefs_ai_remove_section(name);
|
|
||||||
|
|
||||||
/* Sync autocomplete before removing */
|
/* Sync autocomplete before removing */
|
||||||
autocomplete_remove(providers_ac, name);
|
autocomplete_remove(providers_ac, name);
|
||||||
@@ -601,7 +501,6 @@ ai_set_provider_default_model(const gchar* provider_name, const gchar* model)
|
|||||||
|
|
||||||
g_free(provider->default_model);
|
g_free(provider->default_model);
|
||||||
provider->default_model = g_strdup(model);
|
provider->default_model = g_strdup(model);
|
||||||
prefs_ai_set_default_model(provider_name, model);
|
|
||||||
log_info("Default model for provider '%s' set to: %s", provider_name, model);
|
log_info("Default model for provider '%s' set to: %s", provider_name, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,11 +535,9 @@ ai_set_provider_setting(const gchar* provider_name, const gchar* setting, const
|
|||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
g_hash_table_insert(provider->settings, g_strdup(setting), g_strdup(value));
|
g_hash_table_insert(provider->settings, g_strdup(setting), g_strdup(value));
|
||||||
prefs_ai_set_setting(provider_name, setting, value);
|
|
||||||
log_info("Setting '%s' for provider '%s' set to: %s", setting, provider_name, value);
|
log_info("Setting '%s' for provider '%s' set to: %s", setting, provider_name, value);
|
||||||
} else {
|
} else {
|
||||||
g_hash_table_remove(provider->settings, setting);
|
g_hash_table_remove(provider->settings, setting);
|
||||||
prefs_ai_remove_setting(provider_name, setting);
|
|
||||||
log_info("Setting '%s' removed for provider '%s'", setting, provider_name);
|
log_info("Setting '%s' removed for provider '%s'", setting, provider_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,11 +609,8 @@ _curl_exec_and_handle(CURL* curl, const gchar* url, struct curl_slist* headers,
|
|||||||
log_error("Request failed for '%s': %s", provider->name, error_msg);
|
log_error("Request failed for '%s': %s", provider->name, error_msg);
|
||||||
_aiwin_display_error(user_data, error_msg);
|
_aiwin_display_error(user_data, error_msg);
|
||||||
} else if (http_code >= 400) {
|
} else if (http_code >= 400) {
|
||||||
auto_gchar gchar* parsed = ai_parse_error_message(response->data);
|
auto_gchar gchar* error_msg = g_strdup_printf("HTTP %ld: %s", http_code,
|
||||||
auto_gchar gchar* error_msg = parsed
|
response->data ? response->data : "Unknown error");
|
||||||
? g_strdup_printf("HTTP %ld: %s", http_code, parsed)
|
|
||||||
: g_strdup_printf("HTTP %ld: %s", http_code,
|
|
||||||
response->data && strlen(response->data) > 0 ? response->data : "Unknown error");
|
|
||||||
log_error("Request HTTP error for '%s': %s", provider->name, error_msg);
|
log_error("Request HTTP error for '%s': %s", provider->name, error_msg);
|
||||||
_aiwin_display_error(user_data, error_msg);
|
_aiwin_display_error(user_data, error_msg);
|
||||||
} else {
|
} else {
|
||||||
@@ -1085,18 +979,9 @@ _models_response_handler(AIProvider* provider, const gchar* response, gpointer u
|
|||||||
{
|
{
|
||||||
_parse_and_cache_models(provider, response);
|
_parse_and_cache_models(provider, response);
|
||||||
|
|
||||||
gint count = g_list_length(provider->models);
|
auto_gchar gchar* msg = g_strdup_printf("Cached %d models for provider '%s'. Use '/ai start <provider> <model>' to start a chat.",
|
||||||
GString* msg = g_string_new(NULL);
|
g_list_length(provider->models), provider->name);
|
||||||
g_string_append_printf(msg, "Cached %d models for provider '%s':", count, provider->name);
|
_aiwin_display_response(user_data, msg);
|
||||||
for (GList* curr = provider->models; curr; curr = g_list_next(curr)) {
|
|
||||||
g_string_append_printf(msg, "\n %s", (const gchar*)curr->data);
|
|
||||||
}
|
|
||||||
if (count > 0) {
|
|
||||||
g_string_append_printf(msg, "\nUse '/ai start %s <model>' to start a chat.", provider->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_gchar gchar* out = g_string_free(msg, FALSE);
|
|
||||||
_aiwin_display_response(user_data, out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static gpointer
|
static gpointer
|
||||||
@@ -1371,30 +1256,139 @@ ai_parse_response(const gchar* response_json)
|
|||||||
if (!response_json || strlen(response_json) == 0)
|
if (!response_json || strlen(response_json) == 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
/* Perplexity /v1/agent: {"output":[{"content":[{"text":"...","type":"output_text"}]}]}
|
/* Try Perplexity /v1/agent format first: look for "text":"..." inside output array
|
||||||
* Legacy OpenAI: {"choices":[{"message":{"content":"..."}}]} */
|
* Response: {"output":[{"content":[{"text":"...","type":"output_text"}]}]} */
|
||||||
gchar* text = _extract_json_string(response_json, "text");
|
const gchar* text_start = strstr(response_json, "\"text\":\"");
|
||||||
if (text)
|
if (text_start) {
|
||||||
return text;
|
text_start += strlen("\"text\":\"");
|
||||||
|
const gchar* p = text_start;
|
||||||
|
while (*p) {
|
||||||
|
if (*p == '\\' && *(p + 1) == '"') {
|
||||||
|
p += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
gsize len = p - text_start;
|
||||||
|
gchar* result = g_new0(gchar, len + 1);
|
||||||
|
gchar* out = result;
|
||||||
|
const gchar* in = text_start;
|
||||||
|
while (in < p) {
|
||||||
|
if (*in == '\\' && *(in + 1) == '"') {
|
||||||
|
*out++ = '"';
|
||||||
|
in += 2;
|
||||||
|
} else if (*in == '\\' && *(in + 1) == 'n') {
|
||||||
|
*out++ = '\n';
|
||||||
|
in += 2;
|
||||||
|
} else {
|
||||||
|
*out++ = *in++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out = '\0';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return _extract_json_string(response_json, "content");
|
/* Try legacy OpenAI format: "content":"..."
|
||||||
|
* Response: {"choices":[{"message":{"content":"..."}}]} */
|
||||||
|
const gchar* content_start = strstr(response_json, "\"content\":\"");
|
||||||
|
if (!content_start)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
content_start += strlen("\"content\":\"");
|
||||||
|
|
||||||
|
/* Find the closing quote, accounting for escaped quotes */
|
||||||
|
const gchar* p = content_start;
|
||||||
|
while (*p) {
|
||||||
|
if (*p == '\\' && *(p + 1) == '"') {
|
||||||
|
/* Escaped quote, skip both characters */
|
||||||
|
p += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
/* Found unescaped closing quote */
|
||||||
|
gsize len = p - content_start;
|
||||||
|
/* Unescape the content: convert \" back to " */
|
||||||
|
gchar* result = g_new0(gchar, len + 1);
|
||||||
|
gchar* out = result;
|
||||||
|
const gchar* in = content_start;
|
||||||
|
while (in < p) {
|
||||||
|
if (*in == '\\' && *(in + 1) == '"') {
|
||||||
|
*out++ = '"';
|
||||||
|
in += 2;
|
||||||
|
} else if (*in == '\\' && *(in + 1) == 'n') {
|
||||||
|
*out++ = '\n';
|
||||||
|
in += 2;
|
||||||
|
} else {
|
||||||
|
*out++ = *in++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out = '\0';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extract the human-readable error.message from a provider error envelope:
|
/**
|
||||||
* {"error":{"message":"...","type":"...","code":...}}
|
* Extract error message from an API error JSON response.
|
||||||
* Scoped to the substring after "error": so unrelated top-level "message"
|
* Handles format: {"error":{"message":"...","type":"...","code":...}}
|
||||||
* fields do not get picked up. */
|
*/
|
||||||
gchar*
|
gchar*
|
||||||
ai_parse_error_message(const gchar* error_json)
|
ai_parse_error_message(const gchar* error_json)
|
||||||
{
|
{
|
||||||
if (!error_json || strlen(error_json) == 0)
|
if (!error_json || strlen(error_json) == 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
/* Look for "message":"..." inside "error" object */
|
||||||
const gchar* error_obj = strstr(error_json, "\"error\":");
|
const gchar* error_obj = strstr(error_json, "\"error\":");
|
||||||
if (!error_obj)
|
if (!error_obj)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
return _extract_json_string(error_obj, "message");
|
const gchar* message_key = strstr(error_obj, "\"message\":\"");
|
||||||
|
if (!message_key)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
message_key += strlen("\"message\":\"");
|
||||||
|
const gchar* msg_start = message_key;
|
||||||
|
const gchar* p = msg_start;
|
||||||
|
while (*p) {
|
||||||
|
if (*p == '\\' && *(p + 1) == '"') {
|
||||||
|
p += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
gsize len = p - msg_start;
|
||||||
|
gchar* result = g_new0(gchar, len + 1);
|
||||||
|
gchar* out = result;
|
||||||
|
const gchar* in = msg_start;
|
||||||
|
while (in < p) {
|
||||||
|
if (*in == '\\' && *(in + 1) == '"') {
|
||||||
|
*out++ = '"';
|
||||||
|
in += 2;
|
||||||
|
} else if (*in == '\\' && *(in + 1) == 'n') {
|
||||||
|
*out++ = '\n';
|
||||||
|
in += 2;
|
||||||
|
} else if (*in == '\\' && *(in + 1) == '\\') {
|
||||||
|
*out++ = '\\';
|
||||||
|
in += 2;
|
||||||
|
} else if (*in == '\\' && *(in + 1) == 't') {
|
||||||
|
*out++ = '\t';
|
||||||
|
in += 2;
|
||||||
|
} else {
|
||||||
|
*out++ = *in++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out = '\0';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static gpointer
|
static gpointer
|
||||||
@@ -1419,10 +1413,10 @@ _ai_request_thread(gpointer data)
|
|||||||
* list modifications). We must snapshot all fields atomically.
|
* list modifications). We must snapshot all fields atomically.
|
||||||
* ===================================================================== */
|
* ===================================================================== */
|
||||||
pthread_mutex_lock(&session->lock);
|
pthread_mutex_lock(&session->lock);
|
||||||
auto_gchar gchar* local_provider_name = g_strdup(session->provider_name);
|
gchar* local_provider_name = g_strdup(session->provider_name);
|
||||||
AIProvider* local_provider = ai_provider_ref(session->provider);
|
AIProvider* local_provider = ai_provider_ref(session->provider);
|
||||||
auto_gchar gchar* local_model = g_strdup(session->model);
|
gchar* local_model = g_strdup(session->model);
|
||||||
auto_gchar gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
gchar* local_api_key = g_strdup(session->api_key ? session->api_key : "");
|
||||||
pthread_mutex_unlock(&session->lock);
|
pthread_mutex_unlock(&session->lock);
|
||||||
|
|
||||||
log_debug("[AI-THREAD] Session: %s/%s", local_provider_name, local_model);
|
log_debug("[AI-THREAD] Session: %s/%s", local_provider_name, local_model);
|
||||||
@@ -1434,8 +1428,6 @@ _ai_request_thread(gpointer data)
|
|||||||
local_provider_name, local_provider_name);
|
local_provider_name, local_provider_name);
|
||||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||||
_aiwin_display_error(user_data, error_msg);
|
_aiwin_display_error(user_data, error_msg);
|
||||||
ai_provider_unref(local_provider);
|
|
||||||
ai_session_unref(session);
|
|
||||||
g_free(args);
|
g_free(args);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -1446,8 +1438,6 @@ _ai_request_thread(gpointer data)
|
|||||||
log_error("AI request failed for %s/%s: Failed to initialize curl",
|
log_error("AI request failed for %s/%s: Failed to initialize curl",
|
||||||
local_provider_name, local_model);
|
local_provider_name, local_model);
|
||||||
_aiwin_display_error(user_data, "Failed to initialize curl.");
|
_aiwin_display_error(user_data, "Failed to initialize curl.");
|
||||||
ai_provider_unref(local_provider);
|
|
||||||
ai_session_unref(session);
|
|
||||||
g_free(args);
|
g_free(args);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -1496,7 +1486,6 @@ _ai_request_thread(gpointer data)
|
|||||||
long http_code = 0;
|
long http_code = 0;
|
||||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
log_debug("[AI-THREAD] HTTP response code: %ld", http_code);
|
log_debug("[AI-THREAD] HTTP response code: %ld", http_code);
|
||||||
auto_gchar gchar* response_data = g_steal_pointer(&response.data);
|
|
||||||
|
|
||||||
if (res != CURLE_OK) {
|
if (res != CURLE_OK) {
|
||||||
auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res));
|
auto_gchar gchar* error_msg = g_strdup(curl_easy_strerror(res));
|
||||||
@@ -1505,14 +1494,17 @@ _ai_request_thread(gpointer data)
|
|||||||
} else if (http_code >= 400) {
|
} else if (http_code >= 400) {
|
||||||
/* Handle HTTP errors */
|
/* Handle HTTP errors */
|
||||||
log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s",
|
log_debug("[AI-THREAD] HTTP error response body (%zu bytes): %s",
|
||||||
response.size, response_data);
|
response.size, response.data ? response.data : "NULL");
|
||||||
/* Try to extract the actual error message from the JSON response */
|
/* Try to extract the actual error message from the JSON response */
|
||||||
auto_gchar gchar* parsed_error = ai_parse_error_message(response_data);
|
auto_gchar gchar* parsed_error = ai_parse_error_message(response.data);
|
||||||
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response_data && strlen(response_data) > 0 ? response_data : "Unknown error");
|
auto_gchar gchar* error_msg = parsed_error ? g_strdup_printf("HTTP %ld: %s", http_code, parsed_error) : g_strdup_printf("HTTP %ld: %s", http_code, response.data && strlen(response.data) > 0 ? response.data : "Unknown error");
|
||||||
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
log_error("AI request failed for %s/%s: %s", local_provider_name, local_model, error_msg);
|
||||||
_aiwin_display_error(user_data, error_msg);
|
_aiwin_display_error(user_data, error_msg);
|
||||||
} else {
|
} else {
|
||||||
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response_data);
|
/* Parse response - transfer ownership to auto_gchar for cleanup */
|
||||||
|
log_debug("[AI-THREAD] Raw API response (%zu bytes): %s", response.size, response.data ? response.data : "NULL");
|
||||||
|
auto_gchar gchar* response_data = response.data;
|
||||||
|
response.data = NULL;
|
||||||
auto_gchar gchar* content = ai_parse_response(response_data);
|
auto_gchar gchar* content = ai_parse_response(response_data);
|
||||||
if (content) {
|
if (content) {
|
||||||
/* Add assistant response to history (under lock) */
|
/* Add assistant response to history (under lock) */
|
||||||
@@ -1533,9 +1525,12 @@ _ai_request_thread(gpointer data)
|
|||||||
curl_slist_free_all(headers);
|
curl_slist_free_all(headers);
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
/* Cleanup local copies */
|
||||||
|
g_free(local_provider_name);
|
||||||
ai_provider_unref(local_provider);
|
ai_provider_unref(local_provider);
|
||||||
|
g_free(local_model);
|
||||||
|
g_free(local_api_key);
|
||||||
|
|
||||||
ai_session_unref(session);
|
|
||||||
g_free(args);
|
g_free(args);
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|||||||
@@ -106,6 +106,15 @@ GList* ai_list_providers(void);
|
|||||||
*/
|
*/
|
||||||
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
|
gchar* ai_providers_find(const char* const search_str, gboolean previous, void* context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a model name for autocomplete.
|
||||||
|
* @param search_str The search string
|
||||||
|
* @param previous Whether to go to previous match
|
||||||
|
* @param context ProfAiWin* for the current session
|
||||||
|
* @return Model name, or NULL if not found
|
||||||
|
*/
|
||||||
|
gchar* ai_models_find(const char* const search_str, gboolean previous, void* context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the provider autocomplete state.
|
* Reset the provider autocomplete state.
|
||||||
* Called from cmd_ac_reset() to clear autocomplete state.
|
* Called from cmd_ac_reset() to clear autocomplete state.
|
||||||
|
|||||||
@@ -8516,8 +8516,7 @@ _cmd_execute_default(ProfWin* window, const char* inp)
|
|||||||
case WIN_AI:
|
case WIN_AI:
|
||||||
{
|
{
|
||||||
ProfAiWin* aiwin = (ProfAiWin*)window;
|
ProfAiWin* aiwin = (ProfAiWin*)window;
|
||||||
auto_gchar gchar* stanza_id = connection_create_stanza_id();
|
cl_ev_send_ai_msg(aiwin, inp, connection_create_stanza_id());
|
||||||
cl_ev_send_ai_msg(aiwin, inp, stanza_id);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -10818,6 +10817,7 @@ cmd_ai(ProfWin* window, const char* const command, gchar** args)
|
|||||||
|
|
||||||
cons_show("Use '/ai start' to begin a chat (uses default provider/model).");
|
cons_show("Use '/ai start' to begin a chat (uses default provider/model).");
|
||||||
cons_show("Use '/ai models <provider>' to fetch available models.");
|
cons_show("Use '/ai models <provider>' to fetch available models.");
|
||||||
|
cons_show("Available models: https://models.litellm.ai/");
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10981,9 +10981,6 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
|||||||
}
|
}
|
||||||
log_debug("[AI-CMD] wins_new_ai() returned successfully, window type: %d", ai_win->type);
|
log_debug("[AI-CMD] wins_new_ai() returned successfully, window type: %d", ai_win->type);
|
||||||
|
|
||||||
// Release the reference held by cmd_ai_start, since the window now holds one
|
|
||||||
ai_session_unref(session);
|
|
||||||
|
|
||||||
// Add welcome message to the AI window
|
// Add welcome message to the AI window
|
||||||
log_debug("[AI-CMD] Adding welcome messages...");
|
log_debug("[AI-CMD] Adding welcome messages...");
|
||||||
win_println(ai_win, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
win_println(ai_win, THEME_DEFAULT, "-", "AI Chat: %s/%s", provider_name, model);
|
||||||
@@ -11177,24 +11174,30 @@ cmd_ai_clear(ProfWin* window, const char* const command, gchar** args)
|
|||||||
gboolean
|
gboolean
|
||||||
cmd_ai_providers(ProfWin* window, const char* const command, gchar** args)
|
cmd_ai_providers(ProfWin* window, const char* const command, gchar** args)
|
||||||
{
|
{
|
||||||
|
if (args[1] == NULL || g_strcmp0(args[1], "list") != 0) {
|
||||||
|
// List all available providers
|
||||||
|
cons_show("Available AI providers:");
|
||||||
|
cons_show("");
|
||||||
|
cons_show(" openai - OpenAI API (https://api.openai.com)");
|
||||||
|
cons_show(" perplexity - Perplexity API (https://api.perplexity.ai)");
|
||||||
|
cons_show("");
|
||||||
|
cons_show("Add custom providers with: /ai set provider <name> <url>");
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List configured providers with key status
|
||||||
cons_show("Configured providers:");
|
cons_show("Configured providers:");
|
||||||
cons_show("");
|
cons_show("");
|
||||||
|
|
||||||
GList* providers = ai_list_providers();
|
GList* providers = ai_list_providers();
|
||||||
if (!providers) {
|
|
||||||
cons_show(" (none configured)");
|
|
||||||
cons_show("");
|
|
||||||
cons_show("Add a provider with: /ai set provider <name> <url>");
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
|
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
|
||||||
AIProvider* provider = curr->data;
|
AIProvider* provider = curr->data;
|
||||||
auto_gchar gchar* key = ai_get_provider_key(provider->name);
|
gchar* key = ai_get_provider_key(provider->name);
|
||||||
cons_show(" %s", provider->name);
|
cons_show(" %s", provider->name);
|
||||||
cons_show(" URL: %s", provider->api_url);
|
cons_show(" URL: %s", provider->api_url);
|
||||||
cons_show(" Key: %s", key ? "configured" : "NOT configured");
|
cons_show(" Key: %s", key ? "configured" : "NOT configured");
|
||||||
cons_show("");
|
cons_show("");
|
||||||
|
g_free(key);
|
||||||
}
|
}
|
||||||
g_list_free(providers);
|
g_list_free(providers);
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,6 @@
|
|||||||
#define PREF_GROUP_PLUGINS "plugins"
|
#define PREF_GROUP_PLUGINS "plugins"
|
||||||
#define PREF_GROUP_EXECUTABLES "executables"
|
#define PREF_GROUP_EXECUTABLES "executables"
|
||||||
#define PREF_GROUP_AI "ai"
|
#define PREF_GROUP_AI "ai"
|
||||||
#define PREF_GROUP_AI_PREFIX "ai/" /* per-provider subsection prefix: [ai/<provider>] */
|
|
||||||
#define PREF_AI_SETTING_PREFIX "setting." /* per-provider custom setting key prefix */
|
|
||||||
|
|
||||||
#define INPBLOCK_DEFAULT 1000
|
#define INPBLOCK_DEFAULT 1000
|
||||||
|
|
||||||
@@ -245,69 +243,6 @@ _prefs_load(void)
|
|||||||
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.jid", NULL);
|
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.jid", NULL);
|
||||||
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.name", NULL);
|
g_key_file_remove_key(prefs, PREF_GROUP_UI, "titlebar.muc.title.name", NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Migrate legacy flat [ai] layout
|
|
||||||
* openai_url=..., openai=<token>, openai_models=..., openai_default_model=...
|
|
||||||
* into per-provider subsections
|
|
||||||
* [ai/openai] url=..., key=..., models=..., default_model=...
|
|
||||||
* Idempotent: once the flat keys are gone, this is a no-op. */
|
|
||||||
if (g_key_file_has_group(prefs, PREF_GROUP_AI)) {
|
|
||||||
gsize key_count = 0;
|
|
||||||
auto_gcharv gchar** all_keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &key_count, NULL);
|
|
||||||
|
|
||||||
/* Collect provider names from <name>_url keys first so we can migrate
|
|
||||||
* the whole bundle (url + token + models + default_model) atomically
|
|
||||||
* per provider, without misclassifying unrelated flat keys. */
|
|
||||||
GHashTable* to_migrate = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
|
||||||
for (gsize i = 0; i < key_count; i++) {
|
|
||||||
const gchar* k = all_keys[i];
|
|
||||||
if (g_str_has_suffix(k, "_url") && !g_str_has_suffix(k, "_models_url")) {
|
|
||||||
gsize klen = strlen(k);
|
|
||||||
if (klen > 4) {
|
|
||||||
g_hash_table_add(to_migrate, g_strndup(k, klen - 4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GList* provider_names = g_hash_table_get_keys(to_migrate);
|
|
||||||
for (GList* curr = provider_names; curr; curr = g_list_next(curr)) {
|
|
||||||
const gchar* name = (const gchar*)curr->data;
|
|
||||||
auto_gchar gchar* group = g_strconcat(PREF_GROUP_AI_PREFIX, name, NULL);
|
|
||||||
|
|
||||||
const struct
|
|
||||||
{
|
|
||||||
const gchar* flat_suffix;
|
|
||||||
const gchar* new_key;
|
|
||||||
} map[] = {
|
|
||||||
{ "_url", "url" },
|
|
||||||
{ "_models", "models" },
|
|
||||||
{ "_default_model", "default_model" },
|
|
||||||
{ NULL, NULL },
|
|
||||||
};
|
|
||||||
for (int j = 0; map[j].flat_suffix; j++) {
|
|
||||||
auto_gchar gchar* flat_key = g_strconcat(name, map[j].flat_suffix, NULL);
|
|
||||||
if (g_key_file_has_key(prefs, PREF_GROUP_AI, flat_key, NULL)) {
|
|
||||||
auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, flat_key, NULL);
|
|
||||||
if (val) {
|
|
||||||
g_key_file_set_string(prefs, group, map[j].new_key, val);
|
|
||||||
}
|
|
||||||
g_key_file_remove_key(prefs, PREF_GROUP_AI, flat_key, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Token was stored as the bare provider name in [ai]. */
|
|
||||||
if (g_key_file_has_key(prefs, PREF_GROUP_AI, name, NULL)) {
|
|
||||||
auto_gchar gchar* val = g_key_file_get_string(prefs, PREF_GROUP_AI, name, NULL);
|
|
||||||
if (val) {
|
|
||||||
g_key_file_set_string(prefs, group, "key", val);
|
|
||||||
}
|
|
||||||
g_key_file_remove_key(prefs, PREF_GROUP_AI, name, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g_list_free(provider_names);
|
|
||||||
g_hash_table_destroy(to_migrate);
|
|
||||||
}
|
|
||||||
|
|
||||||
_save_prefs();
|
_save_prefs();
|
||||||
|
|
||||||
boolean_choice_ac = autocomplete_new();
|
boolean_choice_ac = autocomplete_new();
|
||||||
@@ -1773,103 +1708,73 @@ _save_prefs(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
* AI provider preferences
|
* AI Token Management
|
||||||
*
|
|
||||||
* Each provider lives in its own [ai/<name>] subsection of the keyfile with:
|
|
||||||
* url=...
|
|
||||||
* key=<api token>
|
|
||||||
* default_model=...
|
|
||||||
* models=model1;model2;...
|
|
||||||
* setting.<custom>=value (e.g. setting.tools=enabled)
|
|
||||||
* The bare [ai] section keeps only the global "defaults_initialized" sentinel.
|
|
||||||
* Legacy flat-key layouts are migrated on first load in _prefs_load().
|
|
||||||
* ======================================================================== */
|
* ======================================================================== */
|
||||||
|
|
||||||
static gchar*
|
|
||||||
_ai_section(const char* const provider)
|
|
||||||
{
|
|
||||||
return g_strconcat(PREF_GROUP_AI_PREFIX, provider, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
_ai_set_string(const char* const provider, const char* const key, const char* const value)
|
|
||||||
{
|
|
||||||
if (!provider || !key || !value || !prefs)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
auto_gchar gchar* group = _ai_section(provider);
|
|
||||||
g_key_file_set_string(prefs, group, key, value);
|
|
||||||
_save_prefs();
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char*
|
|
||||||
_ai_get_string(const char* const provider, const char* const key)
|
|
||||||
{
|
|
||||||
if (!provider || !key || !prefs)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
auto_gchar gchar* group = _ai_section(provider);
|
|
||||||
if (!g_key_file_has_key(prefs, group, key, NULL))
|
|
||||||
return NULL;
|
|
||||||
return g_key_file_get_string(prefs, group, key, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
_ai_remove_string(const char* const provider, const char* const key)
|
|
||||||
{
|
|
||||||
if (!provider || !key || !prefs)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
auto_gchar gchar* group = _ai_section(provider);
|
|
||||||
if (!g_key_file_has_key(prefs, group, key, NULL))
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
g_key_file_remove_key(prefs, group, key, NULL);
|
|
||||||
_save_prefs();
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Token --- */
|
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
prefs_ai_set_token(const char* const provider, const char* const token)
|
prefs_ai_set_token(const char* const provider, const char* const token)
|
||||||
{
|
{
|
||||||
if (!provider || !prefs)
|
if (!provider)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
if (!token)
|
|
||||||
|
if (!prefs)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
g_key_file_set_string(prefs, PREF_GROUP_AI, provider, token);
|
||||||
|
_save_prefs();
|
||||||
|
return TRUE;
|
||||||
|
} else {
|
||||||
return prefs_ai_remove_token(provider);
|
return prefs_ai_remove_token(provider);
|
||||||
return _ai_set_string(provider, "key", token);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
prefs_ai_remove_token(const char* const provider)
|
prefs_ai_remove_token(const char* const provider)
|
||||||
{
|
{
|
||||||
return _ai_remove_string(provider, "key");
|
if (!provider)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (!prefs)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, provider, NULL)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_key_file_remove_key(prefs, PREF_GROUP_AI, provider, NULL);
|
||||||
|
_save_prefs();
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
char*
|
char*
|
||||||
prefs_ai_get_token(const char* const provider)
|
prefs_ai_get_token(const char* const provider)
|
||||||
{
|
{
|
||||||
return _ai_get_string(provider, "key");
|
if (!provider || !prefs)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
return g_key_file_get_string(prefs, PREF_GROUP_AI, provider, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
GList*
|
GList*
|
||||||
prefs_ai_list_tokens(void)
|
prefs_ai_list_tokens(void)
|
||||||
{
|
{
|
||||||
if (!prefs)
|
if (!prefs || !g_key_file_has_group(prefs, PREF_GROUP_AI)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
GList* result = NULL;
|
GList* result = NULL;
|
||||||
GList* providers = prefs_ai_list_providers();
|
gsize len;
|
||||||
for (GList* curr = providers; curr; curr = g_list_next(curr)) {
|
auto_gcharv gchar** keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &len, NULL);
|
||||||
const gchar* name = (const gchar*)curr->data;
|
|
||||||
auto_gchar gchar* tok = prefs_ai_get_token(name);
|
for (gsize i = 0; i < len; i++) {
|
||||||
if (tok && tok[0] != '\0') {
|
char* name = keys[i];
|
||||||
|
auto_gchar gchar* value = g_key_file_get_string(prefs, PREF_GROUP_AI, name, NULL);
|
||||||
|
if (value) {
|
||||||
result = g_list_append(result, g_strdup(name));
|
result = g_list_append(result, g_strdup(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs_free_ai_providers(providers);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1881,45 +1786,72 @@ prefs_free_ai_tokens(GList* tokens)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Models cache --- */
|
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count)
|
prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count)
|
||||||
{
|
{
|
||||||
if (!provider || count <= 0 || !prefs)
|
if (!provider || count <= 0)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
|
if (!prefs)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
/* Build semicolon-separated model list */
|
||||||
GString* model_str = g_string_new("");
|
GString* model_str = g_string_new("");
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
if (i > 0)
|
if (i > 0) {
|
||||||
g_string_append_c(model_str, ';');
|
g_string_append_c(model_str, ';');
|
||||||
|
}
|
||||||
g_string_append(model_str, models[i]);
|
g_string_append(model_str, models[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean ok = _ai_set_string(provider, "models", model_str->str);
|
g_key_file_set_string(prefs, PREF_GROUP_AI, g_strconcat(provider, "_models", NULL), model_str->str);
|
||||||
g_string_free(model_str, TRUE);
|
g_string_free(model_str, TRUE);
|
||||||
return ok;
|
_save_prefs();
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
prefs_ai_remove_models(const char* const provider)
|
prefs_ai_remove_models(const char* const provider)
|
||||||
{
|
{
|
||||||
return _ai_remove_string(provider, "models");
|
if (!provider)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (!prefs)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
auto_gchar gchar* key = g_strconcat(provider, "_models", NULL);
|
||||||
|
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, key, NULL)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_key_file_remove_key(prefs, PREF_GROUP_AI, key, NULL);
|
||||||
|
_save_prefs();
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
GList*
|
GList*
|
||||||
prefs_ai_get_models(const char* const provider)
|
prefs_ai_get_models(const char* const provider)
|
||||||
{
|
{
|
||||||
auto_gchar gchar* model_str = _ai_get_string(provider, "models");
|
if (!provider || !prefs)
|
||||||
if (!model_str || model_str[0] == '\0')
|
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
auto_gchar gchar* key = g_strconcat(provider, "_models", NULL);
|
||||||
|
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, key, NULL)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_gchar gchar* model_str = g_key_file_get_string(prefs, PREF_GROUP_AI, key, NULL);
|
||||||
|
if (!model_str || strlen(model_str) == 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
GList* result = NULL;
|
GList* result = NULL;
|
||||||
gchar** models = g_strsplit(model_str, ";", -1);
|
gchar** models = g_strsplit(model_str, ";", -1);
|
||||||
for (int i = 0; models[i] != NULL; i++) {
|
for (int i = 0; models[i] != NULL; i++) {
|
||||||
result = g_list_append(result, g_strdup(models[i]));
|
result = g_list_append(result, g_strdup(models[i]));
|
||||||
}
|
}
|
||||||
g_strfreev(models);
|
g_strfreev(models);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1931,89 +1863,78 @@ prefs_free_ai_models(GList* models)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Default model --- */
|
/* ========================================================================
|
||||||
|
* AI Provider Management
|
||||||
gboolean
|
* ======================================================================== */
|
||||||
prefs_ai_set_default_model(const char* const provider, const char* const model)
|
|
||||||
{
|
|
||||||
if (!provider || !model || !prefs)
|
|
||||||
return FALSE;
|
|
||||||
if (model[0] == '\0')
|
|
||||||
return prefs_ai_remove_default_model(provider);
|
|
||||||
return _ai_set_string(provider, "default_model", model);
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean
|
|
||||||
prefs_ai_remove_default_model(const char* const provider)
|
|
||||||
{
|
|
||||||
return _ai_remove_string(provider, "default_model");
|
|
||||||
}
|
|
||||||
|
|
||||||
char*
|
|
||||||
prefs_ai_get_default_model(const char* const provider)
|
|
||||||
{
|
|
||||||
return _ai_get_string(provider, "default_model");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Provider URL --- */
|
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
prefs_ai_set_provider(const char* const provider, const char* const url)
|
prefs_ai_set_provider(const char* const provider, const char* const url)
|
||||||
{
|
{
|
||||||
if (!provider || !url)
|
if (!provider || !url)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
return _ai_set_string(provider, "url", url);
|
|
||||||
|
if (!prefs)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
/* Store URL with "_url" suffix to avoid collision with API key storage */
|
||||||
|
g_key_file_set_string(prefs, PREF_GROUP_AI, g_strconcat(provider, "_url", NULL), url);
|
||||||
|
_save_prefs();
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
prefs_ai_remove_provider(const char* const provider)
|
prefs_ai_remove_provider(const char* const provider)
|
||||||
{
|
{
|
||||||
/* Provider presence is identified by the url key; removing it is what
|
if (!provider)
|
||||||
* makes prefs_ai_list_providers stop returning this name. The remaining
|
return FALSE;
|
||||||
* sub-keys are wiped through prefs_ai_remove_section(). */
|
|
||||||
return _ai_remove_string(provider, "url");
|
if (!prefs)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
auto_gchar gchar* url_key = g_strconcat(provider, "_url", NULL);
|
||||||
|
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, url_key, NULL)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_key_file_remove_key(prefs, PREF_GROUP_AI, url_key, NULL);
|
||||||
|
_save_prefs();
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
char*
|
char*
|
||||||
prefs_ai_get_provider_url(const char* const provider)
|
prefs_ai_get_provider_url(const char* const provider)
|
||||||
{
|
|
||||||
return _ai_get_string(provider, "url");
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean
|
|
||||||
prefs_ai_remove_section(const char* const provider)
|
|
||||||
{
|
{
|
||||||
if (!provider || !prefs)
|
if (!provider || !prefs)
|
||||||
return FALSE;
|
return NULL;
|
||||||
|
|
||||||
auto_gchar gchar* group = _ai_section(provider);
|
auto_gchar gchar* key = g_strconcat(provider, "_url", NULL);
|
||||||
if (!g_key_file_has_group(prefs, group))
|
if (!g_key_file_has_key(prefs, PREF_GROUP_AI, key, NULL)) {
|
||||||
return FALSE;
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
g_key_file_remove_group(prefs, group, NULL);
|
return g_key_file_get_string(prefs, PREF_GROUP_AI, key, NULL);
|
||||||
_save_prefs();
|
|
||||||
return TRUE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GList*
|
GList*
|
||||||
prefs_ai_list_providers(void)
|
prefs_ai_list_providers(void)
|
||||||
{
|
{
|
||||||
if (!prefs)
|
if (!prefs || !g_key_file_has_group(prefs, PREF_GROUP_AI)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
GList* result = NULL;
|
GList* result = NULL;
|
||||||
gsize len = 0;
|
gsize len = 0;
|
||||||
auto_gcharv gchar** groups = g_key_file_get_groups(prefs, &len);
|
auto_gcharv gchar** keys = g_key_file_get_keys(prefs, PREF_GROUP_AI, &len, NULL);
|
||||||
if (!groups)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
gsize prefix_len = strlen(PREF_GROUP_AI_PREFIX);
|
|
||||||
for (gsize i = 0; i < len; i++) {
|
for (gsize i = 0; i < len; i++) {
|
||||||
const gchar* g = groups[i];
|
char* key = keys[i];
|
||||||
if (g_str_has_prefix(g, PREF_GROUP_AI_PREFIX) && strlen(g) > prefix_len) {
|
/* Match keys ending with "_url" that are not "_models" suffix */
|
||||||
/* Only count groups that actually identify a provider (have url). */
|
if (g_str_has_suffix(key, "_url") && !g_str_has_suffix(key, "_models_url")) {
|
||||||
if (g_key_file_has_key(prefs, g, "url", NULL)) {
|
/* Extract provider name by removing "_url" suffix */
|
||||||
result = g_list_append(result, g_strdup(g + prefix_len));
|
gsize key_len = strlen(key);
|
||||||
|
if (key_len > 4) {
|
||||||
|
gchar* provider_name = g_strndup(key, key_len - 4);
|
||||||
|
result = g_list_append(result, provider_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2032,106 +1953,38 @@ prefs_free_ai_providers(GList* providers)
|
|||||||
GList*
|
GList*
|
||||||
prefs_ai_get_providers(void)
|
prefs_ai_get_providers(void)
|
||||||
{
|
{
|
||||||
|
/* If user has configured any providers, return those */
|
||||||
GList* configured = prefs_ai_list_providers();
|
GList* configured = prefs_ai_list_providers();
|
||||||
GList* result = NULL;
|
if (configured && g_list_length(configured) > 0) {
|
||||||
|
/* Build result list with name, url pairs */
|
||||||
for (GList* curr = configured; curr; curr = g_list_next(curr)) {
|
GList* result = NULL;
|
||||||
const gchar* name = (const gchar*)curr->data;
|
GList* curr = configured;
|
||||||
auto_gchar gchar* url = prefs_ai_get_provider_url(name);
|
while (curr) {
|
||||||
if (url && strlen(url) > 0) {
|
gchar* name = (gchar*)curr->data;
|
||||||
result = g_list_append(result, g_strdup(name));
|
gchar* url = prefs_ai_get_provider_url(name);
|
||||||
result = g_list_append(result, g_strdup(url));
|
if (url && strlen(url) > 0) {
|
||||||
|
result = g_list_append(result, g_strdup(name));
|
||||||
|
result = g_list_append(result, g_strdup(url));
|
||||||
|
}
|
||||||
|
g_free(url);
|
||||||
|
curr = g_list_next(curr);
|
||||||
}
|
}
|
||||||
|
prefs_free_ai_providers(configured);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
prefs_free_ai_providers(configured);
|
prefs_free_ai_providers(configured);
|
||||||
|
|
||||||
/* Seed openai/perplexity into the in-memory result on very first init
|
prefs_ai_set_provider("openai", "https://api.openai.com/");
|
||||||
* only. In production the "defaults_initialized" sentinel in [ai] is what
|
prefs_ai_set_provider("perplexity", "https://api.perplexity.ai/");
|
||||||
* tells us we've already seeded — a user who removes every provider must
|
|
||||||
* not get them silently re-added on the next start. When prefs is NULL
|
|
||||||
* (e.g. unit tests that bring up ai_client without prefs_load) we still
|
|
||||||
* seed the in-memory list so behaviour matches a fresh first run, but
|
|
||||||
* skip the persistence write. */
|
|
||||||
gboolean already_seeded = prefs && g_key_file_get_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", NULL);
|
|
||||||
if (!already_seeded && !result) {
|
|
||||||
if (prefs) {
|
|
||||||
prefs_ai_set_provider("openai", "https://api.openai.com/");
|
|
||||||
prefs_ai_set_provider("perplexity", "https://api.perplexity.ai/");
|
|
||||||
}
|
|
||||||
result = g_list_append(result, g_strdup("openai"));
|
|
||||||
result = g_list_append(result, g_strdup("https://api.openai.com/"));
|
|
||||||
result = g_list_append(result, g_strdup("perplexity"));
|
|
||||||
result = g_list_append(result, g_strdup("https://api.perplexity.ai/"));
|
|
||||||
}
|
|
||||||
if (prefs && !already_seeded) {
|
|
||||||
g_key_file_set_boolean(prefs, PREF_GROUP_AI, "defaults_initialized", TRUE);
|
|
||||||
_save_prefs();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Custom per-provider settings (setting.<name>) --- */
|
|
||||||
|
|
||||||
gboolean
|
|
||||||
prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value)
|
|
||||||
{
|
|
||||||
if (!provider || !setting)
|
|
||||||
return FALSE;
|
|
||||||
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
|
|
||||||
if (!value)
|
|
||||||
return _ai_remove_string(provider, key);
|
|
||||||
return _ai_set_string(provider, key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean
|
|
||||||
prefs_ai_remove_setting(const char* const provider, const char* const setting)
|
|
||||||
{
|
|
||||||
if (!provider || !setting)
|
|
||||||
return FALSE;
|
|
||||||
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
|
|
||||||
return _ai_remove_string(provider, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
char*
|
|
||||||
prefs_ai_get_setting(const char* const provider, const char* const setting)
|
|
||||||
{
|
|
||||||
if (!provider || !setting)
|
|
||||||
return NULL;
|
|
||||||
auto_gchar gchar* key = g_strconcat(PREF_AI_SETTING_PREFIX, setting, NULL);
|
|
||||||
return _ai_get_string(provider, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
GList*
|
|
||||||
prefs_ai_list_settings(const char* const provider)
|
|
||||||
{
|
|
||||||
if (!provider || !prefs)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
auto_gchar gchar* group = _ai_section(provider);
|
|
||||||
if (!g_key_file_has_group(prefs, group))
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
GList* result = NULL;
|
GList* result = NULL;
|
||||||
gsize len = 0;
|
result = g_list_append(result, g_strdup("openai"));
|
||||||
auto_gcharv gchar** keys = g_key_file_get_keys(prefs, group, &len, NULL);
|
result = g_list_append(result, g_strdup("https://api.openai.com/"));
|
||||||
gsize prefix_len = strlen(PREF_AI_SETTING_PREFIX);
|
result = g_list_append(result, g_strdup("perplexity"));
|
||||||
for (gsize i = 0; i < len; i++) {
|
result = g_list_append(result, g_strdup("https://api.perplexity.ai/"));
|
||||||
if (g_str_has_prefix(keys[i], PREF_AI_SETTING_PREFIX) && strlen(keys[i]) > prefix_len) {
|
|
||||||
result = g_list_append(result, g_strdup(keys[i] + prefix_len));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
prefs_free_ai_settings(GList* settings)
|
|
||||||
{
|
|
||||||
if (settings) {
|
|
||||||
g_list_free_full(settings, g_free);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the preference group for a specific preference
|
// get the preference group for a specific preference
|
||||||
// for example the PREF_BEEP setting ("beep" in .profrc, see _get_key) belongs
|
// for example the PREF_BEEP setting ("beep" in .profrc, see _get_key) belongs
|
||||||
// to the [ui] section.
|
// to the [ui] section.
|
||||||
|
|||||||
@@ -374,27 +374,10 @@ GList* prefs_ai_list_providers(void);
|
|||||||
void prefs_free_ai_providers(GList* providers);
|
void prefs_free_ai_providers(GList* providers);
|
||||||
GList* prefs_ai_get_providers(void);
|
GList* prefs_ai_get_providers(void);
|
||||||
|
|
||||||
/* Wipe the whole [ai/<provider>] subsection (url, key, models, default_model,
|
|
||||||
* custom settings) in one call. */
|
|
||||||
gboolean prefs_ai_remove_section(const char* const provider);
|
|
||||||
|
|
||||||
/* AI model cache management */
|
/* AI model cache management */
|
||||||
gboolean prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count);
|
gboolean prefs_ai_set_models(const char* const provider, const gchar* const* models, gint count);
|
||||||
gboolean prefs_ai_remove_models(const char* const provider);
|
gboolean prefs_ai_remove_models(const char* const provider);
|
||||||
GList* prefs_ai_get_models(const char* const provider);
|
GList* prefs_ai_get_models(const char* const provider);
|
||||||
void prefs_free_ai_models(GList* models);
|
void prefs_free_ai_models(GList* models);
|
||||||
|
|
||||||
/* AI default model per provider */
|
|
||||||
gboolean prefs_ai_set_default_model(const char* const provider, const char* const model);
|
|
||||||
gboolean prefs_ai_remove_default_model(const char* const provider);
|
|
||||||
char* prefs_ai_get_default_model(const char* const provider);
|
|
||||||
|
|
||||||
/* AI per-provider custom settings (e.g. tools, search) — persisted as
|
|
||||||
* setting.<name>=<value> inside [ai/<provider>]. Pass NULL value to remove. */
|
|
||||||
gboolean prefs_ai_set_setting(const char* const provider, const char* const setting, const char* const value);
|
|
||||||
gboolean prefs_ai_remove_setting(const char* const provider, const char* const setting);
|
|
||||||
char* prefs_ai_get_setting(const char* const provider, const char* const setting);
|
|
||||||
GList* prefs_ai_list_settings(const char* const provider);
|
|
||||||
void prefs_free_ai_settings(GList* settings);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -61,22 +61,6 @@ integrity_issue_free(integrity_issue_t* issue)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean
|
|
||||||
log_database_can_recover_messages(void)
|
|
||||||
{
|
|
||||||
if (!active_db_backend) {
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
|
|
||||||
if (g_strcmp0(pref_dblog, "off") == 0) {
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
if (g_strcmp0(pref_dblog, "redact") == 0) {
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean
|
gboolean
|
||||||
log_database_init(ProfAccount* account)
|
log_database_init(ProfAccount* account)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,9 +109,6 @@ ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gb
|
|||||||
void log_database_close(void);
|
void log_database_close(void);
|
||||||
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
|
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
|
||||||
|
|
||||||
// FALSE if no backend, or PREF_DBLOG is "off" / "redact".
|
|
||||||
gboolean log_database_can_recover_messages(void);
|
|
||||||
|
|
||||||
// Cross-backend export/import (requires HAVE_SQLITE)
|
// Cross-backend export/import (requires HAVE_SQLITE)
|
||||||
#ifdef HAVE_SQLITE
|
#ifdef HAVE_SQLITE
|
||||||
int log_database_export_to_flatfile(const gchar* const contact_jid);
|
int log_database_export_to_flatfile(const gchar* const contact_jid);
|
||||||
|
|||||||
@@ -211,13 +211,9 @@ _first_pass(FILE* fp, const char* basename,
|
|||||||
|
|
||||||
if (pl->stanza_id && pl->stanza_id[0] != '\0') {
|
if (pl->stanza_id && pl->stanza_id[0] != '\0') {
|
||||||
if (g_hash_table_contains(seen_stanza_ids, pl->stanza_id)) {
|
if (g_hash_table_contains(seen_stanza_ids, pl->stanza_id)) {
|
||||||
// Older clients (Pidgin, Adium, even early Profanity)
|
*issues = g_slist_prepend(*issues,
|
||||||
// sometimes reuse client-generated stanza-ids across
|
_issue_new(INTEGRITY_WARNING, basename, lineno,
|
||||||
// distinct messages, so a collision is not a real
|
g_strdup_printf("Duplicate stanza-id \"%s\"", pl->stanza_id)));
|
||||||
// integrity defect — log it for diagnostics but don't
|
|
||||||
// surface it through /history verify.
|
|
||||||
log_debug("flatfile verify: duplicate stanza-id \"%s\" at %s:%d",
|
|
||||||
pl->stanza_id, basename, lineno);
|
|
||||||
} else {
|
} else {
|
||||||
g_hash_table_insert(seen_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
g_hash_table_insert(seen_stanza_ids, g_strdup(pl->stanza_id), GINT_TO_POINTER(lineno));
|
||||||
}
|
}
|
||||||
@@ -306,13 +302,7 @@ _verify_contact_dir(const char* cdir_path, GSList** issues)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconstruct the contact JID from the directory name so issue reports
|
const char* basename = "history.log";
|
||||||
// identify which contact the problem belongs to (every contact has its
|
|
||||||
// own history.log, so the bare filename is ambiguous).
|
|
||||||
auto_gchar gchar* dir_name = g_path_get_basename(cdir_path);
|
|
||||||
auto_gcharv gchar** parts = g_strsplit(dir_name, "_at_", -1);
|
|
||||||
auto_gchar gchar* contact_jid = g_strjoinv("@", parts);
|
|
||||||
auto_gchar gchar* basename = g_strdup_printf("%s/history.log", contact_jid);
|
|
||||||
|
|
||||||
_check_permissions(filepath, basename, issues);
|
_check_permissions(filepath, basename, issues);
|
||||||
|
|
||||||
|
|||||||
@@ -466,36 +466,23 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
|||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check timestamp ordering, restricted to consecutive rows belonging
|
// Check timestamp ordering for a specific contact or all
|
||||||
// to the same conversation pair so adjacency does not span unrelated
|
|
||||||
// contacts. Adjacent rows by id from different conversations are
|
|
||||||
// legitimately out-of-order and should not be reported.
|
|
||||||
const Jid* myjid = connection_get_jid();
|
const Jid* myjid = connection_get_jid();
|
||||||
if (myjid && myjid->barejid) {
|
if (myjid && myjid->barejid) {
|
||||||
auto_sqlite char* query = NULL;
|
auto_sqlite char* query = NULL;
|
||||||
if (contact_barejid) {
|
if (contact_barejid) {
|
||||||
query = sqlite3_mprintf(
|
query = sqlite3_mprintf(
|
||||||
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||||
"FROM `ChatLogs` A "
|
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||||
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
|
||||||
" AND B.`id` > A.`id` "
|
|
||||||
"WHERE A.`timestamp` > B.`timestamp` "
|
"WHERE A.`timestamp` > B.`timestamp` "
|
||||||
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
"AND ((A.`from_jid` = %Q AND A.`to_jid` = %Q) OR (A.`from_jid` = %Q AND A.`to_jid` = %Q)) "
|
||||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
|
||||||
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
|
||||||
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
|
||||||
"LIMIT 50",
|
"LIMIT 50",
|
||||||
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid);
|
contact_barejid, myjid->barejid, myjid->barejid, contact_barejid);
|
||||||
} else {
|
} else {
|
||||||
query = sqlite3_mprintf(
|
query = sqlite3_mprintf(
|
||||||
"SELECT A.`id`, A.`timestamp`, A.`from_jid`, A.`to_jid`, B.`id`, B.`timestamp` "
|
"SELECT A.`id`, A.`timestamp`, B.`id`, B.`timestamp` FROM `ChatLogs` A "
|
||||||
"FROM `ChatLogs` A "
|
"JOIN `ChatLogs` B ON B.`id` = A.`id` + 1 "
|
||||||
"JOIN `ChatLogs` B ON B.`from_jid` = A.`from_jid` AND B.`to_jid` = A.`to_jid` "
|
|
||||||
" AND B.`id` > A.`id` "
|
|
||||||
"WHERE A.`timestamp` > B.`timestamp` "
|
"WHERE A.`timestamp` > B.`timestamp` "
|
||||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` C "
|
|
||||||
" WHERE C.`from_jid` = A.`from_jid` AND C.`to_jid` = A.`to_jid` "
|
|
||||||
" AND C.`id` > A.`id` AND C.`id` < B.`id`) "
|
|
||||||
"LIMIT 50");
|
"LIMIT 50");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,19 +490,15 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
|||||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
int id_a = sqlite3_column_int(stmt, 0);
|
int id_a = sqlite3_column_int(stmt, 0);
|
||||||
const char* ts_a = (const char*)sqlite3_column_text(stmt, 1);
|
const char* ts_a = (const char*)sqlite3_column_text(stmt, 1);
|
||||||
const char* from_a = (const char*)sqlite3_column_text(stmt, 2);
|
int id_b = sqlite3_column_int(stmt, 2);
|
||||||
const char* to_a = (const char*)sqlite3_column_text(stmt, 3);
|
const char* ts_b = (const char*)sqlite3_column_text(stmt, 3);
|
||||||
int id_b = sqlite3_column_int(stmt, 4);
|
|
||||||
const char* ts_b = (const char*)sqlite3_column_text(stmt, 5);
|
|
||||||
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
issue->level = INTEGRITY_WARNING;
|
issue->level = INTEGRITY_WARNING;
|
||||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
issue->file = g_strdup("chatlog.db");
|
||||||
from_a ? from_a : "?", to_a ? to_a : "?");
|
|
||||||
issue->line = id_a;
|
issue->line = id_a;
|
||||||
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
|
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
|
||||||
id_a, ts_a ? ts_a : "NULL",
|
id_a, ts_a ? ts_a : "NULL", id_b, ts_b ? ts_b : "NULL");
|
||||||
id_b, ts_b ? ts_b : "NULL");
|
|
||||||
issues = g_slist_append(issues, issue);
|
issues = g_slist_append(issues, issue);
|
||||||
}
|
}
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
@@ -523,7 +506,7 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
|||||||
|
|
||||||
// Check broken LMC references
|
// Check broken LMC references
|
||||||
auto_sqlite char* lmc_query = sqlite3_mprintf(
|
auto_sqlite char* lmc_query = sqlite3_mprintf(
|
||||||
"SELECT A.`id`, A.`replaces_db_id`, A.`from_jid`, A.`to_jid` FROM `ChatLogs` A "
|
"SELECT A.`id`, A.`replaces_db_id` FROM `ChatLogs` A "
|
||||||
"WHERE A.`replaces_db_id` IS NOT NULL "
|
"WHERE A.`replaces_db_id` IS NOT NULL "
|
||||||
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` B WHERE B.`id` = A.`replaces_db_id`) "
|
"AND NOT EXISTS (SELECT 1 FROM `ChatLogs` B WHERE B.`id` = A.`replaces_db_id`) "
|
||||||
"LIMIT 50");
|
"LIMIT 50");
|
||||||
@@ -532,13 +515,10 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
|
|||||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
int id = sqlite3_column_int(stmt, 0);
|
int id = sqlite3_column_int(stmt, 0);
|
||||||
int replaces_id = sqlite3_column_int(stmt, 1);
|
int replaces_id = sqlite3_column_int(stmt, 1);
|
||||||
const char* from = (const char*)sqlite3_column_text(stmt, 2);
|
|
||||||
const char* to = (const char*)sqlite3_column_text(stmt, 3);
|
|
||||||
|
|
||||||
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
|
||||||
issue->level = INTEGRITY_ERROR;
|
issue->level = INTEGRITY_ERROR;
|
||||||
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
|
issue->file = g_strdup("chatlog.db");
|
||||||
from ? from : "?", to ? to : "?");
|
|
||||||
issue->line = id;
|
issue->line = id;
|
||||||
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
|
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
|
||||||
id, replaces_id);
|
id, replaces_id);
|
||||||
|
|||||||
@@ -301,6 +301,12 @@ cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reset paged flag before printing user message.
|
||||||
|
* If the user scrolled up to view history, paged=1 would suppress
|
||||||
|
* the message in _win_printf(). Reset it here so the message displays. */
|
||||||
|
aiwin->window.layout->paged = 0;
|
||||||
|
aiwin->window.layout->unread_msg = 0;
|
||||||
|
|
||||||
// Display user message in AI window.
|
// Display user message in AI window.
|
||||||
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);
|
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "ui/titlebar.h"
|
#include "ui/titlebar.h"
|
||||||
#include "ui/inputwin.h"
|
#include "ui/inputwin.h"
|
||||||
#include "ui/win_types.h"
|
|
||||||
#include "ui/window_list.h"
|
#include "ui/window_list.h"
|
||||||
#include "ui/window.h"
|
#include "ui/window.h"
|
||||||
#include "ui/screen.h"
|
#include "ui/screen.h"
|
||||||
@@ -253,10 +252,6 @@ _title_bar_draw(void)
|
|||||||
_show_muc_privacy(mucwin);
|
_show_muc_privacy(mucwin);
|
||||||
_show_attention(current, mucwin->has_attention);
|
_show_attention(current, mucwin->has_attention);
|
||||||
_show_scrolled(current);
|
_show_scrolled(current);
|
||||||
} else if (current && current->type == WIN_AI) {
|
|
||||||
ProfAiWin* aiwin = (ProfAiWin*)current;
|
|
||||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
|
||||||
_show_scrolled(current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_show_self_presence();
|
_show_self_presence();
|
||||||
|
|||||||
@@ -848,14 +848,6 @@ win_page_down(ProfWin* window, int scroll_size)
|
|||||||
window->layout->paged = 0;
|
window->layout->paged = 0;
|
||||||
window->layout->unread_msg = 0;
|
window->layout->unread_msg = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-chat windows have no DB to fetch from, so WIN_SCROLL_REACHED_BOTTOM
|
|
||||||
// never fires; reset paged once the buffer's last line is on screen.
|
|
||||||
if (window->type != WIN_CHAT
|
|
||||||
&& (total_rows - *page_start) <= page_space + 1) {
|
|
||||||
window->layout->paged = 0;
|
|
||||||
window->layout->unread_msg = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -1809,12 +1801,10 @@ static void
|
|||||||
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
|
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/* Prevent printing and buffer update when user is viewing message history [SCROLLING]*/
|
||||||
if (window->layout->paged && wins_is_current(window)) {
|
if (window->layout->paged && wins_is_current(window)) {
|
||||||
window->layout->unread_msg++;
|
window->layout->unread_msg++;
|
||||||
// Skip buffer only when DB can replay the message on scroll-down.
|
return;
|
||||||
if (window->type == WIN_CHAT && log_database_can_recover_messages()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timestamp == NULL) {
|
if (timestamp == NULL) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#include "glib.h"
|
|
||||||
#include "prof_cmocka.h"
|
#include "prof_cmocka.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "ai/ai_client.h"
|
#include "ai/ai_client.h"
|
||||||
@@ -704,6 +703,7 @@ test_ai_parse_models_openai_format(void** state)
|
|||||||
models = g_list_next(models);
|
models = g_list_next(models);
|
||||||
assert_string_equal("google/gemini-3-flash-preview", models->data);
|
assert_string_equal("google/gemini-3-flash-preview", models->data);
|
||||||
|
|
||||||
|
ai_provider_unref(provider);
|
||||||
ai_client_shutdown();
|
ai_client_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,6 +776,7 @@ test_ai_parse_models_perplexity_format(void** state)
|
|||||||
models = g_list_last(models);
|
models = g_list_last(models);
|
||||||
assert_string_equal("xai/grok-4.3", models->data);
|
assert_string_equal("xai/grok-4.3", models->data);
|
||||||
|
|
||||||
|
ai_provider_unref(provider);
|
||||||
ai_client_shutdown();
|
ai_client_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,6 +801,7 @@ test_ai_parse_models_array_format(void** state)
|
|||||||
models = g_list_next(models);
|
models = g_list_next(models);
|
||||||
assert_string_equal("model3", models->data);
|
assert_string_equal("model3", models->data);
|
||||||
|
|
||||||
|
ai_provider_unref(provider);
|
||||||
ai_client_shutdown();
|
ai_client_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,6 +821,7 @@ test_ai_parse_models_empty_json(void** state)
|
|||||||
assert_null(provider->models);
|
assert_null(provider->models);
|
||||||
assert_int_equal(0, g_list_length(provider->models));
|
assert_int_equal(0, g_list_length(provider->models));
|
||||||
|
|
||||||
|
ai_provider_unref(provider);
|
||||||
ai_client_shutdown();
|
ai_client_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,6 +837,7 @@ test_ai_parse_models_null_handling(void** state)
|
|||||||
ai_parse_models_from_json(provider, NULL);
|
ai_parse_models_from_json(provider, NULL);
|
||||||
assert_null(provider->models);
|
assert_null(provider->models);
|
||||||
|
|
||||||
|
ai_provider_unref(provider);
|
||||||
ai_client_shutdown();
|
ai_client_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,6 +861,7 @@ test_ai_parse_models_escaped_quotes(void** state)
|
|||||||
GList* models = provider->models;
|
GList* models = provider->models;
|
||||||
assert_non_null(models);
|
assert_non_null(models);
|
||||||
|
|
||||||
|
ai_provider_unref(provider);
|
||||||
ai_client_shutdown();
|
ai_client_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,6 +885,7 @@ test_ai_parse_models_with_whitespace(void** state)
|
|||||||
GList* models = provider->models;
|
GList* models = provider->models;
|
||||||
assert_string_equal("model-with-spaces", models->data);
|
assert_string_equal("model-with-spaces", models->data);
|
||||||
|
|
||||||
|
ai_provider_unref(provider);
|
||||||
ai_client_shutdown();
|
ai_client_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,7 +1142,7 @@ test_ai_providers_find_cycles_through_many(void** state)
|
|||||||
ai_add_provider("alpine", "https://al.example/");
|
ai_add_provider("alpine", "https://al.example/");
|
||||||
|
|
||||||
/* Three providers share prefix "alp": alpha, alphabet, alpine. */
|
/* Three providers share prefix "alp": alpha, alphabet, alpine. */
|
||||||
GHashTable* seen = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
GHashTable* seen = g_hash_table_new(g_str_hash, g_str_equal);
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
auto_gchar gchar* match = ai_providers_find("alp", FALSE, NULL);
|
auto_gchar gchar* match = ai_providers_find("alp", FALSE, NULL);
|
||||||
|
|||||||
Reference in New Issue
Block a user