Improve UI

This commit is contained in:
Dita Aji Pratama 2026-06-25 15:50:40 +07:00
parent 03221ca119
commit 4f4e686ca1
2 changed files with 69 additions and 44 deletions

View File

@ -44,7 +44,6 @@ def handle_key(app, stdscr, key):
# Cancel stream yang sedang berjalan # Cancel stream yang sedang berjalan
app.llm.cancel_requested = True app.llm.cancel_requested = True
log(app, "system", " Stream cancelled by user") log(app, "system", " Stream cancelled by user")
app.scroll = 999999
else: else:
app.running = False app.running = False
elif key == curses.KEY_PPAGE: elif key == curses.KEY_PPAGE:

View File

@ -36,7 +36,7 @@ def init_colors():
curses.init_pair(C_STATUS, curses.COLOR_BLACK, curses.COLOR_YELLOW) 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_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_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_SEP, curses.COLOR_MAGENTA, -1)
curses.init_pair(C_ERROR, curses.COLOR_RED, -1) curses.init_pair(C_ERROR, curses.COLOR_RED, -1)
curses.init_pair(C_INPUT_BORDER, curses.COLOR_BLUE, -1) curses.init_pair(C_INPUT_BORDER, curses.COLOR_BLUE, -1)
@ -57,27 +57,52 @@ def draw(app, stdscr):
def draw_header(app, stdscr): def draw_header(app, stdscr):
# Baris paling atas: " Hendrik AI Agent ─────── <model> " # Baris 1: " Hendrik AI Agent ─────── <model> "
w = app.w w = app.w
name = " Hendrik AI Agent " name = " Hendrik AI Agent "
model = f" {app.llm.model} " model = f" {app.llm.model} "
# Perbaiki kalkulasi agar line1 tepat mengisi w kolom
mid = w - len(model) - 1 mid = w - len(model) - 1
pad = max(1, mid - len(name) - 1) pad = max(1, mid - len(name) - 1)
line = name + "\u2500" * pad + " " + model line1 = name + "\u2500" * pad + " " + model
attr = curses.color_pair(C_HEADER) | curses.A_BOLD # Gunakan ljust(w) untuk memastikan background biru penuh sampai ujung kanan
stdscr.addstr(0, 0, line[:w], attr) 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): 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. # Bisa di-scroll dengan Page Up / Page Down.
# app.log berisi daftar item (role, text, time) untuk display. # app.log berisi daftar item (role, text, time) untuk display.
h, w = app.h, app.w h, w = app.h, app.w
chat_top = 1 chat_top = 2
chat_h = h - 10 chat_h = h - 11
if chat_h <= 0: if chat_h <= 0:
return 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. # Render log ke list of rows; setiap row = list of (color, text) segments.
# Ini memungkinkan satu baris punya multi-warna (misal label tool_call). # Ini memungkinkan satu baris punya multi-warna (misal label tool_call).
# Setiap baris di-wrap sesuai lebar terminal. # Setiap baris di-wrap sesuai lebar terminal.
@ -216,6 +241,7 @@ def draw_chat(app, stdscr):
# Clamp scroll agar tidak melebihi total baris # Clamp scroll agar tidak melebihi total baris
total = len(rendered) total = len(rendered)
max_scroll = max(0, total - chat_h) max_scroll = max(0, total - chat_h)
if app.scroll > max_scroll: if app.scroll > max_scroll:
app.scroll = max_scroll app.scroll = max_scroll
app.scroll = max(0, app.scroll) app.scroll = max(0, app.scroll)
@ -348,50 +374,50 @@ def draw_input(app, stdscr):
def draw_status(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 h, w = app.h, app.w
y = h - 9 y = h - 9
ws = os.getcwd() ws = os.getcwd()
session_tag = "" session_tag = ""
if app.current_session: if app.current_session:
sname = app.current_session.name session_tag = f" {app.current_session.name} "
if len(sname) > 20:
sname = sname[:17] + "..."
session_tag = f"[{sname}] "
mode = " PROCESSING " if app.processing else " READY " mode = " PROCESSING " if app.processing else " READY "
if app.processing:
hints = " ^N:new ^O:open ^R:rename ^E:model ^W:ws ^C:cancel " # Format: [MODE] workspace session (menggunakan spasi sebagai pemisah)
else: status_text = f"{mode} {ws} {session_tag}"
hints = " ^N:new ^O:open ^R:rename ^D:send ^E:model ^W:ws ^C:exit "
max_left = w - len(mode) - len(hints) - 4 # Jika terlalu panjang, potong bagian workspace-nya saja agar tetap readable
left = session_tag + ws ws_display = ws
if len(left) > max_left: if len(status_text) > w:
left = ".." + left[-(max_left - 2):] 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] # Gambar background dasar
stdscr.addstr(y, 0, status, curses.color_pair(C_STATUS_INFO)) full_status = status_text.ljust(w)[:w]
stdscr.addstr(y, 0, full_status, curses.color_pair(C_STATUS_INFO))
# Highlight mode dengan warna berbeda # Highlight mode dengan warna berbeda (Hijau/Kuning)
mode_start = len(f" {left} \u2502")
mode_end = mode_start + len(mode)
mode_attr = curses.color_pair(C_STATUS_READY) if not app.processing else curses.color_pair(C_STATUS_PROC) 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 # Highlight Workspace dan Session dengan warna Putih-Bold
x = mode_end + 2 highlight_attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD
hints_parts = [
("^N:new", True), try:
(" ^O:open", True), # Mode sudah digambar, kita cari posisi setelah mode
(" ^R:rename", True), ws_start = len(mode) + 1 # melewati mode + 1 spasi
(" ^D:send", not app.processing), ws_len = len(ws_display)
(" ^E:model", True),
(" ^W:ws", True), # Gambar Workspace
(" ^C:cancel", app.processing), stdscr.addstr(y, ws_start, ws_display, highlight_attr)
(" ^C:exit", not app.processing),
] # Gambar Session
for text, enabled in hints_parts: session_start = ws_start + ws_len + 1 # melewati workspace + 1 spasi
attr = curses.color_pair(C_STATUS_INFO) | curses.A_BOLD if enabled else curses.color_pair(C_HINT_DISABLED) if session_tag:
stdscr.addstr(y, x, text, attr) stdscr.addstr(y, session_start, session_tag, highlight_attr)
x += len(text) except curses.error:
pass