Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TeXmacs/misc/images/images.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<file>pdf-reader/screenshot.svg</file>

<file>llm-chat/send.svg</file>
<file>llm-chat/cancel.svg</file>

<!-- dark theme -->
<file>ocr-button/left-align-white.svg</file>
Expand Down
114 changes: 114 additions & 0 deletions TeXmacs/misc/images/llm-chat/cancel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions TeXmacs/progs/dynamic/chat-adapter.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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
75 changes: 75 additions & 0 deletions devel/1017.md
Original file line number Diff line number Diff line change
@@ -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` 时出错
42 changes: 30 additions & 12 deletions src/Plugins/Qt/qt_chat_tab_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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); });
}
}

/**
Expand All @@ -1336,17 +1363,8 @@ QTChatTabWidget::notifyStateChanged (const string& sessionId,
static_cast<ChatConversationPanel*> (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);
}
Expand Down Expand Up @@ -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));
Expand Down
7 changes: 7 additions & 0 deletions src/Plugins/Qt/qt_chat_tab_widget.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 待读取输入的会话面板。
Expand Down
Loading