C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
JS/TS / lint (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 16s
C++ / clang-tidy (pull_request) Failing after 43s
Python / static (pull_request) Failing after 57s
Rust / fmt (pull_request) Successful in 1m30s
Rust / build (pull_request) Successful in 2m5s
Rust / clippy (pull_request) Successful in 1m55s
Python / verify (pull_request) Successful in 2m23s
616 lines
18 KiB
C++
616 lines
18 KiB
C++
#include "chatstore.hpp"
|
|
|
|
#include "llmclient.hpp"
|
|
#include "message.hpp"
|
|
#include "segment.hpp"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QDateTime>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QPointer>
|
|
#include <QStandardPaths>
|
|
#include <QSqlDatabase>
|
|
#include <QSqlError>
|
|
#include <QSqlQuery>
|
|
#include <QThreadPool>
|
|
#include <QVector>
|
|
#include <QUuid>
|
|
|
|
#include <algorithm>
|
|
|
|
namespace ZShell::llm {
|
|
|
|
namespace {
|
|
|
|
QString segmentTypeName(LlmSegment::Type type) {
|
|
switch (type) {
|
|
case LlmSegment::Type::Reasoning:
|
|
return QStringLiteral("reasoning");
|
|
case LlmSegment::Type::ToolCall:
|
|
return QStringLiteral("tool_call");
|
|
case LlmSegment::Type::Content:
|
|
return QStringLiteral("content");
|
|
}
|
|
return QStringLiteral("reasoning");
|
|
}
|
|
|
|
LlmSegment::Type segmentTypeFromName(const QString& name) {
|
|
if (name == QLatin1String("tool_call")) return LlmSegment::Type::ToolCall;
|
|
if (name == QLatin1String("content")) return LlmSegment::Type::Content;
|
|
return LlmSegment::Type::Reasoning;
|
|
}
|
|
|
|
QString sqlText(const QString& value) {
|
|
if (value.isNull()) return QStringLiteral("");
|
|
return value;
|
|
}
|
|
|
|
struct SegmentRow {
|
|
QString type;
|
|
QString text;
|
|
QString name;
|
|
QString toolCallId;
|
|
QString arguments;
|
|
QString result;
|
|
int status = 0;
|
|
qint64 elapsedMs = 0;
|
|
qint64 timestamp = 0;
|
|
};
|
|
|
|
struct GenerationRow {
|
|
qint64 timestamp = 0;
|
|
bool active = false;
|
|
QVector<SegmentRow> segments;
|
|
};
|
|
|
|
struct MessageRow {
|
|
bool user = false;
|
|
qint64 timestamp = 0;
|
|
QVector<GenerationRow> generations;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
ChatStore::ChatStore(QObject* parent)
|
|
: QObject(parent), m_connectionName(QUuid::createUuid().toString()) {
|
|
openDb();
|
|
load();
|
|
}
|
|
|
|
ChatStore::~ChatStore() {
|
|
if (m_connectionName.isEmpty()) return;
|
|
QSqlDatabase db = QSqlDatabase::database(m_connectionName, false);
|
|
db.close();
|
|
QSqlDatabase::removeDatabase(m_connectionName);
|
|
}
|
|
|
|
QSqlDatabase ChatStore::db() const {
|
|
return QSqlDatabase::database(m_connectionName);
|
|
}
|
|
|
|
void ChatStore::openDb() {
|
|
m_dbPath =
|
|
QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation) +
|
|
QStringLiteral("/zshell/chats.sqlite");
|
|
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
|
|
|
|
QSqlDatabase db =
|
|
QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
|
|
db.setDatabaseName(m_dbPath);
|
|
if (!db.open()) {
|
|
qWarning() << "ChatStore: failed to open database" << m_dbPath << ":"
|
|
<< db.lastError().text();
|
|
return;
|
|
}
|
|
{
|
|
QSqlQuery pragma(db);
|
|
pragma.exec(QStringLiteral("PRAGMA foreign_keys = ON"));
|
|
}
|
|
{
|
|
QSqlQuery query(db);
|
|
query.exec(QStringLiteral(
|
|
"CREATE TABLE IF NOT EXISTS sessions (\n"
|
|
" id TEXT PRIMARY KEY,\n"
|
|
" title TEXT NOT NULL DEFAULT '',\n"
|
|
" icon TEXT NOT NULL DEFAULT '',\n"
|
|
" created_at INTEGER NOT NULL,\n"
|
|
" updated_at INTEGER NOT NULL,\n"
|
|
" pinned INTEGER NOT NULL DEFAULT 0\n"
|
|
")"));
|
|
}
|
|
{
|
|
QSqlQuery query(db);
|
|
query.exec(QStringLiteral(
|
|
"CREATE TABLE IF NOT EXISTS messages (\n"
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
|
" session_id TEXT NOT NULL REFERENCES sessions (id) "
|
|
"ON DELETE CASCADE,\n"
|
|
" role TEXT NOT NULL,\n"
|
|
" timestamp INTEGER NOT NULL\n"
|
|
")"));
|
|
}
|
|
{
|
|
QSqlQuery query(db);
|
|
query.exec(QStringLiteral(
|
|
"CREATE TABLE IF NOT EXISTS generations (\n"
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
|
" message_id INTEGER NOT NULL REFERENCES messages "
|
|
"(id) ON DELETE CASCADE,\n"
|
|
" timestamp INTEGER NOT NULL,\n"
|
|
" is_active INTEGER NOT NULL DEFAULT 1\n"
|
|
")"));
|
|
}
|
|
{
|
|
QSqlQuery query(db);
|
|
query.exec(QStringLiteral(
|
|
"CREATE TABLE IF NOT EXISTS segments (\n"
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
|
" generation_id INTEGER NOT NULL REFERENCES generations "
|
|
"(id) ON DELETE CASCADE,\n"
|
|
" type TEXT NOT NULL,\n"
|
|
" text TEXT NOT NULL DEFAULT '',\n"
|
|
" name TEXT NOT NULL DEFAULT '',\n"
|
|
" tool_call_id TEXT NOT NULL DEFAULT '',\n"
|
|
" arguments TEXT NOT NULL DEFAULT '',\n"
|
|
" result TEXT NOT NULL DEFAULT '',\n"
|
|
" status INTEGER NOT NULL DEFAULT 0,\n"
|
|
" elapsed_ms INTEGER NOT NULL DEFAULT 0,\n"
|
|
" timestamp INTEGER NOT NULL\n"
|
|
")"));
|
|
}
|
|
{
|
|
QSqlQuery query(db);
|
|
query.exec(QStringLiteral(
|
|
"CREATE INDEX IF NOT EXISTS idx_messages_session "
|
|
"ON messages (session_id)"));
|
|
query.exec(QStringLiteral(
|
|
"CREATE INDEX IF NOT EXISTS idx_generations_message "
|
|
"ON generations (message_id)"));
|
|
query.exec(QStringLiteral(
|
|
"CREATE INDEX IF NOT EXISTS idx_segments_generation "
|
|
"ON segments (generation_id)"));
|
|
}
|
|
}
|
|
|
|
int ChatStore::count() const {
|
|
return static_cast<int>(m_sessions.size());
|
|
}
|
|
|
|
QVariantList ChatStore::values() const {
|
|
QVariantList vals;
|
|
vals.reserve(m_sessions.size());
|
|
for (const auto* session : m_sessions)
|
|
vals.append(QVariant::fromValue(session));
|
|
return vals;
|
|
}
|
|
|
|
ChatSession* ChatStore::at(int index) const {
|
|
if (index < 0 || index >= m_sessions.size()) return nullptr;
|
|
return m_sessions.at(index);
|
|
}
|
|
|
|
ChatSession* ChatStore::insert(int index) {
|
|
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
|
const QString id = QUuid::createUuid().toString();
|
|
{
|
|
QSqlQuery query(db());
|
|
query.prepare(
|
|
"INSERT INTO sessions (id, title, created_at, updated_at) "
|
|
"VALUES (:id, '', :created_at, :updated_at)");
|
|
query.bindValue(":id", id);
|
|
query.bindValue(":created_at", now);
|
|
query.bindValue(":updated_at", now);
|
|
if (!query.exec())
|
|
qWarning() << "ChatStore: failed to insert session" << id << ":"
|
|
<< query.lastError().text();
|
|
}
|
|
auto* session = new ChatSession(id, this);
|
|
session->setMeta(QString(), now, now, 0);
|
|
const int pos = index >= 0 && index <= m_sessions.size() ? index : 0;
|
|
m_sessions.insert(pos, session);
|
|
Q_EMIT countChanged();
|
|
Q_EMIT valuesChanged();
|
|
return session;
|
|
}
|
|
|
|
void ChatStore::remove(int index) {
|
|
removeSession(at(index));
|
|
}
|
|
|
|
void ChatStore::remove(ChatSession* chat) {
|
|
removeSession(chat);
|
|
}
|
|
|
|
void ChatStore::removeSession(ChatSession* session) {
|
|
if (!session || !m_sessions.contains(session)) return;
|
|
const QList<ChatSession*> before = m_sessions;
|
|
Q_EMIT sessionRemoved(session);
|
|
{
|
|
QSqlQuery query(db());
|
|
query.prepare("DELETE FROM sessions WHERE id = :id");
|
|
query.bindValue(":id", session->id());
|
|
query.exec();
|
|
}
|
|
m_sessions.removeOne(session);
|
|
session->deleteLater();
|
|
notify(before);
|
|
}
|
|
|
|
void ChatStore::move(int from, int to) {
|
|
if (from < 0 || from >= m_sessions.size() || to < 0 ||
|
|
to >= m_sessions.size() || from == to)
|
|
return;
|
|
m_sessions.move(from, to);
|
|
Q_EMIT valuesChanged();
|
|
}
|
|
|
|
void ChatStore::clear() {
|
|
const QList<ChatSession*> sessions = m_sessions;
|
|
for (ChatSession* session : sessions)
|
|
removeSession(session);
|
|
}
|
|
|
|
ChatSession* ChatStore::sessionById(const QString& id) {
|
|
for (auto* session : m_sessions)
|
|
if (session->id() == id) return session;
|
|
return nullptr;
|
|
}
|
|
|
|
void ChatStore::setLlmClient(LlmClient* client) {
|
|
m_llmClient = client;
|
|
}
|
|
|
|
void ChatStore::persist(ChatSession* session) {
|
|
if (!session || !m_sessions.contains(session)) return;
|
|
if (!session->isLoaded()) {
|
|
m_pendingPersists.insert(session);
|
|
return;
|
|
}
|
|
session->setUpdatedAt(QDateTime::currentMSecsSinceEpoch());
|
|
if (!saveSession(session)) return;
|
|
sortAndNotify();
|
|
}
|
|
|
|
void ChatStore::saveMeta(ChatSession* session) {
|
|
if (!session || !m_sessions.contains(session)) return;
|
|
QSqlQuery query(db());
|
|
query.prepare(
|
|
"UPDATE sessions SET title = :title, icon = :icon "
|
|
"WHERE id = :id");
|
|
query.bindValue(":title", sqlText(session->title()));
|
|
query.bindValue(":icon", sqlText(session->icon()));
|
|
query.bindValue(":id", session->id());
|
|
if (!query.exec())
|
|
qWarning() << "ChatStore: failed to save meta for" << session->id()
|
|
<< ":" << query.lastError().text();
|
|
}
|
|
|
|
bool ChatStore::saveSession(ChatSession* session) {
|
|
const QString id = session->id();
|
|
QSqlDatabase handle = db();
|
|
if (!handle.transaction()) {
|
|
qWarning() << "ChatStore: failed to begin transaction:"
|
|
<< handle.lastError().text();
|
|
return false;
|
|
}
|
|
bool ok = true;
|
|
{
|
|
QSqlQuery query(handle);
|
|
query.prepare(
|
|
"UPDATE sessions SET title = :title, updated_at = :updated_at "
|
|
"WHERE id = :id");
|
|
query.bindValue(":title", sqlText(session->title()));
|
|
query.bindValue(":updated_at", session->updatedAtMs());
|
|
query.bindValue(":id", session->id());
|
|
ok = query.exec();
|
|
}
|
|
if (ok) {
|
|
QSqlQuery query(handle);
|
|
query.prepare("DELETE FROM messages WHERE session_id = :id");
|
|
query.bindValue(":id", session->id());
|
|
ok = query.exec();
|
|
}
|
|
if (ok) {
|
|
QSqlQuery messageInsert(handle);
|
|
ok = messageInsert.prepare(
|
|
"INSERT INTO messages (session_id, role, timestamp) "
|
|
"VALUES (:id, :role, :timestamp)");
|
|
QSqlQuery generationInsert(handle);
|
|
ok = ok &&
|
|
generationInsert.prepare(
|
|
"INSERT INTO generations (message_id, timestamp, is_active) "
|
|
"VALUES (:mid, :timestamp, :is_active)");
|
|
QSqlQuery segmentInsert(handle);
|
|
ok = ok &&
|
|
segmentInsert.prepare(
|
|
"INSERT INTO segments (generation_id, type, text, name, "
|
|
"tool_call_id, arguments, result, status, elapsed_ms, "
|
|
"timestamp) VALUES (:gid, :type, :text, :name, "
|
|
":tool_call_id, :arguments, :result, :status, :elapsed_ms, "
|
|
":timestamp)");
|
|
const auto* model = session->messagesModel();
|
|
for (int row = model->rowCount() - 1; ok && row >= 0; --row) {
|
|
const auto* message = model->at(row);
|
|
messageInsert.bindValue(":id", session->id());
|
|
messageInsert.bindValue(
|
|
":role",
|
|
message->role() == ChatMessage::Role::User
|
|
? QStringLiteral("user")
|
|
: QStringLiteral("assistant"));
|
|
messageInsert.bindValue(":timestamp", message->timestamp());
|
|
if (!messageInsert.exec()) {
|
|
ok = false;
|
|
qWarning() << "ChatStore: saveSession" << id
|
|
<< "message insert failed:"
|
|
<< messageInsert.lastError().text();
|
|
break;
|
|
}
|
|
const int messageId = messageInsert.lastInsertId().toInt();
|
|
for (int i = 0; ok && i < message->generationCount(); ++i) {
|
|
const auto* generation = message->generation(i);
|
|
generationInsert.bindValue(":mid", messageId);
|
|
generationInsert.bindValue(
|
|
":timestamp", generation->timestamp());
|
|
generationInsert.bindValue(
|
|
":is_active",
|
|
i == message->activeGenerationIndex() ? 1 : 0);
|
|
if (!generationInsert.exec()) {
|
|
ok = false;
|
|
qWarning() << "ChatStore: saveSession" << id
|
|
<< "generation insert failed:"
|
|
<< generationInsert.lastError().text();
|
|
break;
|
|
}
|
|
const int generationId =
|
|
generationInsert.lastInsertId().toInt();
|
|
for (const auto* segment : generation->segments()) {
|
|
segmentInsert.bindValue(":gid", generationId);
|
|
segmentInsert.bindValue(
|
|
":type", segmentTypeName(segment->type()));
|
|
segmentInsert.bindValue(":text", sqlText(segment->text()));
|
|
segmentInsert.bindValue(":name", sqlText(segment->name()));
|
|
segmentInsert.bindValue(
|
|
":tool_call_id", sqlText(segment->toolCallId()));
|
|
segmentInsert.bindValue(
|
|
":arguments", sqlText(segment->arguments()));
|
|
segmentInsert.bindValue(
|
|
":result", sqlText(segment->result()));
|
|
segmentInsert.bindValue(
|
|
":status", static_cast<int>(segment->status()));
|
|
segmentInsert.bindValue(":elapsed_ms", segment->elapsedMs());
|
|
segmentInsert.bindValue(":timestamp", segment->timestamp());
|
|
if (!segmentInsert.exec()) {
|
|
ok = false;
|
|
qWarning() << "ChatStore: saveSession" << id
|
|
<< "segment insert failed:"
|
|
<< segmentInsert.lastError().text();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!ok || !handle.commit()) {
|
|
qWarning() << "ChatStore: saveSession" << id
|
|
<< "commit failed, rolling back";
|
|
handle.rollback();
|
|
ok = false;
|
|
qWarning() << "ChatStore: failed to save session" << session->id()
|
|
<< ":" << handle.lastError().text();
|
|
}
|
|
return ok;
|
|
}
|
|
void ChatStore::loadMessagesInto(ChatSession* session) {
|
|
if (!session) return;
|
|
const QString sessionId = session->id();
|
|
const QString path = m_dbPath;
|
|
|
|
QThreadPool::globalInstance()->start([store = QPointer<ChatStore>(this),
|
|
session =
|
|
QPointer<ChatSession>(session),
|
|
sessionId,
|
|
path]() {
|
|
QList<MessageRow> rows;
|
|
const QString connName = QUuid::createUuid().toString();
|
|
{
|
|
QSqlDatabase db =
|
|
QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connName);
|
|
db.setDatabaseName(path);
|
|
if (db.open()) {
|
|
QSqlQuery busy(db);
|
|
busy.exec(QStringLiteral("PRAGMA busy_timeout = 3000"));
|
|
QSqlQuery query(db);
|
|
query.prepare(
|
|
"SELECT id, role, timestamp FROM messages "
|
|
"WHERE session_id = :id ORDER BY rowid DESC");
|
|
query.bindValue(":id", sessionId);
|
|
if (!query.exec()) {
|
|
qWarning() << "ChatStore: failed to load messages for"
|
|
<< sessionId << ":" << query.lastError().text();
|
|
} else {
|
|
while (query.next()) {
|
|
const int messageId = query.value(0).toInt();
|
|
MessageRow message;
|
|
message.user = query.value(1).toString() ==
|
|
QLatin1String("user");
|
|
message.timestamp = query.value(2).toLongLong();
|
|
QSqlQuery generationQuery(db);
|
|
generationQuery.prepare(
|
|
"SELECT id, timestamp, is_active FROM "
|
|
"generations WHERE message_id = :mid "
|
|
"ORDER BY rowid");
|
|
generationQuery.bindValue(":mid", messageId);
|
|
if (generationQuery.exec()) {
|
|
while (generationQuery.next()) {
|
|
GenerationRow generation;
|
|
generation.timestamp =
|
|
generationQuery.value(1).toLongLong();
|
|
generation.active =
|
|
generationQuery.value(2).toInt() != 0;
|
|
QSqlQuery segmentQuery(db);
|
|
segmentQuery.prepare(
|
|
"SELECT type, text, name, tool_call_id, "
|
|
"arguments, result, status, elapsed_ms, "
|
|
"timestamp FROM segments WHERE "
|
|
"generation_id = :gid ORDER BY rowid");
|
|
segmentQuery.bindValue(
|
|
":gid", generationQuery.value(0).toInt());
|
|
if (segmentQuery.exec()) {
|
|
while (segmentQuery.next()) {
|
|
SegmentRow segment;
|
|
segment.type =
|
|
segmentQuery.value(0).toString();
|
|
segment.text =
|
|
segmentQuery.value(1).toString();
|
|
segment.name =
|
|
segmentQuery.value(2).toString();
|
|
segment.toolCallId =
|
|
segmentQuery.value(3).toString();
|
|
segment.arguments =
|
|
segmentQuery.value(4).toString();
|
|
segment.result =
|
|
segmentQuery.value(5).toString();
|
|
segment.status =
|
|
segmentQuery.value(6).toInt();
|
|
segment.elapsedMs =
|
|
segmentQuery.value(7).toLongLong();
|
|
segment.timestamp =
|
|
segmentQuery.value(8).toLongLong();
|
|
generation.segments.append(segment);
|
|
}
|
|
} else {
|
|
qWarning()
|
|
<< "ChatStore: failed to load "
|
|
"segments for generation"
|
|
<< generationQuery.value(0).toInt()
|
|
<< ":"
|
|
<< segmentQuery.lastError().text();
|
|
}
|
|
message.generations.append(generation);
|
|
}
|
|
} else {
|
|
qWarning()
|
|
<< "ChatStore: failed to load generations "
|
|
"for message"
|
|
<< messageId << ":"
|
|
<< generationQuery.lastError().text();
|
|
}
|
|
rows.append(message);
|
|
}
|
|
}
|
|
db.close();
|
|
} else {
|
|
qWarning() << "ChatStore: failed to open database for load:"
|
|
<< db.lastError().text();
|
|
}
|
|
}
|
|
QSqlDatabase::removeDatabase(connName);
|
|
|
|
QMetaObject::invokeMethod(
|
|
QCoreApplication::instance(),
|
|
[store, session, rows = std::move(rows)]() mutable {
|
|
ChatStore* st = store;
|
|
ChatSession* s = session;
|
|
if (!st || !s) return;
|
|
|
|
auto* model = s->model();
|
|
if (!model) return;
|
|
QList<ChatMessage*> messages;
|
|
for (const MessageRow& row : rows) {
|
|
auto* message = model->createMessage(
|
|
row.user ? ChatMessage::Role::User
|
|
: ChatMessage::Role::Assistant,
|
|
row.timestamp);
|
|
int activeIndex = 0;
|
|
for (int i = 0; i < row.generations.size(); ++i) {
|
|
const GenerationRow& generationRow =
|
|
row.generations.at(i);
|
|
auto* generation =
|
|
message->addGeneration(generationRow.timestamp);
|
|
for (const SegmentRow& segmentRow :
|
|
generationRow.segments) {
|
|
auto* segment = new LlmSegment(
|
|
segmentTypeFromName(segmentRow.type),
|
|
segmentRow.timestamp,
|
|
generation);
|
|
segment->setText(segmentRow.text);
|
|
segment->setName(segmentRow.name);
|
|
segment->setToolCallId(segmentRow.toolCallId);
|
|
segment->appendArguments(segmentRow.arguments);
|
|
segment->setResult(segmentRow.result);
|
|
segment->setStatus(
|
|
static_cast<LlmSegment::Status>(
|
|
segmentRow.status));
|
|
segment->restore(segmentRow.elapsedMs);
|
|
generation->addSegment(segment);
|
|
}
|
|
if (generationRow.active) activeIndex = i;
|
|
}
|
|
message->setActiveGeneration(activeIndex);
|
|
messages.append(message);
|
|
}
|
|
if (model->rowCount() > 0) {
|
|
QList<ChatMessage*> live = messages;
|
|
for (int r = 0; r < model->rowCount(); ++r)
|
|
live.prepend(model->at(r));
|
|
messages = live;
|
|
}
|
|
if (!messages.isEmpty() || model->rowCount() > 0)
|
|
s->adoptMessages(messages);
|
|
|
|
s->markLoaded();
|
|
|
|
if (s->takeClearPending()) {
|
|
s->clear();
|
|
} else if (st->m_pendingPersists.remove(s)) {
|
|
st->persist(s);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
});
|
|
}
|
|
|
|
void ChatStore::load() {
|
|
QSqlQuery query(db());
|
|
query.exec(
|
|
"SELECT s.id, s.title, s.icon, s.created_at, s.updated_at, s.pinned, "
|
|
"(SELECT COUNT(*) FROM messages m WHERE m.session_id = s.id) AS cnt "
|
|
"FROM sessions s ORDER BY s.pinned DESC, s.updated_at DESC");
|
|
while (query.next()) {
|
|
auto* session = new ChatSession(query.value(0).toString(), this);
|
|
session->setMeta(
|
|
query.value(1).toString(),
|
|
query.value(3).toLongLong(),
|
|
query.value(4).toLongLong(),
|
|
query.value(6).toInt());
|
|
session->setIcon(query.value(2).toString());
|
|
session->setPinned(query.value(5).toInt() != 0);
|
|
m_sessions.append(session);
|
|
}
|
|
sortAndNotify();
|
|
}
|
|
|
|
void ChatStore::sortAndNotify() {
|
|
const QList<ChatSession*> before = m_sessions;
|
|
std::stable_sort(
|
|
m_sessions.begin(),
|
|
m_sessions.end(),
|
|
[](const ChatSession* a, const ChatSession* b) {
|
|
if (a->pinned() != b->pinned()) return a->pinned() > b->pinned();
|
|
return a->updatedAtMs() > b->updatedAtMs();
|
|
});
|
|
notify(before);
|
|
}
|
|
|
|
void ChatStore::notify(const QList<ChatSession*>& before) {
|
|
if (before.size() != m_sessions.size()) Q_EMIT countChanged();
|
|
bool same = before.size() == m_sessions.size();
|
|
for (int i = 0; same && i < m_sessions.size(); ++i)
|
|
if (before.at(i) != m_sessions.at(i)) same = false;
|
|
if (!same) Q_EMIT valuesChanged();
|
|
}
|
|
|
|
} // namespace ZShell::llm
|