diff --git a/interfaces/tui/agent.py b/interfaces/tui/agent.py index 3a063bf..da86ac0 100644 --- a/interfaces/tui/agent.py +++ b/interfaces/tui/agent.py @@ -100,7 +100,8 @@ def _agent_loop(app): for step in range(app.agent_max_iterations): stamp_step = ntro.start() log(app, "system", f" step {step + 1} \u2014 Thinking...") - app.scroll = 999999 + if app.scroll_follow: + app.scroll = 999999 # Streaming response stream_buffer = [] @@ -121,7 +122,8 @@ def _agent_loop(app): if app.log[i].get('role') == 'ai' and (app.log[i].get('text') == placeholder_marker or (i > 0 and app.log[i-1].get('role') == 'system' and 'Thinking' in app.log[i-1].get('text', ''))): app.log[i]['text'] = current_text break - app.scroll = 999999 + if app.scroll_follow: + app.scroll = 999999 skill = config.AGENT_SKILLS tool_reminder = None @@ -189,7 +191,8 @@ def _agent_loop(app): "name": tname, "arguments": targs, })) - app.scroll = 999999 + if app.scroll_follow: + app.scroll = 999999 result = agent_loop.execute_tool(tc, app.TOOL_HANDLERS) if isinstance(result, ImagePayload): _add_msg(app, "tool", [ diff --git a/interfaces/tui/app.py b/interfaces/tui/app.py index 58e0ebd..509f244 100644 --- a/interfaces/tui/app.py +++ b/interfaces/tui/app.py @@ -34,6 +34,7 @@ class HendrikTUI: self.input_line = 0 self.input_col = 0 self.scroll = 0 + self.scroll_follow = True # follow bottom saat streaming, mati kalau user scroll up self.processing = False self.running = True self.h, self.w = 0, 0 diff --git a/interfaces/tui/input.py b/interfaces/tui/input.py index 2e12f7f..8d3ed94 100644 --- a/interfaces/tui/input.py +++ b/interfaces/tui/input.py @@ -49,8 +49,10 @@ def handle_key(app, stdscr, key): # -- Scroll & resize -- if key == curses.KEY_PPAGE: app.scroll = max(0, app.scroll - (app.h - 10) // 2) + app.scroll_follow = False elif key == curses.KEY_NPAGE: app.scroll += (app.h - 10) // 2 + app.scroll_follow = True elif key == curses.KEY_RESIZE: pass diff --git a/interfaces/tui/keycodes.py b/interfaces/tui/keycodes.py index 631ea87..61ec6e3 100644 --- a/interfaces/tui/keycodes.py +++ b/interfaces/tui/keycodes.py @@ -94,6 +94,16 @@ _CSI_NAV = { "5~": KEY_PPAGE, "6~": KEY_NPAGE, } +# Versi numerik (kitty CSI-u / tilde dengan modifier eksplisit): +# \x1b[2~ / \x1b[2;1~ / \x1b[2u / \x1b[2;1u → INSERT, dst. +# Ini bentuk yang dikirim terminal ketika keyboard protocol (kitty/xterm +# modifyOtherKeys) aktif, yang sebelumnya jatuh jadi KEY_ESC. +_CSI_NAV_NUM = { + 2: KEY_INSERT, 3: KEY_DC, + 5: KEY_PPAGE, 6: KEY_NPAGE, + 7: KEY_HOME, 8: KEY_END, +} + # F1-F4 bentuk huruf (kitty protocol aktif): CSI P/Q/S = F1/F2/F4. # F3 (huruf R) dihapus dari spec (bentrok Cursor Position Report) → pakai [13~. _CSI_LETTER_F = { @@ -173,6 +183,11 @@ def _map_modified_key(code: int, mod, terminator: str) -> int | None: return _CSI_F_KEYS["13"] # F3 return 13 + # Navigasi numerik (Insert/Delete/PageUp/PageDown/Home/End) — terlepas + # dari terminator (u/~) dan modifier yang menyertainya. + if code in _CSI_NAV_NUM: + return _CSI_NAV_NUM[code] + # F1-F12 dengan/tanpa modifier if str(code) in _CSI_F_KEYS: return _CSI_F_KEYS[str(code)] diff --git a/interfaces/tui/render.py b/interfaces/tui/render.py index 51d3215..2b82940 100644 --- a/interfaces/tui/render.py +++ b/interfaces/tui/render.py @@ -214,6 +214,10 @@ def draw_chat(app, stdscr): if app.scroll > max_scroll: app.scroll = max_scroll app.scroll = max(0, app.scroll) + # Kalau sudah mentok di bawah, kembalikan auto-follow (streaming akan + # menempel di bottom lagi; PgUp keluar dari mode follow). + if app.scroll >= max_scroll: + app.scroll_follow = True # Gambar baris yang terlihat (scroll sampai scroll + chat_h) # Clear area chat dulu biar nggak ada text lama yang nyisa