Skip to content
Merged
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
84 changes: 84 additions & 0 deletions devel/1023.md
Original file line number Diff line number Diff line change
@@ -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 位

无需额外改动。
6 changes: 4 additions & 2 deletions src/Plugins/Qt/QTMTabPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
24 changes: 8 additions & 16 deletions src/Texmacs/Data/new_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/Texmacs/Data/new_view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
20 changes: 18 additions & 2 deletions src/Texmacs/Data/new_window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<url> all_views = get_all_views ();
Expand Down Expand Up @@ -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<url> vs= buffer_to_views (name);
for (int i= 0; i < N (vs); i++)
if (!is_none (vs[i])) {
Expand Down
Loading