diff --git a/devel/1023.md b/devel/1023.md new file mode 100644 index 0000000000..af9427bdba --- /dev/null +++ b/devel/1023.md @@ -0,0 +1,84 @@ +# [1023] 商业版默认创建 AI 聊天标签页 + +## 1 相关文档 +- [dddd.md](dddd.md) - 任务文档模板 + +## 2 任务相关的代码文件 +- `src/Texmacs/Data/new_window.cpp` +- `src/Texmacs/Data/new_view.cpp` +- `src/Texmacs/Data/new_view.hpp` +- `src/Plugins/Qt/QTMTabPage.cpp` + +## 3 如何测试 + +### 3.1 确定性测试(单元测试) +```bash +xmake b stem +``` + +### 3.2 非确定性测试(文档验证) +1. 编译并启动商业版(非 IS_COMMUNITY) +2. 确认窗口默认有两个标签页:启动页(第一个,默认激活)、Chat(第二个) +3. 确认 Chat 标签页没有关闭按钮 +4. 确认关闭文档/标签页的快捷键对 Chat 标签页无效 +5. 编译并启动社区版(IS_COMMUNITY),确认默认没有 Chat 标签页 + +## 4 如何提交 + +提交前执行以下最少步骤: + +```bash +xmake b stem +``` + +## 5 What + +商业版(非 community)启动时默认创建 AI 聊天标签页,固定在第二个 tab 位置,但默认仍显示启动页。 + +1. `ensure_window` 中商业版默认创建 `tmfs://chat-tab` buffer,通过 `view_set_window(..., false)` 将其加入当前窗口但不激活 +2. 修复 `view_set_window` 的 `focus=false` 分支未注册 view_history 的问题,补充 `notify_set_view` +3. `kill_buffer` 和 `kill_tabpage` 中保护 chat-tab 不被关闭 +4. `QTMTabPage::updateCloseButtonVisibility` 中隐藏 chat-tab 的关闭按钮 +5. 各处添加 TODO 注释,标记后续需支持删除 + +## 6 Why + +提升商业版用户体验,让 AI 聊天功能开箱即用,同时保持启动页作为默认入口。 + +## 7 How + +### 7.1 默认创建 chat-tab + +在 `ensure_window` 的窗口初始化流程中,创建启动页后,商业版额外执行: + +```cpp +url chat_name= "tmfs://chat-tab"; +if (is_nil (concrete_buffer (chat_name))) { + create_buffer (chat_name, tree (DOCUMENT)); + set_title_buffer (chat_name, "Chat"); +} +url chat_view= get_passive_view (chat_name); +if (!is_none (chat_view)) { + view_set_window (chat_view, win, false); +} +``` + +### 7.2 view_history 注册 + +`view_set_window(..., false)` 原本只设置 `win_tabpage` 指针,不调用 `attach_view`,导致 view 漏掉 `view_history` 注册,tab bar 刷新时找不到该标签。 + +修复:在 `view_set_window` 的 `focus=false` 分支末尾补充 `notify_set_view(view_u)`,确保 view 被正确加入 view_history。 + +### 7.3 不可关闭 + +- `kill_buffer`:使用 `is_chat_tab_buffer(name)` 拦截所有以 `tmfs://chat-tab` 开头的 buffer +- `kill_tabpage`:使用 `is_chat_tab_buffer(vw->buf->buf->name)` 拦截关闭 +- `QTMTabPage::updateCloseButtonVisibility`:对 chat-tab 隐藏关闭按钮 + +### 7.4 固定位置 + +`QTMTabPageContainer::extractTabPages` 中已有排序逻辑: +- 启动页固定到第 0 位 +- chat-tab 固定到第 1 位 + +无需额外改动。 diff --git a/src/Plugins/Qt/QTMTabPage.cpp b/src/Plugins/Qt/QTMTabPage.cpp index e2ca5b7051..f7f592e921 100644 --- a/src/Plugins/Qt/QTMTabPage.cpp +++ b/src/Plugins/Qt/QTMTabPage.cpp @@ -319,8 +319,10 @@ QTMTabPage::leaveEvent (QEvent* e) { void QTMTabPage::updateCloseButtonVisibility () { if (!m_closeBtn) return; - bool shouldShow= - !is_startup_tab_view (m_viewUrl) && (underMouse () || isChecked ()); + // TODO: 聊天标签页当前不可关闭,后续需支持可删除 + bool shouldShow= !is_startup_tab_view (m_viewUrl) && + !is_chat_tab_view (m_viewUrl) && + (underMouse () || isChecked ()); bool wasVisible= m_closeBtn->isVisible (); m_closeBtn->setVisible (shouldShow); diff --git a/src/Texmacs/Data/new_view.cpp b/src/Texmacs/Data/new_view.cpp index 9b691bb482..fad368a629 100644 --- a/src/Texmacs/Data/new_view.cpp +++ b/src/Texmacs/Data/new_view.cpp @@ -82,7 +82,7 @@ decode_url (string s) { * @param name 待检测的 buffer URL。 * @return 若名称以 \c tmfs://chat-tab 开头则返回 true。 */ -static bool +bool is_chat_tab_buffer (url name) { return starts (as_string (name), "tmfs://chat-tab"); } @@ -446,25 +446,16 @@ kill_tabpage (url win_u, url u) { if (vw->buf != NULL && vw->buf->buf->name == url ("tmfs://startup-tab")) { return; } + // TODO: 聊天标签页当前不可关闭,后续需支持可删除 + if (vw->buf != NULL && is_chat_tab_buffer (vw->buf->buf->name)) { + return; + } tm_window win = vw->win; tm_window win_tabpage= vw->win_tabpage; if (win_tabpage == NULL) return; if (win == NULL) win= win_tabpage; - url current_u = get_current_view_safe (); - bool is_current= (!is_none (current_u) && current_u == u); - /** - * @note 对于聊天标签页 buffer,嵌入的输入编辑器可能持有键盘焦点, - * 而视图 URL 指向底层的 tmfs://chat-input-* / chat-message-* buffer。 - * 当它们共享同一个主控件时,我们将这类视图视为当前视图。 - */ - if (!is_current && vw->buf != NULL && - is_chat_tab_buffer (vw->buf->buf->name)) { - tm_view current_vw= concrete_view (current_u); - if (current_vw != NULL && current_vw->ed != NULL && - current_vw->ed->mvw == vw) { - is_current= true; - } - } + url current_u = get_current_view_safe (); + bool is_current = (!is_none (current_u) && current_u == u); bool refresh_tabbar_for_non_current= !is_current; // 第一步: 设定 win_tabpage @@ -686,6 +677,7 @@ view_set_window (url view_u, url win_u, bool focus) { } } view->win_tabpage= win; + notify_set_view (view_u); if (attached && !found) { // view 所在的 TabBar 没有其他标签页了 kill_window (view_win_tabpage_u); diff --git a/src/Texmacs/Data/new_view.hpp b/src/Texmacs/Data/new_view.hpp index 6c02494fe8..a0e49ffc46 100644 --- a/src/Texmacs/Data/new_view.hpp +++ b/src/Texmacs/Data/new_view.hpp @@ -51,6 +51,7 @@ bool var_focus_on_buffer (url name); void make_cursor_visible (url u); url get_most_recent_view (); void invalidate_most_recent_view (); +bool is_chat_tab_buffer (url name); bool is_tmfs_view_type (string s, string type); bool is_tmfs_view_type (url s, string type); diff --git a/src/Texmacs/Data/new_window.cpp b/src/Texmacs/Data/new_window.cpp index 6c75715c21..3541f0f656 100644 --- a/src/Texmacs/Data/new_window.cpp +++ b/src/Texmacs/Data/new_window.cpp @@ -305,11 +305,25 @@ ensure_window (tree geom) { set_title_buffer (name, title); url win= new_window (true, geom, true); window_set_view (win, get_passive_view (name), true); - return win; #else url name= make_welcome_buffer (); - return new_buffer_in_new_window (name, tree (DOCUMENT), geom); + url win = new_buffer_in_new_window (name, tree (DOCUMENT), geom); #endif + +#ifndef IS_COMMUNITY + // 商业版默认创建 AI 聊天标签页,固定在第二个位置 + url chat_name= "tmfs://chat-tab"; + if (is_nil (concrete_buffer (chat_name))) { + create_buffer (chat_name, tree (DOCUMENT)); + set_title_buffer (chat_name, "Chat"); + } + url chat_view= get_passive_view (chat_name); + if (!is_none (chat_view)) { + view_set_window (chat_view, win, false); + } +#endif + + return win; } array all_views = get_all_views (); @@ -342,6 +356,8 @@ clone_window () { void kill_buffer (url name) { if (name == url ("tmfs://startup-tab")) return; + // TODO: 聊天标签页当前不可关闭,后续需支持可删除 + if (is_chat_tab_buffer (name)) return; array vs= buffer_to_views (name); for (int i= 0; i < N (vs); i++) if (!is_none (vs[i])) {