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
18 changes: 18 additions & 0 deletions TeXmacs/plugins/llm/progs/llm/chat-protocol.scm
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@
) ;with
) ;tm-define

(tm-define (chat-tab-prefillable-source? buf)
(:synopsis "Whether BUF is a valid source for chat input prefill")
;; chat 相关 buffer(含 tmfs://chat-tab 与 tmfs://chat/<sid>/*)不作为来源,
;; 避免把聊天输入区自身的内容填回自身
(and buf (not (string-starts? (url->system buf) "tmfs://chat")))
) ;tm-define

(tm-define (chat-tab-prefill-from-selection! session-id)
(:synopsis "Prefill the chat input with the current document selection")
(:argument session-id "Session UUID")
;; 须在焦点离开文档编辑器之前调用,selection-tree 取自 current editor
(when (and (chat-tab-prefillable-source? (current-buffer)) (selection-active-any?))
(chat-tab-prefill-input! (chat-tab-session->input-buffer session-id)
(selection-tree)
) ;chat-tab-prefill-input!
) ;when
) ;tm-define

;;; ---------- 会话初始化 ----------

(tm-define (chat-tab-init-session! session-id model)
Expand Down
16 changes: 16 additions & 0 deletions TeXmacs/plugins/llm/progs/llm/chat-tree-ops.scm
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,22 @@
) ;with-buffer
) ;tm-define

(tm-define (chat-tab-should-prefill? input-buffer sel)
(:synopsis "Whether SEL should be prefilled into INPUT-BUFFER")
;; 仅在输入区为空且选区内容非空白时预填,避免覆盖用户已输入内容
(and sel
(not (chat-tab-empty-body? sel))
(chat-tab-buffer-empty? (buffer-get-body input-buffer))
) ;and
) ;tm-define

(tm-define (chat-tab-prefill-input! input-buffer sel)
(:synopsis "Prefill the chat input buffer with the selected content")
(when (chat-tab-should-prefill? input-buffer sel)
(chat-tab-set-input-body! input-buffer sel)
) ;when
) ;tm-define

;;; ---------- 追加对话轮次 ----------

