Skip to content

Commit 4c4da4d

Browse files
committed
🐛 fix CJK 输入残影
1 parent 60663a9 commit 4c4da4d

5 files changed

Lines changed: 25 additions & 40 deletions

File tree

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ require (
2121
github.com/atotto/clipboard v0.1.4 // indirect
2222
github.com/aymerick/douceur v0.2.0 // indirect
2323
github.com/charmbracelet/colorprofile v0.4.3 // indirect
24-
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
24+
github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa // indirect
2525
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
2626
github.com/charmbracelet/x/term v0.2.2 // indirect
2727
github.com/charmbracelet/x/termios v0.1.1 // indirect
@@ -44,7 +44,7 @@ require (
4444
golang.org/x/mod v0.36.0 // indirect
4545
golang.org/x/net v0.54.0 // indirect
4646
golang.org/x/sync v0.20.0 // indirect
47-
golang.org/x/sys v0.44.0 // indirect
47+
golang.org/x/sys v0.45.0 // indirect
4848
golang.org/x/text v0.37.0 // indirect
4949
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
5050
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex
2424
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
2525
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
2626
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
27+
github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa h1:rRT2qwk9xbontVloCXEUIsl1ePz0XFcIWkGi2bvmSTY=
28+
github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY=
2729
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
2830
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
2931
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
@@ -92,6 +94,8 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
9294
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
9395
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
9496
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
97+
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
98+
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
9599
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
96100
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
97101
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=

tui/dashboard.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"deepx/agent"
55
"fmt"
66
"os"
7+
"runtime"
78
"strings"
89
"time"
910

@@ -37,14 +38,15 @@ func padLinesToWidth(content string, w int) string {
3738
return strings.Join(lines, "\n")
3839
}
3940

40-
// graphemeWidthMode 决定显示宽度按 grapheme / Unicode-core(DEC mode 2027)还是 wcwidth 口径算。
41-
// detectGraphemeMode() 只是初始猜测;真正口径由终端对 mode 2027 的真实应答(ModeReportMsg)
42-
// 在运行时校正(见 model.go 的 applyUnicodeCoreReport)。deepx 自有排版(分割线/横幅等,经
43-
// lineDisplayWidth)和 bubbletea cellbuf(textarea 渲染)都跟着终端真实能力走 —— 否则在不支持
44-
// 2027 的终端(Windows conhost / 传统 PowerShell)强行按 grapheme 算,会与终端实际渲染错位,
45-
// 表现为输入框在 ASCII 间插入宽字符时光标后内容重复(issue #113)。
4641
var graphemeWidthMode = detectGraphemeMode()
4742

43+
var widthFunc = func() func(string) int {
44+
if graphemeWidthMode {
45+
return ansi.StringWidth
46+
}
47+
return ansi.StringWidthWc
48+
}()
49+
4850
func detectGraphemeMode() bool {
4951
switch os.Getenv("TERM_PROGRAM") {
5052
case "vscode", "Apple_Terminal", "iTerm.app", "WezTerm", "ghostty":
@@ -59,21 +61,14 @@ func detectGraphemeMode() bool {
5961
if os.Getenv("VTE_VERSION") != "" || os.Getenv("KONSOLE_VERSION") != "" {
6062
return true
6163
}
62-
// Windows Terminal:现代版支持 2027,先乐观猜 true,真实应答若不支持会下调到 wcwidth。
63-
// 注意:不再因 runtime.GOOS == "windows" 一律默认 true —— conhost / 传统 PowerShell 不支持
64-
// 2027,默认 false 走 wcwidth 才与终端实际渲染一致(issue #113)。
65-
if os.Getenv("WT_SESSION") != "" {
64+
if runtime.GOOS == "windows" || os.Getenv("WT_SESSION") != "" {
6665
return true
6766
}
6867
return false
6968
}
7069

71-
// lineDisplayWidth 每次按当前 graphemeWidthMode 现取口径,运行时被 ModeReport 校正后即时生效。
7270
func lineDisplayWidth(s string) int {
73-
if graphemeWidthMode {
74-
return ansi.StringWidth(s)
75-
}
76-
return ansi.StringWidthWc(s)
71+
return widthFunc(s)
7772
}
7873

7974
// isWhitespaceLike 判断 rune 是否是已经能起字符边界作用的空白。

tui/dashboard_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ func TestDetectGraphemeMode(t *testing.T) {
2222
{"WindowsTerminal → grapheme", map[string]string{"WT_SESSION": "some-guid"}, true},
2323
{"GNOME/VTE → grapheme", map[string]string{"VTE_VERSION": "7200"}, true},
2424
{"Konsole → grapheme", map[string]string{"KONSOLE_VERSION": "220400"}, true},
25-
// conhost / 传统 PowerShell:无任何特征 env → 默认 wcwidth(issue #113)。
26-
{"plain(conhost/PowerShell) → wcwidth", map[string]string{}, false},
2725
}
2826
for _, tc := range cases {
2927
t.Run(tc.name, func(t *testing.T) {

tui/model.go

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,24 +1025,21 @@ func (m model) Init() tea.Cmd {
10251025
}
10261026
// 视觉能力探测:每次启动对各模型重探一次(见 vision.go),结果经 visionCapMsg 回灌。
10271027
cmds = append(cmds, visionProbeCmds(m.models)...)
1028-
// 不再强制注入伪 ModeReportMsg 切 grapheme —— bubbletea 启动会自己向终端查询 mode 2027,
1029-
// 终端真实应答经 applyUnicodeCoreReport 校正显示宽度口径,让 deepx 与终端一致(issue #113)。
1028+
if cmd := ForceGraphemeCmd(); cmd != nil {
1029+
cmds = append(cmds, cmd)
1030+
}
10301031
// 启动即把控制态与已恢复的历史推进 hub 快照,晚连接的浏览器据此与 TUI 对齐。
10311032
m.broadcastControlState()
10321033
m.broadcastSessionLoaded()
10331034
return tea.Batch(cmds...)
10341035
}
10351036

1036-
// applyUnicodeCoreReport 据终端对 mode 2027(Unicode-core)的真实应答校正显示宽度口径。
1037-
// 与 bubbletea cellbuf 的判定保持同口径(其 tea.go 内部:Set/Reset/PermanentlySet 才启用 grapheme),
1038-
// 否则 deepx 自有排版与 textarea 渲染口径不一致,在不支持 2027 的终端会让输入框插入宽字符后
1039-
// 光标后内容重复(issue #113)。NotRecognized / PermanentlyReset 视为不支持,退回 wcwidth。
1040-
func applyUnicodeCoreReport(value ansi.ModeSetting) {
1041-
switch value {
1042-
case ansi.ModeSet, ansi.ModeReset, ansi.ModePermanentlySet:
1043-
graphemeWidthMode = true
1044-
default:
1045-
graphemeWidthMode = false
1037+
func ForceGraphemeCmd() tea.Cmd {
1038+
if !graphemeWidthMode {
1039+
return nil
1040+
}
1041+
return func() tea.Msg {
1042+
return tea.ModeReportMsg{Mode: ansi.ModeUnicodeCore, Value: ansi.ModeSet}
10461043
}
10471044
}
10481045

@@ -1057,15 +1054,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
10571054

10581055
switch msg := msg.(type) {
10591056

1060-
case tea.ModeReportMsg:
1061-
// 终端对 mode 2027(Unicode-core)的真实应答。据此把显示宽度口径校正到与终端一致,
1062-
// 修输入框插入宽字符后光标后内容重复的问题(issue #113)。其它 mode(如 2026 同步输出)
1063-
// 由 bubbletea 内部处理,这里只关心 Unicode-core。
1064-
if msg.Mode == ansi.ModeUnicodeCore {
1065-
applyUnicodeCoreReport(msg.Value)
1066-
}
1067-
return m, nil
1068-
10691057
case webInputMsg:
10701058
// 浏览器提交的输入,走和终端 Enter 完全相同的提交逻辑。
10711059
var cmd tea.Cmd

0 commit comments

Comments
 (0)