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/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 6e3b528e1f..c96749be22 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 ("Cancel");
+ 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);
}
@@ -1869,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));
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 待读取输入的会话面板。