@@ -32,11 +32,8 @@ func (m model) wrapView(content string) tea.View {
3232 if m .showSetup || m .showMcpAdd || m .showSkillAdd || m .showWebConfig {
3333 v .MouseMode = tea .MouseModeNone
3434 }
35- // 开 Kitty keyboard 协议的"alternate keys"上报 — 让 Ctrl+Enter / Shift+Enter
36- // 等组合键以独立 escape 序列发到程序,而不是被终端合并成普通 Enter。
37- // 支持的终端:Kitty / Wezterm / Foot / iTerm2(实验性);macOS Terminal.app 不支持
38- // (这俩组合在 Terminal.app 下仍跟 Enter 等价,只能用 Alt/Option+Enter 换行)。
39- v .KeyboardEnhancements .ReportAlternateKeys = true
35+ // 换行键统一为 ctrl+j(LF,终端原生、不依赖 Kitty 协议、三平台一致,见 issue #124),
36+ // 不再绑定 Ctrl+Enter / Shift+Enter,故无需开 Kitty "alternate keys" 上报。
4037 return v
4138}
4239
@@ -45,9 +42,12 @@ func (m model) wrapView(content string) tea.View {
4542const inputTopPad = 2
4643const inputBotPad = 0
4744
48- // inputAreaHeight 是底部输入框占用的固定行数:textarea 3 行 + 上下留白。
49- // textarea 高 = inputAreaHeight - inputTopPad - inputBotPad。
50- const inputAreaHeight = 3 + inputTopPad + inputBotPad
45+ // inputTextRows 是输入框 textarea 的固定显示行数。内容超过时不长高,
46+ // 靠 ↑/↓ 移动光标带动 textarea 内部滚动(见 model.go 按键处理)。
47+ const inputTextRows = 3
48+
49+ // inputAreaHeight 是底部输入框区域占用的固定行数:textarea inputTextRows 行 + 上下留白。
50+ const inputAreaHeight = inputTextRows + inputTopPad + inputBotPad
5151
5252// inputGutterWidth 是输入区左侧固定 gutter 列宽:首行画 "❱ ",其余行 " "。
5353// textarea 实际宽度 = m.width - inputGutterWidth。
@@ -181,8 +181,8 @@ func (m model) View() tea.View {
181181 leftW = 1
182182 }
183183 // 排队区(流式中暂存的待发送消息)挂在输入框上方,占 queuedH 行,从 body 高度里扣。
184- // 别让它把对话挤没:至少给 chat 留 1 行。
185- queuedLines := m .queuedDisplayLines (m . width )
184+ // 别让它把对话挤没:至少给 chat 留 1 行。它在左列,按 leftW 折行。
185+ queuedLines := m .queuedDisplayLines (leftW )
186186 if maxQ := m .height - inputAreaHeight - 1 ; len (queuedLines ) > maxQ {
187187 if maxQ < 0 {
188188 maxQ = 0
@@ -208,34 +208,12 @@ func (m model) View() tea.View {
208208 chatLines = chatLines [len (chatLines )- bodyH :]
209209 }
210210
211- // 右栏:status section 区,固定 rightW × bodyH。隐藏时全空行(不渲染状态栏)。
212- rightLines := make ([]string , bodyH )
213- if ! m .hideStatusPanel {
214- right := lipgloss .NewStyle ().
215- Width (rightW ).
216- Height (bodyH ).
217- Padding (0 , 1 ).
218- Render (m .rightPanelView ())
219- rightLines = strings .Split (right , "\n " )
220- for len (rightLines ) < bodyH {
221- rightLines = append (rightLines , strings .Repeat (" " , rightW ))
222- }
223- if len (rightLines ) > bodyH {
224- rightLines = rightLines [:bodyH ]
225- }
226- }
227-
228- // 手动逐行拼接:chat_line + 分隔线/滚动条(scrollbarWidth 列)+ right_line。
229- divs := m .scrollbarDividers (bodyH )
230- bodyLines := make ([]string , bodyH )
231- for i := 0 ; i < bodyH ; i ++ {
232- bodyLines [i ] = chatLines [i ] + divs [i ] + rightLines [i ]
233- }
234- body := strings .Join (bodyLines , "\n " )
211+ // === 布局:左列(对话 + 输入)│ 分隔线 │ 右列(状态栏,全高)===
212+ // 状态栏独占右半区、从顶到底;分隔线一条 ┃ 贯穿全高,把左半区(对话+输入)与状态栏隔开。
213+ // 输入区因此收进左列(宽 leftW,见 resize / toggleStatusPanel 的 SetWidth)。
214+ // 分隔线始终贯穿全高;状态栏隐藏(rightW==0)时右列为空,线仍在最右列一直到底。
235215
236- // 输入区 = 左侧固定 gutter + 右侧 textarea,逐行拼接。
237- // gutter 首行 "> "(粉紫),其余行 " ";textarea 宽度已是 m.width-gutter。
238- // 这样多行粘贴 / 滚动时 "> " 始终钉在左上角,不会跟内容滚走。
216+ // 输入列内容:gutter + textarea 逐行拼接,首行 "❱ ";上接活动状态行/留白,中间夹排队区。
239217 taView := m .input .View ()
240218 taLines := strings .Split (taView , "\n " )
241219 if m .inputAllSelected {
@@ -257,27 +235,68 @@ func (m model) View() tea.View {
257235 }
258236 inputRows [i ] = gutter + tl
259237 }
260- // 输入区不画竖分隔线 —— 分隔线只到 body 底(对话+右栏区),输入区整宽。
261- // 顶部 / 底部按 inputTopPad / inputBotPad 留白,normalizeFrame 会把空行补成整宽。
262238 inputLines := make ([]string , 0 , queuedH + len (inputRows )+ inputTopPad + inputBotPad )
263239 for i := 0 ; i < inputTopPad ; i ++ {
264- // 顶部留白的第一行用来挂活动状态行 (运行中 spinner+耗时 / 空闲"就绪"),
265- // 其余仍是空行。 inputTopPad 不变, 光标 Y(bodyH+inputTopPad)也不变。
240+ // 顶部留白第一行挂活动状态行 (运行中 spinner+耗时 / 空闲"就绪"),其余空行。
241+ // inputTopPad 不变 → 光标 Y(bodyH+queuedH +inputTopPad)也不变。
266242 if i == 0 && inputTopPad > 0 {
267- inputLines = append (inputLines , m .statusFooterLine (m . width ))
243+ inputLines = append (inputLines , m .statusFooterLine (leftW ))
268244 continue
269245 }
270- inputLines = append (inputLines , "" ) // 顶部留白行
246+ inputLines = append (inputLines , "" )
271247 }
272- // 排队区放在活动状态行之后、输入框之前(紧贴输入框),让"待发送"和你正在打的字成组。
273- inputLines = append (inputLines , queuedLines ... )
248+ inputLines = append (inputLines , queuedLines ... ) // 排队区紧贴输入框上方
274249 inputLines = append (inputLines , inputRows ... )
275250 for i := 0 ; i < inputBotPad ; i ++ {
276- inputLines = append (inputLines , "" ) // 底部留白行
251+ inputLines = append (inputLines , "" )
277252 }
278- inputBlock := strings .Join (inputLines , "\n " )
279253
280- mainUI := lipgloss .JoinVertical (lipgloss .Left , body , inputBlock )
254+ // 左列 = 对话(bodyH 行)+ 输入列,逐行锁到精确 leftW(短补空格/长截断),
255+ // 保证分隔线在每行都落在同一列、不会参差。
256+ leftLines := make ([]string , 0 , len (chatLines )+ len (inputLines ))
257+ leftLines = append (leftLines , chatLines ... )
258+ leftLines = append (leftLines , inputLines ... )
259+ leftCol := strings .Split (padLinesToWidth (strings .Join (leftLines , "\n " ), leftW ), "\n " )
260+
261+ // 右列 = 状态栏,全高 rightW;隐藏时空。
262+ panelShown := ! m .hideStatusPanel && rightW > 0
263+ rightCol := make ([]string , m .height )
264+ if panelShown {
265+ right := lipgloss .NewStyle ().
266+ Width (rightW ).
267+ Height (m .height ).
268+ Padding (0 , 1 ).
269+ Render (m .rightPanelView ())
270+ rightCol = strings .Split (right , "\n " )
271+ for len (rightCol ) < m .height {
272+ rightCol = append (rightCol , strings .Repeat (" " , rightW ))
273+ }
274+ if len (rightCol ) > m .height {
275+ rightCol = rightCol [:m .height ]
276+ }
277+ }
278+
279+ // 分隔线始终贯穿全高:对话区那 bodyH 行是滚动条(可拖滑块,亮白滑块+暗轨道),
280+ // 其余行(输入区)是纯暗色 ┃ —— 状态栏显隐都一样,线一直到底。
281+ divs := m .scrollbarDividers (bodyH )
282+ track := scrollbarDividerStyle .Render ("┃" )
283+ rows := make ([]string , m .height )
284+ for i := 0 ; i < m .height ; i ++ {
285+ l := strings .Repeat (" " , leftW )
286+ if i < len (leftCol ) {
287+ l = leftCol [i ]
288+ }
289+ d := track
290+ if i < bodyH && i < len (divs ) {
291+ d = divs [i ]
292+ }
293+ r := ""
294+ if i < len (rightCol ) {
295+ r = rightCol [i ]
296+ }
297+ rows [i ] = l + d + r
298+ }
299+ mainUI := strings .Join (rows , "\n " )
281300
282301 // 复制成功提示:在鼠标松开的位置叠一个绿色"✓ 已复制"小标(copyHintClearMsg 到点清空)。
283302 if m .copyHint != "" {
0 commit comments