(tm-define (chat-tab-append-round! message-buffer body model)
Expand Down
154 changes: 154 additions & 0 deletions TeXmacs/tests/1243.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; MODULE : 1243.scm
;; DESCRIPTION : 打开 AI 侧边栏时自动填入文档选区内容
;; COPYRIGHT : (C) 2026 Mogan STEM
;;
;; This software falls under the GNU general public license version 3 or later.
;; It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE
;; in the root directory or <http://www.gnu.org/licenses/gpl-3.0.html>.
;;
;; PURPOSE
;; chat-tab-should-prefill? / chat-tab-prefillable-source? 是纯判定,
;; headless 下直接断言。
;; 端到端用例以间谍替换写入函数 chat-tab-set-input-body!(其内部
;; with-buffer + buffer-focus 仅对已有嵌入式视图的生产输入 buffer
;; 有效),验证选区捕获与参数传递;真实写入路径由
;; chat-tab-clear-input! 的生产调用(每次发送后清空输入区)覆盖。
;;
;; USAGE
;; xmake b stem && xmake r 1243
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(texmacs-module (texmacs tests 1243)
(:use (utils library cursor))
) ;texmacs-module

(import (liii check))

(check-set-mode! 'report)

;;; ---------- 辅助 ----------

(define (in-buf sid)
(chat-tab-session->input-buffer sid)
) ;define

(define (reset-input! sid)
(buffer-set-body (in-buf sid) '(document ""))
) ;define

;;; ---------- chat-tab-should-prefill?(纯判定,headless) ----------

;; 输入区为空 + 选区非空 => 预填
(define (test-should-prefill-empty-input)
(let ((sid "test1243a"))
(reset-input! sid)
(check (chat-tab-should-prefill? (in-buf sid)
(stree->tree '(document "hello")))
=> #t)
) ;let
) ;define

;; 输入区已有内容 => 不覆盖
(define (test-should-prefill-nonempty-input)
(let ((sid "test1243b"))
(buffer-set-body (in-buf sid) '(document "typed"))
(check (chat-tab-should-prefill? (in-buf sid)
(stree->tree '(document "hello")))
=> #f)
) ;let
) ;define

;; 选区为 #f(无选区)=> 不预填
(define (test-should-prefill-false-selection)
(let ((sid "test1243c"))
(reset-input! sid)
(check (chat-tab-should-prefill? (in-buf sid) #f) => #f)
) ;let
) ;define

;; 选区为纯空白 => 不预填
(define (test-should-prefill-blank-selection)
(let ((sid "test1243d"))
(reset-input! sid)
(check (chat-tab-should-prefill? (in-buf sid)
(stree->tree '(document " ")))
=> #f)
) ;let
) ;define

;; 选区为原子串(selection-tree 对单行选区的实际返回形态)=> 预填
(define (test-should-prefill-atomic-selection)
(let ((sid "test1243e"))
(reset-input! sid)
(check (chat-tab-should-prefill? (in-buf sid) (string->tree "plain text"))
=> #t)
) ;let
) ;define

;;; ---------- chat-tab-prefillable-source?(来源 buffer 守卫) ----------

(define (test-prefillable-source)
(check (chat-tab-prefillable-source? (string->url "tmfs://chat/abc/input")) => #f)
(check (chat-tab-prefillable-source? (string->url "tmfs://chat/abc/message")) => #f)
(check (chat-tab-prefillable-source? (string->url "tmfs://chat-tab")) => #f)
(check (chat-tab-prefillable-source? (string->url "tmfs://startup-tab")) => #t)
(check (chat-tab-prefillable-source? (string->url "/tmp/test1243.tm")) => #t)
) ;define

;;; ---------- 端到端(选区 → 写入调用链) ----------

;; 写入函数 chat-tab-set-input-body! 内部走 with-buffer + buffer-focus,
;; 仅对已有嵌入式视图的生产输入 buffer 有效;测试中以间谍替换,
;; 验证包装函数正确捕获选区并以正确参数触发写入
(define prefill-spy '())

(define (test-prefill-from-selection-e2e)
(let ((sid "test1243e2e"))
(reset-input! sid)
(set! prefill-spy '())
(tm-define (chat-tab-set-input-body! input-buffer body)
(set! prefill-spy (list input-buffer body))
) ;tm-define
;; headless 验证过当前 buffer 上 insert/select-all 可用
(insert "pick me")
(select-all)
(check (selection-active-any?) => #t)
(chat-tab-prefill-from-selection! sid)
(check (url->string (car prefill-spy)) => "tmfs://chat/test1243e2e/input")
;; selection-tree 对单行选区返回原子串
(check (tree->stree (cadr prefill-spy)) => "pick me")
) ;let
) ;define

;; 无选区时包装函数不触发写入
(define (test-prefill-from-selection-no-selection)
(let ((sid "test1243e2f"))
(reset-input! sid)
(set! prefill-spy '())
(selection-cancel)
(check (selection-active-any?) => #f)
(chat-tab-prefill-from-selection! sid)
(check prefill-spy => '())
) ;let
) ;define

;;; ---------- 入口 ----------

(tm-define (test_1243)
;; llm 插件按 idle 延迟初始化,headless 下测试先于插件加载执行
(use-modules (llm chat-loader))
(test-should-prefill-empty-input)
(test-should-prefill-nonempty-input)
(test-should-prefill-false-selection)
(test-should-prefill-blank-selection)
(test-should-prefill-atomic-selection)
(test-prefillable-source)
;; e2e 会修改当前 buffer 内容并重定义写入函数,须最后执行
(test-prefill-from-selection-e2e)
(test-prefill-from-selection-no-selection)
(check-report)
(when (getenv "MOGAN_TEST_GUI") (quit-TeXmacs))
) ;tm-define
106 changes: 106 additions & 0 deletions devel/1243.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# [1243] 打开 AI 侧边栏时自动填入文档选区内容

## 1 相关文档

- [dddd.md](dddd.md) - 任务文档模板
- [1230.md](1230.md) - chat 首轮回显问题(texmacs_input_widget 覆盖 buffer 的教训)

## 2 任务相关的代码文件

- `src/Plugins/Qt/qt_tm_widget.cpp` — `sync_chat_sidebar_mode()`,侧边栏显隐同步,预填钩子挂在这里
- `TeXmacs/plugins/llm/progs/llm/chat-tree-ops.scm` — 纯判定 `chat-tab-should-prefill?` 与写入 `chat-tab-prefill-input!`
- `TeXmacs/plugins/llm/progs/llm/chat-protocol.scm` — 来源守卫 `chat-tab-prefillable-source?` 与包装 `chat-tab-prefill-from-selection!`
- `TeXmacs/tests/1243.scm` — 集成测试

## 3 如何测试

### 3.1 确定性测试(集成测试,headless 可跑全部断言)

```bash
xmake b stem && xmake r 1243
```

当前结果:15 correct, 0 failed(headless 与 GUI 模式均通过)。

覆盖:
- `chat-tab-should-prefill?` 全组合:输入区空/非空 × 选区有效/#f/纯空白/原子串
- `chat-tab-prefillable-source?`:chat 相关 buffer(`tmfs://chat/`、
`tmfs://chat-tab`)不作为预填来源
- 端到端:在当前 buffer 构造真实选区,间谍替换写入函数
`chat-tab-set-input-body!`(其内部 `with-buffer` + `buffer-focus` 仅对
已有嵌入式视图的生产输入 buffer 有效,测试环境无视图会挂起/短路),
验证选区捕获与参数传递;真实写入路径由 `chat-tab-clear-input!`
的生产调用(每次发送后清空输入区)覆盖

### 3.2 非确定性测试(GUI 验证,需非 community 构建)

community 构建没有 AI 侧边栏(`chatSideDock` 为空,
`sync_chat_sidebar_mode` 直接返回),C++ 钩子需非 community 配置构建后
手工验证:

```bash
xmake f --is_community=n -c --yes
xmake b stem
```

手工步骤:

1. 打开一个文档,选中一段文字
2. 点击文档区右上角的 AI 对话按钮(或快捷键 `std j`)
3. 侧边栏打开,输入区自动出现选中内容,光标在输入区
4. 不发送,关闭侧边栏后修改输入内容再重开:已有内容不应被覆盖
5. 未选中任何内容时打开侧边栏:输入区保持空白

## 4 如何提交

```bash
gf fmt --changed-since=main
git push
```

## 5 What

文档中存在选区时,打开 AI 对话侧边栏(点击右上角按钮或 `std j`),
若当前会话输入区为空,则自动把选中内容填入输入区。

1. scheme 纯判定 `chat-tab-should-prefill?`:输入区为空且选区非空白
2. scheme 写入 `chat-tab-prefill-input!`:复用生产已有写入路径
`chat-tab-set-input-body!`
3. scheme 包装 `chat-tab-prefill-from-selection!`:捕获当前编辑器选区,
守卫 chat 相关 buffer 不作为来源
4. C++ 钩子:`sync_chat_sidebar_mode()` 在侧边栏「隐藏→显示」转换时、
焦点切到聊天输入框之前调用上述包装函数

## 6 Why

用户选中一段内容后想让 AI 处理(解释、翻译、润色等),
目前需要手动复制粘贴到对话框。自动填入减少一步操作。

只在输入区为空时填入,避免覆盖用户已输入的内容;
只在侧边栏从隐藏变为显示时触发,避免 tab 切换等场景反复打扰。

## 7 How

- 预填判定全部放在 scheme 侧,复用已有的 `chat-tab-buffer-empty?` /
`chat-tab-empty-body?` / `chat-tab-set-input-body!`,C++ 只负责在正确的
时机调一行 `call`。
- 时机选择 `sync_chat_sidebar_mode()` 中 `chatSideDock->show()` 之后、
`focusInput()` 之前:此时 current editor 仍是文档编辑器(按钮是
`Qt::NoFocus`,点击不夺焦点),`selection-tree` 能取到文档选区。
该函数同时覆盖按钮点击和 scheme `show-chat-sidebar`(快捷键)两条路径。
- 用 `chatSideDock->isVisible()` 做隐藏→显示的转换检测,避免窗口布局
刷新、tab 切换恢复等重复调用 sync 时重复预填。
- 焦点已在 chat 相关 buffer(`tmfs://chat` 前缀,含 chat-tab 与
会话 input/message)时不预填,避免把聊天输入区自身的内容填回自身。

### 测试过程中的坑(记录)

- `with-buffer` 内部经 `buffer-focus` 切换视图,对无视图的 buffer 在
headless 下会挂起(`buffer-set-body` 直接建的 buffer 即无视图);
生产环境输入 buffer 由 `texmacs_input_widget` 创建嵌入式视图,无此问题。
因此测试用间谍替换写入函数,只验证判定与参数传递。
- headless 下当前 buffer(`tmfs://startup-tab`)上可直接
`insert` / `select-all` / `selection-tree` 构造真实选区。
- `check` 的输出经 stdout 缓冲,进程 `(exit 1)` 异常退出时缓冲丢失,
日志末尾只能看到最后几个用例的结果——误判过「前面的用例没跑」。
- 0205 在本分支基线上已有失败(`chat-tab-get-state` unbound),与本改动无关。
8 changes: 8 additions & 0 deletions src/Plugins/Qt/qt_tm_widget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,7 @@ qt_tm_widget_rep::sync_chat_sidebar_mode () {
if (!layout) return;

if (chatSidebarMode) {
bool wasVisible= chatSideDock->isVisible ();
// 确保不与全屏聊天模式同时存在
if (chatTabMode) {
chatTabMode= false;
Expand Down Expand Up @@ -1449,6 +1450,13 @@ qt_tm_widget_rep::sync_chat_sidebar_mode () {

chatSideDock->show ();
chatContentWidget->show ();
// 侧边栏由隐藏转为显示时,若输入区为空则预填文档选区内容。
// 必须在 focusInput 之前调用:此时 current editor 仍是文档编辑器,
// scheme 侧的 selection-tree 才能取到选区
if (!wasVisible && chatWidget && chatWidget->activeConversation ()) {
call ("chat-tab-prefill-from-selection!",
chatWidget->activeConversation ()->sessionId ());
}
// 焦点切到聊天输入框
if (chatWidget && chatWidget->activeConversation ()) {
chatWidget->activeConversation ()->focusInput ();
Expand Down
Loading