diff --git a/internal/cli/chat_tui.go b/internal/cli/chat_tui.go index 2b058f450..0f27a66c8 100644 --- a/internal/cli/chat_tui.go +++ b/internal/cli/chat_tui.go @@ -806,6 +806,12 @@ func (m chatTUI) update(msg tea.Msg) (tea.Model, tea.Cmd) { case "pgdown": m.viewport.PageDown() return m, finalize(m, cmds) + case "ctrl+home": + m.viewport.GotoTop() + return m, finalize(m, cmds) + case "ctrl+end": + m.viewport.GotoBottom() + return m, finalize(m, cmds) case "ctrl+z": return m, tea.Suspend } diff --git a/internal/cli/chat_tui_test.go b/internal/cli/chat_tui_test.go index 1cc39170c..44c0bcb2c 100644 --- a/internal/cli/chat_tui_test.go +++ b/internal/cli/chat_tui_test.go @@ -582,6 +582,37 @@ func TestInsertNewlineKeyBinding(t *testing.T) { } } +func TestCtrlHomeEndScrollKeyBindings(t *testing.T) { + ctrl := control.New(control.Options{}) + ch := make(chan event.Event, 1) + notice := agentEventMsg(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "line"}) + adv := func(m chatTUI, msg tea.Msg) chatTUI { + n, _ := m.Update(msg) + return n.(chatTUI) + } + + cur := adv(newChatTUI(ctrl, "", ch, 80), tea.WindowSizeMsg{Width: 80, Height: 8}) + for i := 0; i < 12; i++ { + cur = adv(cur, notice) + } + // Viewport should be at the bottom after output. + if !cur.viewport.AtBottom() { + t.Fatal("viewport should start at the bottom after streaming output") + } + + // Ctrl+Home should scroll to the top. + cur = adv(cur, tea.KeyPressMsg{Code: tea.KeyHome, Mod: tea.ModCtrl}) + if !cur.viewport.AtTop() { + t.Fatalf("ctrl+home should scroll to top, AtTop=%v, YOffset=%d", cur.viewport.AtTop(), cur.viewport.YOffset()) + } + + // Ctrl+End should scroll back to the bottom. + cur = adv(cur, tea.KeyPressMsg{Code: tea.KeyEnd, Mod: tea.ModCtrl}) + if !cur.viewport.AtBottom() { + t.Fatalf("ctrl+end should scroll to bottom, AtBottom=%v, YOffset=%d", cur.viewport.AtBottom(), cur.viewport.YOffset()) + } +} + func TestEchoLocalCommandAddsTranscriptMarker(t *testing.T) { m := newTestChatTUI() m.echoLocalCommand(" /tree ") diff --git a/internal/i18n/messages_en.go b/internal/i18n/messages_en.go index aeda40ffc..524ad7548 100644 --- a/internal/i18n/messages_en.go +++ b/internal/i18n/messages_en.go @@ -48,7 +48,7 @@ var English = Messages{ ChatStatusCycleHint: "shift+tab to cycle", ChatStatusCacheNowFmt: "turn hit %s", ChatStatusCacheAvgFmt: "avg %s", - ChatStatusPlanApproval: "Enter/y approves & executes · n/Esc keeps planning · PgUp/PgDn scrolls", + ChatStatusPlanApproval: "Enter/y approves & executes · n/Esc keeps planning · PgUp/PgDn/Ctrl+Home/End scrolls", PlanApprovalPrompt: "Plan ready above — Enter/y to approve & execute, n/Esc to keep planning", ChatStatusToolApproval: "1 approve once · 2 allow scope this session · 3/4 prefix or save when offered · n/Esc deny · Ctrl-C cancels turn", AskTypeSomething: "Type something else", diff --git a/internal/i18n/messages_zh.go b/internal/i18n/messages_zh.go index 06ce87cc6..026e66e76 100644 --- a/internal/i18n/messages_zh.go +++ b/internal/i18n/messages_zh.go @@ -49,7 +49,7 @@ var Chinese = Messages{ ChatStatusCycleHint: "shift+tab 循环切换", ChatStatusCacheNowFmt: "本次命中 %s", ChatStatusCacheAvgFmt: "平均 %s", - ChatStatusPlanApproval: "Enter/y 批准并执行 · n/Esc 继续规划 · PgUp/PgDn 滚动", + ChatStatusPlanApproval: "Enter/y 批准并执行 · n/Esc 继续规划 · PgUp/PgDn/Ctrl+Home/End 滚动", PlanApprovalPrompt: "计划已生成(见上方)— Enter/y 批准执行,n/Esc 继续规划", ChatStatusToolApproval: "1 本次允许 · 2 本会话允许此范围 · 提供时 3/4 为前缀或保存 · n/Esc 拒绝 · Ctrl-C 取消本轮", AskTypeSomething: "自己输入",