From 399850eb3b3c70cdeec9d13a5d2aa4d7ed514749 Mon Sep 17 00:00:00 2001 From: Dita Aji Pratama Date: Tue, 9 Jun 2026 11:38:50 +0700 Subject: [PATCH] Improving tool calling information --- tui/agent.py | 6 ++- tui/render.py | 110 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/tui/agent.py b/tui/agent.py index 4b6e918..a037ef6 100644 --- a/tui/agent.py +++ b/tui/agent.py @@ -74,7 +74,11 @@ def _agent_loop(app): app.scroll = 999999 for tc in response.tool_calls: tname = tc["function"]["name"] - log(app, "system", f" \u2192 {tname}") + targs = tc["function"]["arguments"] + log(app, "tool_call", json.dumps({ + "name": tname, + "arguments": targs, + })) app.scroll = 999999 execute_tool(app, tc) else: diff --git a/tui/render.py b/tui/render.py index 0c63923..a518713 100644 --- a/tui/render.py +++ b/tui/render.py @@ -3,6 +3,7 @@ # lalu membaca state dari `app` untuk menggambar di layar. import curses +import json import os import textwrap @@ -21,6 +22,8 @@ C_INPUT_BORDER = 9 # border input box: biru C_STATUS_INFO = 12 # status info (workspace/hints): putih C_HINT_DISABLED = 13 # hint disabled (abu-abu) C_WELCOME = 14 # welcome art: light blue +C_TOOL_CALL = 15 # tool call: kuning terang +C_TOOL_RESULT = 16 # tool result: magenta muda def init_colors(): @@ -40,6 +43,8 @@ def init_colors(): curses.init_pair(C_INPUT_BORDER, curses.COLOR_BLUE, -1) curses.init_pair(C_HINT_DISABLED, 8, -1) # abu-abu di atas bg default curses.init_pair(C_WELCOME, curses.COLOR_BLUE + 8, -1) # light blue + curses.init_pair(C_TOOL_CALL, curses.COLOR_YELLOW + 8, -1) # bright yellow + curses.init_pair(C_TOOL_RESULT, curses.COLOR_MAGENTA + 8, -1) # bright magenta def draw(app, stdscr): @@ -74,67 +79,113 @@ def draw_chat(app, stdscr): if chat_h <= 0: return - # Render log ke list of (color, text) agar scroll calculation akurat - # Setiap baris di-wrap sesuai lebar terminal - rendered = [] + # 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. + rendered = [] # list of list of (color, text) + + def _add_row(segments): + # segments: list of (color, text) + rendered.append(segments) + + def _add_blank(): + rendered.append([(None, "")]) def _wrap_render(text, indent=0, color=C_INPUT): available = w - indent - 1 # sisakan 1 kolom margin kanan if available <= 0: - rendered.append((color, " " * indent)) + _add_row([(color, " " * indent)]) return for line in text.split("\n"): if not line: - rendered.append((color, " " * indent)) + _add_row([(color, " " * indent)]) continue start = 0 while start < len(line): chunk = line[start:start + available] - rendered.append((color, " " * indent + chunk)) + _add_row([(color, " " * indent + chunk)]) start += available for idx, item in enumerate(app.log): role, text = item["role"], item["text"] if role == "sep": - rendered.append((None, "")) - rendered.append((None, "")) + _add_blank() + _add_blank() continue # Tambah blank line sebelum system log setelah user/ai response if role == "system" and idx > 0 and app.log[idx - 1]["role"] in ("user", "ai"): - rendered.append((None, "")) + _add_blank() # Tambah blank line sebelum ai response setelah user (langsung, tanpa tools) if role == "ai" and idx > 0 and app.log[idx - 1]["role"] in ("user", "ai"): - rendered.append((None, "")) + _add_blank() # Tambah blank line sebelum ai response setelah system log if role == "ai" and idx > 0 and app.log[idx - 1]["role"] == "system": - rendered.append((None, "")) + _add_blank() if role == "user": label = f" You ({item['time']}) " - rendered.append((C_USER, label)) + _add_row([(C_USER, label)]) _wrap_render(text, indent=1, color=C_INPUT) elif role == "ai": label = f" Hendrik ({item['time']}) " - rendered.append((C_AI, label)) + _add_row([(C_AI, label)]) _wrap_render(text, indent=1, color=C_INPUT) elif role == "system": lines = text.split("\n") - rendered.append((C_SYSTEM, lines[0])) + _add_row([(C_SYSTEM, lines[0])]) for line in lines[1:]: - rendered.append((C_SYSTEM, " " + line)) + _add_row([(C_SYSTEM, " " + line)]) elif role == "welcome": lines = text.split("\n") for line in lines: - rendered.append((C_WELCOME, " " + line)) + _add_row([(C_WELCOME, " " + line)]) + elif role == "tool_call": + # Format: + # (blank line) + # Hendrik run_bash (HH:MM) ← "Hendrik" hijau, "run_bash (HH:MM)" kuning + # { ← arguments indent 1 spasi, kuning + # "command": "ls -la" + # } + # (blank line) + # Blank line di atas (kecuali sebelumnya sudah blank dari role lain) + if idx > 0 and app.log[idx - 1]["role"] not in ("sep", "welcome"): + _add_blank() + try: + tc = json.loads(text) + tname = tc["name"] + targs_raw = tc["arguments"] + # Pretty-print arguments + try: + targs = json.loads(targs_raw) if isinstance(targs_raw, str) else targs_raw + args_str = json.dumps(targs, indent=2, ensure_ascii=False) + except Exception: + args_str = str(targs_raw) + + # Label: "Hendrik" hijau + "tool_name" kuning + "(HH:MM)" hijau + _add_row([ + (C_AI, " Hendrik "), + (C_TOOL_CALL, tname), + (C_AI, f" ({item['time']}) "), + ]) + for aline in args_str.split("\n"): + _add_row([(C_INPUT, " " + aline)]) + except Exception: + _add_row([ + (C_AI, " Hendrik "), + (C_TOOL_CALL, "unknown"), + (C_AI, f" ({item['time']}) "), + ]) + # Blank line di bawah + _add_blank() elif role == "error": label = " \u2717 " lines = text.split("\n") - rendered.append((C_ERROR, label + (lines[0] if lines else ""))) + _add_row([(C_ERROR, label + (lines[0] if lines else ""))]) for line in lines[1:]: - rendered.append((C_ERROR, " " + line)) + _add_row([(C_ERROR, " " + line)]) # Clamp scroll agar tidak melebihi total baris total = len(rendered) @@ -153,14 +204,21 @@ def draw_chat(app, stdscr): y = chat_top for i in range(app.scroll, min(app.scroll + chat_h, total)): - color, text = rendered[i] - attr = curses.color_pair(color) | curses.A_BOLD if color else curses.A_NORMAL - if len(text) > w: - text = text[:w] - try: - stdscr.addstr(y, 0, text, attr) - except curses.error: - pass + segments = rendered[i] + x = 0 + for color, text in segments: + if not text: + continue + attr = curses.color_pair(color) | curses.A_BOLD if color else curses.A_NORMAL + remaining = w - x + if remaining <= 0: + break + display = text[:remaining] + try: + stdscr.addstr(y, x, display, attr) + except curses.error: + pass + x += len(display) y += 1