@@ -28,7 +28,6 @@ import (
2828 "charm.land/glamour/v2"
2929 "charm.land/glamour/v2/styles"
3030 "charm.land/lipgloss/v2"
31- "github.com/atotto/clipboard"
3231 "github.com/charmbracelet/x/ansi"
3332)
3433
@@ -716,8 +715,9 @@ func initialModel(models agent.ModelConfig, needsSetup bool, version string, hub
716715 mode : agent .AgentMode_Auto ,
717716 workingMode : agent .WorkingModeDefault , // 默认 kp;下方从 session 恢复
718717 status : "idle" ,
719- hideStatusPanel : metaGet ().HideStatus , // 记忆上次的状态栏显隐
720- showThinking : metaGet ().ShowThinking , // 记忆上次的思考显隐
718+ hideStatusPanel : metaGet ().HideStatus , // 记忆上次的状态栏显隐
719+ showThinking : metaGet ().ShowThinking , // 记忆上次的思考显隐
720+ mousePassthrough : metaGet ().MousePassthrough , // 记忆上次的鼠标穿透选择(F2)
721721 spinner : sp ,
722722 workspace : wd ,
723723 setupInput : si ,
@@ -1465,14 +1465,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
14651465 return m , nil
14661466 }
14671467 if msg .Button != tea .MouseLeft {
1468- // 右键点击输入区 → 读取剪贴板文本并粘贴
1469- if msg .Button == tea .MouseRight {
1470- leftW , vpH := m .layout ()
1471- inInput := msg .Y >= vpH && msg .Y < m .height && msg .X >= 0 && msg .X < leftW
1472- if inInput {
1473- if text , err := clipboard .ReadAll (); err == nil && text != "" {
1474- m .input .InsertString (text )
1475- }
1468+ // 右键点击输入区 → 读剪贴板文本粘贴(issue #211)。走 insertPastedText 与 tea.PasteMsg
1469+ // 同一条管线:CRLF 归一 + 超长走占位符,少一样都会出问题(见该函数注释)。
1470+ if msg .Button == tea .MouseRight && m .inInputArea (msg .X , msg .Y ) {
1471+ if text , err := readClipboardText (); err == nil && text != "" {
1472+ return m , m .insertPastedText (text )
14761473 }
14771474 }
14781475 return m , nil
@@ -1492,11 +1489,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
14921489 // 选区只认内容列 [chatLeft, leftW),分隔线列按 X 与上面互斥。
14931490 inChat := msg .X >= chatLeft && msg .X < leftW &&
14941491 msg .Y >= chatTop && msg .Y < chatBottom
1495- // 输入区:左列 body 下方那块(空白行 + textarea 行),Y ∈ [vpH, m.height) 且 X ∈ [0, leftW)。
1496- // X 上界收到 leftW —— 右侧是全高状态栏,点那里不该进输入编辑。
1497- inInput := msg .Y >= vpH && msg .Y < m .height && msg .X >= 0 && msg .X < leftW
1498-
1499- if inInput {
1492+ if m .inInputArea (msg .X , msg .Y ) {
15001493 // 按下进入输入区:清 chat 选区 + 记拖拽锚点,准备"拖拽选择片段"。
15011494 // 同时把上一次的输入选区清掉:若接着是真拖动,MouseMotion 会重新点亮;
15021495 // 若只是单击松手,就等价于"点一下取消"(#188)。
@@ -1681,28 +1674,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
16811674 }
16821675 return m , nil
16831676 }
1684- // 文本粘贴:超阈值(>800 字符,或行数超 pasteLineCap=min(height-10,2)、常态 >2 行)走占位符,否则让 textarea 正常接。
1685- // 占位符方案避免超长文本被 textarea 的 CharLimit(4000)截断、也不撑爆输入区。
1686- // 进入 textarea 之前先把 \r\n 归一为 \n:否则 textarea 的 Sanitizer 会把
1687- // \r\n 拆成两个 \n(\r 一个、\n 一个),造成行翻倍、历史/输入框错乱(见 normalizeNewlines)。
1688- text := msg .Content
1689- text = normalizeNewlines (text )
1690- numLines := strings .Count (text , "\n " )
1691- if len (text ) > pasteTextThreshold || numLines > m .pasteLineCap () {
1692- // 先回收上一轮已无引用的全文,再存新的;避免反复大段粘贴时 map 只增不减。
1693- m .prunePastedTexts ()
1694- if m .pastedTexts == nil {
1695- m .pastedTexts = make (map [int ]string )
1696- }
1697- id := m .nextPasteID
1698- m .nextPasteID ++
1699- m .pastedTexts [id ] = text
1700- m .input .InsertString (formatPastedTextRef (id , numLines ))
1701- return m , nil
1702- }
1703- var c tea.Cmd
1704- m .input , c = m .input .Update (tea.PasteMsg {Content : text })
1705- return m , c
1677+ return m , m .insertPastedText (msg .Content )
17061678
17071679 case tea.KeyPressMsg :
17081680 // 配置 modal 处于活动状态时,按键全部在这里处理,绕过主界面。两步:选供应商 / 填配置。
@@ -2494,14 +2466,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
24942466 return m , nil
24952467 case "f2" :
24962468 // 切换鼠标穿透模式:关掉 MouseMode,让终端原生处理右键粘贴和文字选择。
2497- m .mousePassthrough = ! m .mousePassthrough
2498- hint := "鼠标"
2499- if m .mousePassthrough {
2500- hint += "穿透已开启(终端原生选择/粘贴)"
2501- } else {
2502- hint += "捕获已恢复(deepx 选区/复制)"
2503- }
2504- m .appendChat ("System" , hint )
2469+ m .toggleMousePassthrough ()
25052470 return m , nil
25062471 case "ctrl+v" :
25072472 // 剪贴板有图就落盘并插入到输入框;没图则下落到 textinput 走文本粘贴。
@@ -3958,6 +3923,65 @@ func (m model) compactCmd(manual bool) tea.Cmd {
39583923 }
39593924}
39603925
3926+ // inInputArea 判断鼠标坐标是否落在输入区:左列 body 下方那块(空白行 + textarea 行),
3927+ // Y ∈ [vpH, m.height) 且 X ∈ [0, leftW)。X 上界收到 leftW —— 右侧是全高状态栏,点那里不该进输入编辑。
3928+ // 左键(进入编辑/拖选)与右键(粘贴)共用同一判断,避免两处各写一份、日后改布局漏改一边。
3929+ func (m model ) inInputArea (x , y int ) bool {
3930+ leftW , vpH := m .layout ()
3931+ return y >= vpH && y < m .height && x >= 0 && x < leftW
3932+ }
3933+
3934+ // insertPastedText 把一段外部文本按统一规则送进输入框。**所有粘贴入口都必须走这里**
3935+ // (终端 tea.PasteMsg、右键粘贴),绕过去会踩两个坑:
3936+ // - 不归一 CRLF:textarea 的 Sanitizer 会把 \r\n 拆成两个 \n(\r 一个、\n 一个),
3937+ // 行数翻倍、输入框与历史一起错乱(Windows / WSL2 剪贴板里 CRLF 是常态,见 normalizeNewlines);
3938+ // - 不走占位符:超阈值(>pasteTextThreshold 字符,或行数超 pasteLineCap)的文本直插,
3939+ // 会被 textarea 的 CharLimit(4000)静默截断,还把输入区撑爆。
3940+ //
3941+ // 返回值是要交回 bubbletea 的 Cmd(走占位符时为 nil)。
3942+ func (m * model ) insertPastedText (text string ) tea.Cmd {
3943+ text = normalizeNewlines (text )
3944+ if text == "" {
3945+ return nil
3946+ }
3947+ numLines := strings .Count (text , "\n " )
3948+ if len (text ) > pasteTextThreshold || numLines > m .pasteLineCap () {
3949+ // 先回收上一轮已无引用的全文,再存新的;避免反复大段粘贴时 map 只增不减。
3950+ m .prunePastedTexts ()
3951+ if m .pastedTexts == nil {
3952+ m .pastedTexts = make (map [int ]string )
3953+ }
3954+ id := m .nextPasteID
3955+ m .nextPasteID ++
3956+ m .pastedTexts [id ] = text
3957+ m .input .InsertString (formatPastedTextRef (id , numLines ))
3958+ return nil
3959+ }
3960+ var c tea.Cmd
3961+ m .input , c = m .input .Update (tea.PasteMsg {Content : text })
3962+ return c
3963+ }
3964+
3965+ // toggleMousePassthrough 切换鼠标穿透(F2):关掉鼠标捕获后由终端原生处理选择与右键粘贴。
3966+ // 记忆到 meta(同 Ctrl+B 的状态栏显隐),否则每次启动都要重按一次 —— 需要穿透的用户是一直需要。
3967+ //
3968+ // 切换时必须清掉选区/拖拽态:穿透打开后 deepx 收不到任何鼠标事件,若此刻正拖着选区,
3969+ // MouseReleaseMsg 永远不会到,高亮会一直卡在屏幕上。
3970+ func (m * model ) toggleMousePassthrough () {
3971+ m .mousePassthrough = ! m .mousePassthrough
3972+ metaUpdate (func (mm * meta ) { mm .MousePassthrough = m .mousePassthrough })
3973+ m .selecting = false
3974+ m .inputSelecting = false
3975+ m .inputDragging = false
3976+ m .scrollbarDragging = false
3977+ if m .mousePassthrough {
3978+ m .chatContent .Open (kindSystem , T ("mouse.passthrough.on" ))
3979+ } else {
3980+ m .chatContent .Open (kindSystem , T ("mouse.passthrough.off" ))
3981+ }
3982+ m .refreshViewport ()
3983+ }
3984+
39613985// compactFailNote 生成"自动压缩失败"提示的正文,按处置区分说法。
39623986//
39633987// 原来两种情况统一写"本轮改用撞窗口保护",既看不懂、也偏乐观 —— 那不是什么主动防护,
0 commit comments