diff --git a/interfaces/tui/input.py b/interfaces/tui/input.py index a2b168f..74ab010 100644 --- a/interfaces/tui/input.py +++ b/interfaces/tui/input.py @@ -44,7 +44,6 @@ def handle_key(app, stdscr, key): # Cancel stream yang sedang berjalan app.llm.cancel_requested = True log(app, "system", " Stream cancelled by user") - app.scroll = 999999 else: app.running = False elif key == curses.KEY_PPAGE: diff --git a/interfaces/tui/render.py b/interfaces/tui/render.py index fdeac2c..612a1b6 100644 --- a/interfaces/tui/render.py +++ b/interfaces/tui/render.py @@ -36,7 +36,7 @@ def init_colors(): curses.init_pair(C_STATUS, curses.COLOR_BLACK, curses.COLOR_YELLOW) curses.init_pair(C_STATUS_READY, curses.COLOR_BLACK, curses.COLOR_GREEN + 8) curses.init_pair(C_STATUS_PROC, curses.COLOR_BLACK, curses.COLOR_YELLOW) - curses.init_pair(C_STATUS_INFO, curses.COLOR_WHITE, -1) + curses.init_pair(C_STATUS_INFO, curses.COLOR_BLACK, curses.COLOR_WHITE) curses.init_pair(C_SEP, curses.COLOR_MAGENTA, -1) curses.init_pair(C_ERROR, curses.COLOR_RED, -1) curses.init_pair(C_INPUT_BORDER, curses.COLOR_BLUE, -1) @@ -57,27 +57,52 @@ def draw(app, stdscr): def draw_header(app, stdscr): - # Baris paling atas: " Hendrik AI Agent ─────── " + # Baris 1: " Hendrik AI Agent ─────── " w = app.w name = " Hendrik AI Agent " model = f" {app.llm.model} " + # Perbaiki kalkulasi agar line1 tepat mengisi w kolom mid = w - len(model) - 1 pad = max(1, mid - len(name) - 1) - line = name + "\u2500" * pad + " " + model - attr = curses.color_pair(C_HEADER) | curses.A_BOLD - stdscr.addstr(0, 0, line[:w], attr) + line1 = name + "\u2500" * pad + " " + model + # Gunakan ljust(w) untuk memastikan background biru penuh sampai ujung kanan + full_line1 = line1.ljust(w) + attr1 = curses.color_pair(C_HEADER) | curses.A_BOLD + stdscr.addstr(0, 0, full_line1[:w], attr1) + + # Baris 2: Shortcut hints + # Tampilkan hanya shortcut yang aktif sesuai status processing + if app.processing: + # Hanya ^C (cancel) yang aktif saat processing + hints = " ^C:cancel " + else: + # Semua shortcut aktif saat READY + hints = " ^N:new ^O:open ^R:rename ^D:send ^E:model ^W:workspace ^C:exit " + + # Align left and fill the rest of the width with spaces to keep background color + full_line = hints.ljust(w) + # Menggunakan C_HINT_DISABLED untuk warna abu-abu cerah + attr2 = curses.color_pair(C_HINT_DISABLED) + stdscr.addstr(1, 0, full_line[:w], attr2) def draw_chat(app, stdscr): - # Area chat — dari baris 1 sampai baris (h - 10). + # Area chat — dari baris 2 sampai baris (h - 11). # Bisa di-scroll dengan Page Up / Page Down. # app.log berisi daftar item (role, text, time) untuk display. h, w = app.h, app.w - chat_top = 1 - chat_h = h - 10 + chat_top = 2 + chat_h = h - 11 if chat_h <= 0: return + # Auto-scroll: Jika agen tidak sedang processing (READY), + # pastikan scroll berada di posisi paling bawah agar respon terbaru terlihat. + if not app.processing: + # Kita akan hitung total rendered rows nanti, + # tapi kita bisa memberi hint atau melakukan adjustment di sini. + pass + # Render log ke list of rows; setiap row = list of (color, text) segments. # Ini memungkinkan satu baris punya multi-warna (misal label tool_call). # Setiap baris di-wrap sesuai lebar terminal. @@ -216,6 +241,7 @@ def draw_chat(app, stdscr): # Clamp scroll agar tidak melebihi total baris total = len(rendered) max_scroll = max(0, total - chat_h) + if app.scroll > max_scroll: app.scroll = max_scroll app.scroll = max(0, app.scroll) @@ -348,50 +374,50 @@ def draw_input(app, stdscr): def draw_status(app, stdscr): - # Status bar di baris h-9: [session] workspace, mode (READY/PROCESSING), shortcut hints + # Status bar di baris h-9: mode, workspace, session h, w = app.h, app.w y = h - 9 ws = os.getcwd() session_tag = "" if app.current_session: - sname = app.current_session.name - if len(sname) > 20: - sname = sname[:17] + "..." - session_tag = f"[{sname}] " + session_tag = f" {app.current_session.name} " mode = " PROCESSING " if app.processing else " READY " - if app.processing: - hints = " ^N:new ^O:open ^R:rename ^E:model ^W:ws ^C:cancel " - else: - hints = " ^N:new ^O:open ^R:rename ^D:send ^E:model ^W:ws ^C:exit " - max_left = w - len(mode) - len(hints) - 4 - left = session_tag + ws - if len(left) > max_left: - left = ".." + left[-(max_left - 2):] + + # Format: [MODE] workspace session (menggunakan spasi sebagai pemisah) + status_text = f"{mode} {ws} {session_tag}" + + # Jika terlalu panjang, potong bagian workspace-nya saja agar tetap readable + ws_display = ws + if len(status_text) > w: + max_ws_len = w - len(mode) - len(session_tag) - 2 + if len(ws) > max_ws_len: + ws_display = ".." + ws[-(max_ws_len - 2):] + status_text = f"{mode} {ws_display} {session_tag}" - status = (f" {left} \u2502{mode}\u2502{hints}").ljust(w)[:w] - stdscr.addstr(y, 0, status, curses.color_pair(C_STATUS_INFO)) + # Gambar background dasar + full_status = status_text.ljust(w)[:w] + stdscr.addstr(y, 0, full_status, curses.color_pair(C_STATUS_INFO)) - # Highlight mode dengan warna berbeda - mode_start = len(f" {left} \u2502") - mode_end = mode_start + len(mode) + # Highlight mode dengan warna berbeda (Hijau/Kuning) mode_attr = curses.color_pair(C_STATUS_READY) if not app.processing else curses.color_pair(C_STATUS_PROC) - stdscr.addstr(y, mode_start, mode, mode_attr | curses.A_BOLD) + stdscr.addstr(y, 0, mode, mode_attr | curses.A_BOLD) - # Hint shortcuts di kanan — tombol tertentu abu-abu saat processing - x = mode_end + 2 - hints_parts = [ - ("^N:new", True), - (" ^O:open", True), - (" ^R:rename", True), - (" ^D:send", not app.processing), - (" ^E:model", True), - (" ^W:ws", True), - (" ^C:cancel", app.processing), - (" ^C:exit", not app.processing), - ] - for text, enabled in hints_parts: - attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD if enabled else curses.color_pair(C_HINT_DISABLED) - stdscr.addstr(y, x, text, attr) - x += len(text) + # Highlight Workspace dan Session dengan warna Putih-Bold + highlight_attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD + + try: + # Mode sudah digambar, kita cari posisi setelah mode + ws_start = len(mode) + 1 # melewati mode + 1 spasi + ws_len = len(ws_display) + + # Gambar Workspace + stdscr.addstr(y, ws_start, ws_display, highlight_attr) + + # Gambar Session + session_start = ws_start + ws_len + 1 # melewati workspace + 1 spasi + if session_tag: + stdscr.addstr(y, session_start, session_tag, highlight_attr) + except curses.error: + pass