From db026c1549c9df1b03a358b1f85959bf3ca952f0 Mon Sep 17 00:00:00 2001 From: Yuki Date: Wed, 20 May 2026 15:18:23 +0800 Subject: [PATCH 1/2] cancel-button --- TeXmacs/misc/images/images.qrc | 1 + TeXmacs/misc/images/llm-chat/cancel.svg | 114 ++++++++++++++++++++++++ src/Plugins/Qt/qt_chat_tab_widget.cpp | 40 ++++++--- src/Plugins/Qt/qt_chat_tab_widget.hpp | 7 ++ 4 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 TeXmacs/misc/images/llm-chat/cancel.svg diff --git a/TeXmacs/misc/images/images.qrc b/TeXmacs/misc/images/images.qrc index 437e5b9a16..61cd924d7b 100644 --- a/TeXmacs/misc/images/images.qrc +++ b/TeXmacs/misc/images/images.qrc @@ -18,6 +18,7 @@ pdf-reader/screenshot.svg llm-chat/send.svg + llm-chat/cancel.svg ocr-button/left-align-white.svg diff --git a/TeXmacs/misc/images/llm-chat/cancel.svg b/TeXmacs/misc/images/llm-chat/cancel.svg new file mode 100644 index 0000000000..cfd59c2abd --- /dev/null +++ b/TeXmacs/misc/images/llm-chat/cancel.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Plugins/Qt/qt_chat_tab_widget.cpp b/src/Plugins/Qt/qt_chat_tab_widget.cpp index 6e3b528e1f..a28e57208e 100644 --- a/src/Plugins/Qt/qt_chat_tab_widget.cpp +++ b/src/Plugins/Qt/qt_chat_tab_widget.cpp @@ -1287,6 +1287,7 @@ QTChatTabWidget::handle_send (ChatConversationPanel* panel) { if (!as_bool (call ("chat-tab-send", panel->sessionId))) return; sessionManager_.setState (panel->sessionId, ChatState::Generating); + update_send_button (panel, ChatState::Generating); enter_conversation_mode (panel); refresh_sidebar (); focus_input_editor (panel); @@ -1314,6 +1315,32 @@ QTChatTabWidget::handle_cancel (ChatConversationPanel* panel) { if (!panel) return; call ("chat-tab-cancel", panel->sessionId); sessionManager_.setState (panel->sessionId, ChatState::Idle); + update_send_button (panel, ChatState::Idle); +} + +/** + * @brief 更新发送/取消按钮的图标和点击行为。 + * @param panel 目标会话面板。 + * @param state 当前生成状态。 + */ +void +QTChatTabWidget::update_send_button (ChatConversationPanel* panel, + ChatState state) { + if (!panel || !panel->sendButton) return; + + disconnect (panel->sendButton, &QPushButton::clicked, this, nullptr); + if (state == ChatState::Generating) { + panel->sendButton->setToolTip ("Stop"); + panel->sendButton->setIcon (QIcon (":llm-chat/cancel.svg")); + connect (panel->sendButton, &QPushButton::clicked, this, + [this, panel] () { handle_cancel (panel); }); + } + else { + panel->sendButton->setToolTip ("Send"); + panel->sendButton->setIcon (QIcon (":llm-chat/send.svg")); + connect (panel->sendButton, &QPushButton::clicked, this, + [this, panel] () { handle_send (panel); }); + } } /** @@ -1336,17 +1363,8 @@ QTChatTabWidget::notifyStateChanged (const string& sessionId, static_cast (session->panel); if (!panel || !panel->sendButton) return; - if (newState == ChatState::Generating) { - panel->sendButton->setToolTip ("Stop"); - disconnect (panel->sendButton, &QPushButton::clicked, this, nullptr); - connect (panel->sendButton, &QPushButton::clicked, this, - [this, panel] () { handle_cancel (panel); }); - } - else { - panel->sendButton->setToolTip ("Send"); - disconnect (panel->sendButton, &QPushButton::clicked, this, nullptr); - connect (panel->sendButton, &QPushButton::clicked, this, - [this, panel] () { handle_send (panel); }); + update_send_button (panel, newState); + if (newState == ChatState::Idle) { // 模型输出结束,保存会话内容 saveOneSession (sessionId); } diff --git a/src/Plugins/Qt/qt_chat_tab_widget.hpp b/src/Plugins/Qt/qt_chat_tab_widget.hpp index 323df8ca77..667dc1fd5f 100644 --- a/src/Plugins/Qt/qt_chat_tab_widget.hpp +++ b/src/Plugins/Qt/qt_chat_tab_widget.hpp @@ -237,6 +237,13 @@ class QTChatTabWidget : public QWidget { */ void handle_cancel (ChatConversationPanel* panel); + /** + * @brief 更新发送/取消按钮的图标和点击行为。 + * @param panel 目标会话面板。 + * @param state 当前生成状态。 + */ + void update_send_button (ChatConversationPanel* panel, ChatState state); + /** * @brief 从输入 buffer 中获取文档树。 * @param panel 待读取输入的会话面板。 From f0f34c9e6f48993b6eabd2c1fe2951267caf1519 Mon Sep 17 00:00:00 2001 From: Yuki Date: Wed, 20 May 2026 16:01:13 +0800 Subject: [PATCH 2/2] =?UTF-8?q?scm=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TeXmacs/progs/dynamic/chat-adapter.scm | 6 ++- devel/1017.md | 75 ++++++++++++++++++++++++++ src/Plugins/Qt/qt_chat_tab_widget.cpp | 4 +- 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 devel/1017.md diff --git a/TeXmacs/progs/dynamic/chat-adapter.scm b/TeXmacs/progs/dynamic/chat-adapter.scm index cbfae03475..bbb5a63875 100644 --- a/TeXmacs/progs/dynamic/chat-adapter.scm +++ b/TeXmacs/progs/dynamic/chat-adapter.scm @@ -94,9 +94,11 @@ (:synopsis "Adapter cancel entry for a chat tab") (:argument session-id "Session UUID") (let* ((st (chat-tab-get-state session-id)) - (model (chat-tab-state-model st)) + (model (if st (chat-tab-state-model st) "default")) (plugin-ses (string-append model ":chat-tab:" session-id)) ) ; - (plugin-cancel chat-tab-session-name plugin-ses #f) + (if (== (connection-status "llm" plugin-ses) 3) + (connection-interrupt "llm" plugin-ses)) + (plugin-cancel "llm" plugin-ses #f) ) ;let* ) ;tm-define diff --git a/devel/1017.md b/devel/1017.md new file mode 100644 index 0000000000..8d1e6c29ec --- /dev/null +++ b/devel/1017.md @@ -0,0 +1,75 @@ +# [1017] Chat Tab 取消按钮功能 + +## 1 相关文档 +- [dddd.md](dddd.md) - 任务文档模板 +- [1016.md](1016.md) - Chat Tab 会话持久化 + +## 2 任务相关的代码文件 +- `src/Plugins/Qt/qt_chat_tab_widget.hpp` +- `src/Plugins/Qt/qt_chat_tab_widget.cpp` +- `TeXmacs/progs/dynamic/chat-adapter.scm` +- `TeXmacs/misc/images/llm-chat/cancel.svg` +- `TeXmacs/misc/images/images.qrc` + +## 3 如何测试 + +### 3.1 确定性测试(构建验证) +```bash +xmake b stem +``` + +### 3.2 非确定性测试(功能验证) +1. 启动 Mogan STEM,打开 Chat Tab +2. 在输入框输入内容,点击 Send 按钮 +3. 观察按钮图标是否从 send.svg 变为 cancel.svg +4. 观察鼠标悬停时 tooltip 是否显示 "Cancel" +5. 点击 Cancel 按钮,观察图标是否恢复为 send.svg,tooltip 恢复为 "Send" +6. 验证 LLM 生成是否被中断(依赖插件端实现) + +## 4 如何提交 + +提交前执行以下最少步骤: + +```bash +xmake b stem +``` + +## 5 What + +实现 Chat Tab 发送按钮在 LLM 生成过程中的取消功能: + +1. 点击 Send 后,按钮立即切换为 Cancel 图标和点击行为 +2. 点击 Cancel 后,按钮立即恢复为 Send 图标和点击行为 +3. 新增 `update_send_button` 私有方法统一管理按钮状态切换 +4. 修复按钮状态切换的时序问题(原实现依赖 Scheme 回调,延迟或缺失导致 cancel 不可用) +5. 修复 Scheme 层 `chat-tab-cancel` 无法中断正在运行的 LLM 生成的问题 + +## 6 Why + +### C++ 层问题 +原代码虽然定义了 `handle_cancel` 和 `notifyStateChanged`,但按钮状态切换完全依赖 Scheme 层回调 "generating" 状态。实际使用中,Scheme 回调存在延迟甚至不触发,导致: + +- 用户点击 Send 后,按钮长时间保持 Send 状态 +- LLM 已经开始生成,但按钮仍连接 `handle_send`,用户无法取消 +- `chat-tab-cancel` Scheme 函数实际上不可达 + +### Scheme 层问题 +`chat-tab-cancel` 原来只调用 `plugin-cancel`,而它只能取消还在 **pending 队列**里等待执行的任务。一旦 LLM worker 取走任务开始网络请求,`pending` 队列就空了,`plugin-cancel` 找不到任何东西可取消,LLM 生成继续运行。 + +## 7 How + +### C++ 层 +1. **新增 `update_send_button(panel, state)` 方法**:统一封装图标切换(send.svg ↔ cancel.svg)、tooltip 更新("Send" ↔ "Cancel")和信号重新绑定(`handle_send` ↔ `handle_cancel`) + +2. **`handle_send` 成功后立即切换**:在 `call("chat-tab-send")` 返回成功后,立即调用 `update_send_button(panel, Generating)`,无需等待 Scheme 回调 + +3. **`handle_cancel` 成功后立即恢复**:在 `call("chat-tab-cancel")` 和设置 Idle 状态后,立即调用 `update_send_button(panel, Idle)` + +4. **简化 `notifyStateChanged`**:复用 `update_send_button`,去除重复的 disconnect/connect 逻辑,仅保留 Scheme 通知时的额外副作用(如保存会话) + +5. **初始化时设置 tooltip**:在 `create_conversation` 和 `restore_conversation` 中初始化按钮时一并设置默认 tooltip "Send" + +### Scheme 层 +6. **增加 `connection-interrupt` 调用**:在 `chat-tab-cancel` 中,先检查插件会话状态 `(connection-status "llm" plugin-ses)`,若返回 `3`(busy/running),则调用 `connection-interrupt` 直接向插件发送中断信号,绕过 pending 队列 + +7. **防御 nil access**:将 `(chat-tab-state-model st)` 改为 `(if st (chat-tab-state-model st) "default")`,防止 `chat-tab-get-state` 返回 `#f` 时出错 diff --git a/src/Plugins/Qt/qt_chat_tab_widget.cpp b/src/Plugins/Qt/qt_chat_tab_widget.cpp index a28e57208e..c96749be22 100644 --- a/src/Plugins/Qt/qt_chat_tab_widget.cpp +++ b/src/Plugins/Qt/qt_chat_tab_widget.cpp @@ -1330,7 +1330,7 @@ QTChatTabWidget::update_send_button (ChatConversationPanel* panel, disconnect (panel->sendButton, &QPushButton::clicked, this, nullptr); if (state == ChatState::Generating) { - panel->sendButton->setToolTip ("Stop"); + panel->sendButton->setToolTip ("Cancel"); panel->sendButton->setIcon (QIcon (":llm-chat/cancel.svg")); connect (panel->sendButton, &QPushButton::clicked, this, [this, panel] () { handle_cancel (panel); }); @@ -1887,7 +1887,7 @@ QTChatTabWidget::restore_conversation (const string& sessionId, panel->sendButton->setFocusPolicy (Qt::NoFocus); panel->sendButton->setCursor (Qt::PointingHandCursor); panel->sendButton->setIcon (QIcon (":llm-chat/send.svg")); - int sendIconSize= DpiUtils::scaled (24); + int sendIconSize= DpiUtils::scaled (30); panel->sendButton->setIconSize (QSize (sendIconSize, sendIconSize)); panel->sendButton->setFixedSize (DpiUtils::scaled (36), DpiUtils::scaled (36));