chore(release): 1.0.0~beta2
Building GXDE Arch Package / detect (push) Skipped
Building GXDE Arch Package / aarch64-arch (push) Skipped
Building GXDE Arch Package / x86_64-arch (push) Skipped
Building GXDE Testing Package / call-arch (push) Skipped
Building Program / call (push) Skipped
Building GXDE Testing Package / call-zhuangzhuang (push) Skipped
Building GXDE Testing Package (zhuangzhuang) / call-i386-zhuangzhuang (push) Skipped
Building GXDE Testing Package (zhuangzhuang) / call-amd64-zhuangzhuang (push) Skipped
Building GXDE Testing Package (zhuangzhuang) / call-arm64-zhuangzhuang (push) Skipped
Building GXDE Testing Package (zhuangzhuang) / call-riscv64-zhuangzhuang (push) Skipped
Building GXDE Testing Package (zhuangzhuang) / call-loong64-zhuangzhuang (push) Skipped
Building GXDE Package / ${{ inputs.display-name || 'Build Job' }} (push) Skipped

- 会话列表拖拽排序/多选/批量删除
- 128k/1024k token 上下文上限
- 待办取消置顶;全屏隐藏联动
- 英文回退翻译补齐;新应用图标
- DTK 关于窗口、分词/朗读对话框、消息常驻操作
This commit is contained in:
shenmo
2026-10-30 22:37:05 +08:00
parent c8a0391abe
commit 4291ee286c
19 changed files with 677 additions and 216 deletions
+22
View File
@@ -117,6 +117,20 @@ bool App::init(bool daemon, bool forceShow, bool showTodo, bool showChat)
}
});
connect(portal, &PortalClient::fullscreenChanged, this, [this](bool fullscreen) {
if (!Config::instance()->getBool(QStringLiteral("panel/hideOnFullscreen"), false)) {
return;
}
if (fullscreen) {
if (m_panel && m_panel->isVisible()) {
m_panel->dismiss();
}
} else if (m_panelPinned || !m_model->isEmpty()) {
ensurePanel();
m_panel->popup();
}
});
connect(m_model, &StagingModel::countChanged, this, [this]() {
if (!m_model->isEmpty() && m_panel) {
m_panel->popup();
@@ -194,6 +208,14 @@ void App::showTodoWindow()
{
if (!m_todoWindow) {
m_todoWindow = new TodoWindow(TodoModel::instance());
connect(m_todoWindow, &TodoWindow::recreateRequested, this, [this]() {
if (m_todoWindow) {
m_todoWindow->close();
m_todoWindow->deleteLater();
m_todoWindow = nullptr;
}
showTodoWindow();
});
}
m_todoWindow->popup();
}
+165 -4
View File
@@ -75,11 +75,20 @@ public:
: QListWidget(parent)
{
setAcceptDrops(true);
setDragEnabled(true);
setDragDropMode(QAbstractItemView::DragDrop);
setDefaultDropAction(Qt::MoveAction);
setDropIndicatorShown(true);
setDragDropOverwriteMode(false);
setSelectionMode(QAbstractItemView::ExtendedSelection);
}
signals:
void droppedAt(int index, const QMimeData *mime);
void left();
void reordered();
void deleteRequested(const QList<int> &rows);
void renameRequested(int row);
protected:
void leaveEvent(QEvent *event) override
@@ -88,10 +97,9 @@ protected:
QListWidget::leaveEvent(event);
}
protected:
void dragEnterEvent(QDragEnterEvent *event) override
{
if (mimeOk(event->mimeData())) {
if (event->source() == this || mimeOk(event->mimeData())) {
event->acceptProposedAction();
} else {
event->ignore();
@@ -100,13 +108,27 @@ protected:
void dragMoveEvent(QDragMoveEvent *event) override
{
if (mimeOk(event->mimeData())) {
if (event->source() == this || mimeOk(event->mimeData())) {
event->acceptProposedAction();
}
}
void dropEvent(QDropEvent *event) override
{
if (event->source() == this) {
const QModelIndex dropIndex = indexAt(event->position().toPoint());
const int to = dropIndex.isValid() ? dropIndex.row() : count();
const int from = currentRow();
if (from >= 0 && to >= 0 && to != from && to != from + 1) {
QListWidgetItem *item = takeItem(from);
const int target = to > from ? to - 1 : to;
insertItem(target, item);
setCurrentRow(target);
Q_EMIT reordered();
}
event->acceptProposedAction();
return;
}
if (!mimeOk(event->mimeData())) {
event->ignore();
return;
@@ -115,6 +137,37 @@ protected:
Q_EMIT droppedAt(index.isValid() ? index.row() : -1, event->mimeData());
event->acceptProposedAction();
}
void contextMenuEvent(QContextMenuEvent *event) override
{
const QModelIndex index = indexAt(event->pos());
QMenu menu(this);
QAction *rename = nullptr;
if (index.isValid()) {
rename = menu.addAction(tr("重命名对话"));
}
QAction *remove = menu.addAction(tr("删除选中"));
QAction *chosen = menu.exec(event->globalPos());
if (!chosen) {
return;
}
if (chosen == rename) {
Q_EMIT renameRequested(index.row());
return;
}
if (chosen == remove) {
QList<int> rows;
for (const QModelIndex &selected : selectionModel()->selectedRows()) {
rows.append(selected.row());
}
if (rows.isEmpty() && index.isValid()) {
rows.append(index.row());
}
if (!rows.isEmpty()) {
Q_EMIT deleteRequested(rows);
}
}
}
};
class ChatView : public QTextBrowser
@@ -546,6 +599,12 @@ void ChatWindow::setupUi()
connect(m_sidebar, &QListWidget::currentRowChanged, this, &ChatWindow::selectConversation);
connect(static_cast<ChatSidebar *>(m_sidebar), &ChatSidebar::droppedAt, this,
&ChatWindow::handleDropOnConversation);
connect(static_cast<ChatSidebar *>(m_sidebar), &ChatSidebar::reordered, this,
&ChatWindow::reorderConversations);
connect(static_cast<ChatSidebar *>(m_sidebar), &ChatSidebar::deleteRequested, this,
&ChatWindow::deleteConversations);
connect(static_cast<ChatSidebar *>(m_sidebar), &ChatSidebar::renameRequested, this,
&ChatWindow::renameConversation);
connect(m_messages, &ChatMessageList::dropped, this, &ChatWindow::handleDroppedMime);
connect(m_messages, &ChatMessageList::linkActivated, this, &ChatWindow::handleAnchor);
connect(m_messages, &ChatMessageList::copyRequested, this, [this](int index) {
@@ -681,12 +740,83 @@ void ChatWindow::selectConversation(int index)
renderConversation();
}
void ChatWindow::reorderConversations()
{
if (m_conversations.isEmpty()) {
return;
}
QHash<QString, ChatConversation> byId;
for (const ChatConversation &conversation : m_conversations) {
byId.insert(conversation.id, conversation);
}
QVector<ChatConversation> ordered;
for (int i = 0; i < m_sidebar->count(); ++i) {
const QString id = m_sidebar->item(i)->data(Qt::UserRole).toString();
auto it = byId.find(id);
if (it != byId.end()) {
ordered.append(it.value());
byId.erase(it);
}
}
for (auto it = byId.begin(); it != byId.end(); ++it) {
ordered.append(it.value());
}
m_conversations = ordered;
const int row = m_sidebar->currentRow();
if (row >= 0 && row < m_conversations.size()) {
m_current = row;
}
save();
updateSidebar();
}
void ChatWindow::deleteConversations(const QList<int> &rows)
{
if (rows.isEmpty()) {
return;
}
if (m_client->busy()) {
m_client->cancel();
m_streamingIndex = -1;
}
QList<int> sorted = rows;
std::sort(sorted.begin(), sorted.end(), std::greater<int>());
for (int row : sorted) {
if (row >= 0 && row < m_conversations.size()) {
m_conversations.removeAt(row);
}
}
m_current = m_conversations.isEmpty() ? -1
: qBound(0, m_current, m_conversations.size() - 1);
save();
updateSidebar();
renderConversation();
}
void ChatWindow::renameConversation(int row)
{
if (row < 0 || row >= m_conversations.size()) {
return;
}
bool ok = false;
const QString title = QInputDialog::getText(nullptr, tr("重命名对话"), tr("名称"),
QLineEdit::Normal,
m_conversations.at(row).title, &ok);
if (ok && !title.trimmed().isEmpty()) {
m_conversations[row].title = title.trimmed();
save();
updateSidebar();
renderConversation();
}
}
void ChatWindow::updateSidebar()
{
const QSignalBlocker blocker(m_sidebar);
m_sidebar->clear();
for (const ChatConversation &conversation : m_conversations) {
auto *item = new QListWidgetItem(conversation.title, m_sidebar);
item->setData(Qt::UserRole, conversation.id);
QString preview = tr("新对话");
for (int i = conversation.messages.size() - 1; i >= 0; --i) {
const ChatMessage &message = conversation.messages.at(i);
@@ -874,8 +1004,26 @@ bool ChatWindow::needsCompression() const
if (!Config::instance()->getBool(QStringLiteral("chat/autoCompress"), true)) {
return false;
}
const QVector<ChatMessage> &messages = m_conversations.at(m_current).messages;
const int keep = Config::instance()->getInt(QStringLiteral("chat/contextLength"), 40);
return m_conversations.at(m_current).messages.size() > keep + 4;
if (messages.size() > keep + 4) {
return true;
}
/* also honour a token budget (128k / 1024k); a rough estimate is enough */
const qint64 tokenLimit =
Config::instance()->getInt(QStringLiteral("chat/contextTokens"), 128000);
if (tokenLimit <= 0) {
return false;
}
qint64 tokens = 0;
for (const ChatMessage &message : messages) {
for (const QChar &ch : message.content) {
tokens += ch.unicode() < 128 ? 1 : 2;
}
}
return tokens / 4 > tokenLimit;
}
void ChatWindow::beginCompression(bool thenSend)
@@ -1153,6 +1301,17 @@ void ChatWindow::openSettings()
Config::instance()->getInt(QStringLiteral("chat/contextLength"), 40));
form->addRow(tr("上下文条数"), contextSpin);
auto *tokenBox = new QComboBox(&dialog);
tokenBox->addItem(tr("128k"), 128000);
tokenBox->addItem(tr("1024k"), 1024000);
const int storedTokens =
Config::instance()->getInt(QStringLiteral("chat/contextTokens"), 128000);
const int tokenIndex = tokenBox->findData(storedTokens);
if (tokenIndex >= 0) {
tokenBox->setCurrentIndex(tokenIndex);
}
form->addRow(tr("上下文长度"), tokenBox);
auto *autoCompressBox = new QCheckBox(tr("超出后自动压缩旧消息"), &dialog);
autoCompressBox->setChecked(
Config::instance()->getBool(QStringLiteral("chat/autoCompress"), true));
@@ -1201,6 +1360,8 @@ void ChatWindow::openSettings()
Config::instance()->set(QStringLiteral("chat/contextLength"), contextSpin->value());
Config::instance()->set(QStringLiteral("chat/autoCompress"),
autoCompressBox->isChecked());
Config::instance()->set(QStringLiteral("chat/contextTokens"),
tokenBox->currentData().toInt());
const QJsonDocument mcpDocument =
QJsonDocument::fromJson(mcpEdit->toPlainText().toUtf8());
if (mcpDocument.isArray() || mcpEdit->toPlainText().trimmed().isEmpty()) {
+3
View File
@@ -62,6 +62,9 @@ private:
void renderConversation();
void updateHeader();
void updateSidebar();
void reorderConversations();
void deleteConversations(const QList<int> &rows);
void renameConversation(int row);
void sendMessage();
void requestCompletion();
void runToolCalls(const QVector<ToolCall> &calls, int index, const QJsonArray &results);
+16 -8
View File
@@ -19,7 +19,7 @@ int main(int argc, char *argv[])
app.setOrganizationName(QStringLiteral("GXDE"));
app.setApplicationName(QStringLiteral("gxde-ai-see"));
app.setApplicationDisplayName(QStringLiteral("GXDE 小埃同学"));
app.setApplicationVersion(QStringLiteral("1.0.0~beta"));
app.setApplicationVersion(QStringLiteral("1.0.0~beta2"));
QIcon appIcon = QIcon::fromTheme(QStringLiteral("gxde-ai-see"));
if (appIcon.isNull()) {
appIcon = QIcon(QStringLiteral("/usr/share/icons/hicolor/256x256/apps/gxde-ai-see.png"));
@@ -35,17 +35,25 @@ int main(int argc, char *argv[])
QStringLiteral("/usr/share/gxde-ai-see/translations"),
QStringLiteral("/usr/local/share/gxde-ai-see/translations"),
};
bool translated = false;
QStringList candidates{ locale, locale.section(QLatin1Char('_'), 0, 0) };
/* ensure unsupported locales fall back to English, never to the Chinese
* source strings */
if (!locale.startsWith(QLatin1String("zh"))) {
candidates.append(QStringLiteral("en"));
}
for (const QString &dir : translationDirs) {
if (translator->load(QStringLiteral("gxde-ai-see_") + locale, dir) ||
translator->load(QStringLiteral("gxde-ai-see_") + locale.section(QLatin1Char('_'), 0, 0),
dir)) {
app.installTranslator(translator);
translated = true;
bool loaded = false;
for (const QString &candidate : candidates) {
if (translator->load(QStringLiteral("gxde-ai-see_") + candidate, dir)) {
app.installTranslator(translator);
loaded = true;
break;
}
}
if (loaded) {
break;
}
}
Q_UNUSED(translated)
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("GXDE 小埃同学"));
+8
View File
@@ -53,6 +53,9 @@ PortalClient::PortalClient(QObject *parent)
bus.connect(QString::fromLatin1(kService), QString::fromLatin1(kPath),
QString::fromLatin1(kInterface), QStringLiteral("PortalClosed"), this,
SLOT(onPortalClosed()));
bus.connect(QString::fromLatin1(kService), QString::fromLatin1(kPath),
QString::fromLatin1(kInterface), QStringLiteral("FullscreenChanged"), this,
SLOT(onFullscreenChanged(bool)));
}
bool PortalClient::beginPortal(const QRect &panelRect)
@@ -96,6 +99,11 @@ void PortalClient::onDragStarted(const QString &mimeTypes)
Q_EMIT dragStarted(mimeTypes);
}
void PortalClient::onFullscreenChanged(bool fullscreen)
{
Q_EMIT fullscreenChanged(fullscreen);
}
void PortalClient::onPortalClosed()
{
Q_EMIT portalClosed();
+2
View File
@@ -34,12 +34,14 @@ signals:
void dragStarted(const QString &mimeTypes);
void dragEnded();
void portalClosed();
void fullscreenChanged(bool fullscreen);
void dropped(const QString &uuid, bool accepted);
private slots:
void onDragStarted(const QString &mimeTypes);
void onDragEnded();
void onPortalClosed();
void onFullscreenChanged(bool fullscreen);
void onDropped(const QString &uuid, bool accepted);
private:
+39
View File
@@ -7,6 +7,9 @@
#include "config.h"
#include <QContextMenuEvent>
#include <QMenu>
#include <QApplication>
#include <QDialog>
#include <QDialogButtonBox>
@@ -159,6 +162,7 @@ TodoWindow::TodoWindow(TodoModel *model, QWidget *parent)
m_rightMargin = Config::instance()->getInt(QStringLiteral("todo/rightMargin"), 16);
m_topMargin = Config::instance()->getInt(QStringLiteral("todo/topMargin"), 72);
m_keepAbove = Config::instance()->getBool(QStringLiteral("todo/keepAbove"), true);
setupWindow();
setupUi();
@@ -172,6 +176,11 @@ TodoWindow::TodoWindow(TodoModel *model, QWidget *parent)
void TodoWindow::setupWindow()
{
if (!m_keepAbove) {
/* plain toplevel: shows in the taskbar and can be covered/moved */
setWindowFlag(Qt::Tool, false);
return;
}
#ifdef GXDE_HAVE_LAYERSHELLQT
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
createWinId();
@@ -308,6 +317,9 @@ void TodoWindow::addEntry()
void TodoWindow::applyMargins()
{
if (!m_keepAbove) {
return;
}
#ifdef GXDE_HAVE_LAYERSHELLQT
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
if (auto *layer = LayerShellQt::Window::get(windowHandle())) {
@@ -322,6 +334,26 @@ void TodoWindow::applyMargins()
}
}
void TodoWindow::contextMenuEvent(QContextMenuEvent *event)
{
QMenu menu(this);
QAction *pin = menu.addAction(tr("置顶"));
pin->setCheckable(true);
pin->setChecked(m_keepAbove);
QAction *hideAction = menu.addAction(tr("隐藏"));
QAction *chosen = menu.exec(event->globalPos());
if (chosen == hideAction) {
hide();
return;
}
if (chosen == pin) {
const bool keep = pin->isChecked();
Config::instance()->set(QStringLiteral("todo/keepAbove"), keep);
Config::instance()->sync();
Q_EMIT recreateRequested();
}
}
void TodoWindow::saveMargins()
{
Config::instance()->set(QStringLiteral("todo/rightMargin"), m_rightMargin);
@@ -332,6 +364,13 @@ void TodoWindow::saveMargins()
void TodoWindow::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && event->position().y() < 48) {
if (!m_keepAbove) {
if (windowHandle()) {
windowHandle()->startSystemMove();
}
event->accept();
return;
}
m_dragging = true;
m_dragStart = event->position().toPoint();
m_dragStartRight = m_rightMargin;
+5
View File
@@ -28,11 +28,15 @@ public:
void refreshLayout();
protected:
void contextMenuEvent(QContextMenuEvent *event) override;
void showEvent(QShowEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
signals:
void recreateRequested();
private:
void setupWindow();
void setupUi();
@@ -45,6 +49,7 @@ private:
QLabel *m_countLabel = nullptr;
int m_rightMargin = 16;
int m_topMargin = 72;
bool m_keepAbove = true;
bool m_dragging = false;
QPoint m_dragStart;
int m_dragStartRight = 0;