Skip to content
Open
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
19 changes: 4 additions & 15 deletions TeXmacs/progs/dynamic/chat-session-persist.scm
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,9 @@
(title (cdr (assoc "title" entry)))
(model (cdr (assoc "model" entry)))
(archived-str (cdr (assoc "archived" entry)))
(msg-path (chat-persist-message-path sid))
(msg-buf (chat-tab-session->message-buffer sid))
) ;
;; 加载消息内容到 buffer
(when (file-exists? msg-path)
(let ((file-url (system->url msg-path)))
(buffer-load file-url)
(buffer-set-body msg-buf (buffer-get-body file-url))
(buffer-pretend-saved msg-buf)
) ;let
) ;when
;; 直接调用 C++ 回调创建 panel
;; C++ restore_conversation 会从文件加载消息内容
(qt-chat-tab-restore-session sid title model archived-str)
) ;let*
) ;lambda
Expand All @@ -110,12 +101,10 @@
;;; ---------- 增量保存 ----------

(tm-define (chat-persist-save-one session-id title model archived)
(let ((msg-path (chat-persist-message-path session-id))
(msg-buf (chat-tab-session->message-buffer session-id))
(let ((msg-buf (chat-tab-session->message-buffer session-id))
) ;
;; 1. 导出 message buffer
(chat-persist-ensure-dir! (chat-persist-parent-dir msg-path))
(buffer-export msg-buf (system->url msg-path) "tmu")
;; 1. 保存 message buffer 到磁盘(buffer 已绑定文件 URL,直接 save)
(buffer-save msg-buf)
;; 2. 增量更新 manifest
(let ((manifest-path (chat-persist-manifest-path))
(entry (chat-persist-make-entry session-id title model archived))
Expand Down
10 changes: 8 additions & 2 deletions TeXmacs/progs/dynamic/chat-tab-session.scm
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@
;;; ---------- Buffer URL 推导函数 ----------

(tm-define (chat-tab-session->message-buffer session-id)
(string->url (string-append "tmfs://chat-message-" session-id))
(let ((base-dir (string-append (url->system (get-texmacs-home-path))
"/system/ai-chat-sessions")))
(string->url (string-append base-dir "/" session-id "/message.tmu"))
) ;let
) ;tm-define

(define (chat-tab-session->input-buffer session-id)
(string->url (string-append "tmfs://chat-input-" session-id))
(let ((base-dir (string-append (url->system (get-texmacs-home-path))
"/system/ai-chat-sessions")))
(string->url (string-append base-dir "/" session-id "/input.tmu"))
) ;let
) ;define

;;; ---------- State 构造器和访问器 ----------
Expand Down
99 changes: 99 additions & 0 deletions devel/0210.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# [0210] Chat Tab Message Buffer 绑定硬盘存储

## 状态:可行性分析完成

## 背景

当前 chat tab 的 message buffer 使用 `tmfs://chat-message-{sessionId}` 虚拟 URL,内容仅存在于内存中。持久化需要显式调用 `buffer-export` 写入磁盘。用户希望将 message buffer 直接绑定到磁盘存储位置,简化持久化逻辑。

## 当前架构

```
新建会话:
C++ create_conversation() → texmacs_input_widget("", style, tmfs://chat-message-{sid})
→ 内存中的虚拟 buffer

保存:
C++ saveOneSession() → Scheme chat-persist-save-one()
→ buffer-export(tmfs buffer → $TEXMACS_HOME/system/ai-chat-sessions/{sid}/message.tmu)

加载:
Scheme chat-persist-load-all()
→ buffer-load(file.tmu) + buffer-set-body(tmfs buffer, file content)
→ C++ qt-chat-tab-restore-session → get_buffer_body(tmfs) → texmacs_input_widget(body, style, tmfs)
```

## 目标架构

```
新建会话:
1. 创建目录和空 TMU 文件: $TEXMACS_HOME/system/ai-chat-sessions/{sid}/message.tmu
2. C++ texmacs_input_widget("", style, file://...message.tmu)
→ buffer 直接关联磁盘文件

保存:
buffer-save(file buffer) → 直接写回磁盘文件
无需 buffer-export

加载:
C++ qt-chat-tab-restore-session → texmacs_input_widget("", style, file://...message.tmu)
→ buffer 从文件自动加载,无需 buffer-set-body
```

## 关键确认

1. **C++ 路径获取**: `get_texmacs_home_path()` 返回 `url` 类型,跨平台可用
2. **texmacs_input_widget 支持 file URL**: TeXmacs URL 系统统一处理 file:// 和 tmfs://
3. **空文件初始化**: 传 `tree(DOCUMENT, "")` 给 `texmacs_input_widget`,内部自动补全 TMU 头部
4. **Input buffer 保持 tmfs 不变**: input 是瞬态数据,无需持久化

## 修改位置

### C++ 侧 (`qt_chat_tab_widget.cpp/hpp`)
1. `ChatSessionManager::messageBufferUrl()` — 从 `tmfs://` 改为 file URL
2. `create_conversation()` — 创建 widget 前先创建空文件和目录
3. `restore_conversation()` — widget 直接从文件加载,无需 get_buffer_body 中转

### Scheme 侧
1. `chat-tab-session->message-buffer` — 从 `tmfs://` 改为 file URL
2. `chat-persist-save-one` — 简化为 `buffer-save`
3. `chat-persist-load-all` — 移除 `buffer-load` + `buffer-set-body` 拷贝
4. `chat-tab-ensure-session!` — `with-buffer` 改为操作 file URL(自动生效)

## 测试说明

### 1. 编译验证
```bash
xmake b stem
```
预期:编译通过,无新增 warning。

### 2. 新建会话文件创建验证
1. 启动 Mogan,切换到 Chat Tab
2. 新建一个会话
3. 检查磁盘目录:
$TEXMACS_HOME_PATH/system/ai-chat-sessions/{sid}/
4. 预期:`message.tmu` 和 `input.tmu` 文件存在(`input.tmu` 可能为空或很小)

### 3. 消息持久化验证
1. 在输入框输入内容,点击发送
2. 等待 LLM 回复完成后,检查 `message.tmu` 文件大小
3. 预期:`message.tmu` 文件大小 > 0,且内容包含发送的消息和 LLM 的回复
4. 直接用 TeXmacs 打开 `message.tmu`,检查内容格式是否正确

### 4. 重启恢复验证
1. 完全退出 Mogan
2. 重新启动 Mogan
3. 检查 Chat Tab 中的会话列表
4. 预期:之前的会话正确恢复,消息内容完整显示
5. 检查 `message.tmu` 的修改时间,确认没有被异常覆盖

### 5. 删除会话验证
1. 删除一个会话
2. 检查磁盘:对应的 `{sid}/` 目录应该被清理
3. 检查 manifest.json:该会话条目已移除

### 6. 边界情况
- 空会话(未发送任何消息):重启后应正确显示为空会话
- 多会话切换:切换会话时文件内容应保持独立
- 归档/恢复:归档后重启,会话状态正确保持
29 changes: 20 additions & 9 deletions src/Plugins/Qt/qt_chat_tab_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include "qt_utilities.hpp"
#include "qt_widget.hpp"
#include "s7_tm.hpp"
#include "tm_file.hpp"
#include "tm_sys_utils.hpp"
#include "tm_window.hpp"

#include <lolly/hash/uuid.hpp>
Expand Down Expand Up @@ -572,6 +574,11 @@ QTChatTabWidget::create_conversation (const QString& title) {
url msgBufUrl= ChatSessionManager::messageBufferUrl (sessionId);
url inBufUrl = ChatSessionManager::inputBufferUrl (sessionId);

// 确保消息 buffer 的父目录存在,以便后续 buffer-save 可写入
url msgDir= get_texmacs_home_path () * url ("system/ai-chat-sessions") *
url (sessionId);
if (!is_directory (msgDir)) mkdir (msgDir);

QWidget* page= new QWidget (conversationStack_);
page->setObjectName ("chat-tab-conversation-page");
panel->pageWidget= page;
Expand Down Expand Up @@ -1656,12 +1663,14 @@ ChatSessionManager::setPanel (const string& sessionId, void* panel) {

url
ChatSessionManager::messageBufferUrl (const string& sessionId) {
return url ("tmfs://chat-message-" * sessionId);
return get_texmacs_home_path () * url ("system/ai-chat-sessions") *
url (sessionId) * url ("message.tmu");
}

url
ChatSessionManager::inputBufferUrl (const string& sessionId) {
return url ("tmfs://chat-input-" * sessionId);
return get_texmacs_home_path () * url ("system/ai-chat-sessions") *
url (sessionId) * url ("input.tmu");
}

void
Expand Down Expand Up @@ -1783,12 +1792,13 @@ QTChatTabWidget::restore_conversation (const string& sessionId,
panel->modelLabel->setMinimumHeight (DpiUtils::scaled (20));
topLayout->addWidget (panel->modelLabel, 0, Qt::AlignHCenter);

// 恢复会话时,buffer 中已有 Scheme 加载的消息内容,需使用 buffer 内容而非空
// tree
tree msgBody= get_buffer_body (msgBufUrl);
if (is_empty_document_body (msgBody)) msgBody= tree (DOCUMENT, "");
panel->messageWidget=
texmacs_input_widget (msgBody, make_chat_embedded_style (), msgBufUrl);
// 从磁盘文件加载消息内容到 buffer
if (exists (msgBufUrl)) {
buffer_load (msgBufUrl);
}

panel->messageWidget= texmacs_input_widget (
tree (DOCUMENT, ""), make_chat_embedded_style (), msgBufUrl);

QWidget* messageQWidget= concrete (panel->messageWidget)->as_qwidget ();
panel->messageFrame = new QWidget (topPanel);
Expand Down Expand Up @@ -1941,7 +1951,8 @@ QTChatTabWidget::restore_conversation (const string& sessionId,
conversationListLayout_->addWidget (panel->itemWidget);

// 如果消息 buffer 非空,进入会话模式
if (!is_empty_document_body (msgBody)) {
tree restoredBody= get_buffer_body (msgBufUrl);
if (!is_empty_document_body (restoredBody)) {
enter_conversation_mode (panel);
}

Expand Down
Loading