Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 91 additions & 8 deletions code_agent/utils/cli/textual_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def override(func):
from textual import events
from textual.app import App, ComposeResult
from textual.containers import VerticalScroll
from textual.widgets import Input, Static
from textual.message import Message
from textual.widgets import Static, TextArea

from code_agent.agent.agent_basics import AgentExecution, AgentStep, AgentStepState
from code_agent.session.commands import SLASH_COMMANDS
Expand Down Expand Up @@ -948,8 +949,92 @@ def __rich_console__(self, console, options):

# ── OpenCookApp ─────────────────────────────────────────────────────────────

class Input(TextArea):
class Changed(Message):
def __init__(self, input: "Input") -> None:
super().__init__()
self.input = input
raw = input.text
self.value = raw if "\n" not in raw else ""

@property
def control(self) -> "Input":
return self.input

class Submitted(Message):
def __init__(self, input: "Input", value: str) -> None:
super().__init__()
self.input = input
self.value = value

@property
def control(self) -> "Input":
return self.input

@property
def value(self) -> str:
return self.text

@value.setter
def value(self, value: str) -> None:
self.text = str(value or "")

@property
def cursor_position(self) -> int:
row, col = self.cursor_location
offset = 0
for r in range(max(0, min(row, self.document.line_count))):
offset += len(self.document.get_line(r)) + 1
return offset + col

@cursor_position.setter
def cursor_position(self, position: int) -> None:
target = max(0, int(position))
remaining = target
line_count = max(1, self.document.line_count)
for row in range(line_count):
line = self.document.get_line(row)
if remaining <= len(line):
self.move_cursor((row, remaining))
return
remaining -= len(line) + 1
last_row = line_count - 1
self.move_cursor((last_row, len(self.document.get_line(last_row))))

def _on_key(self, event: events.Key) -> None:
tui = getattr(self.app, "_tui_console", None)
approval_mode = bool(getattr(tui, "_approval_mode", False))
deny_reason_mode = bool(getattr(tui, "_deny_reason_mode", False))

if event.key == "ctrl+c":
return

if event.key == "tab":
return

if (not approval_mode) and event.key == "ctrl+j":
event.stop()
event.prevent_default()
self.insert("\n")
return

if event.key == "enter":
event.stop()
event.prevent_default()
self.post_message(self.Submitted(self, self.text))
return

if event.key in ("up", "down"):
if approval_mode and not deny_reason_mode:
return
if (not approval_mode) and self.document.line_count <= 1:
return

super()._on_key(event)


class OpenCookApp(App[None]):

"""Full-screen Textual TUI with OpenCook brand theme.

Layout: header | VerticalScroll(ChatLog + status + cmd_popup + Input) | footer
Expand Down Expand Up @@ -1909,11 +1994,9 @@ async def begin_turn(self, user_input: str) -> None:
self._interrupt_time = 0.0

# User message with orange background highlight
short = user_input[:200]
self._write(Text(" ")) # spacer
chip = Text()
chip.append(f" {short} ", style=f"bold {_SURFACE} on {_ORANGE}")
self._write(chip, plain=f"\n> {short[:78]}")
chip = Text(user_input, style=f"bold {_SURFACE} on {_ORANGE}", no_wrap=False, overflow="fold")
self._write(chip, plain=f"\n> {user_input[:78]}")
self._write(Text(" ")) # spacer

self._spinning = True
Expand Down Expand Up @@ -2163,7 +2246,7 @@ def _update_footer(self) -> None:
parts.append((f"in {t.input_tokens} / out {t.output_tokens}", _DIM))

if width >= 66:
parts.append(("Ctrl+C interrupt o open report /help", _DIM))
parts.append(("Ctrl+C interrupt Ctrl+J newline o open report /help", _DIM))

text = Text(" ")
for idx, (part_text, part_style) in enumerate(parts):
Expand Down Expand Up @@ -2854,9 +2937,9 @@ def _build_welcome_hero(self, frame: int) -> Panel:
body = Group(left, Text(" "), live_session_title)

if width >= 90:
subtitle = Text(" Ctrl+C interrupt . o open report . /help commands ", style=_DIM)
subtitle = Text(" Ctrl+C interrupt . Ctrl+J newline . o open report . /help ", style=_DIM)
elif width >= 66:
subtitle = Text(" Ctrl+C . o open report . /help ", style=_DIM)
subtitle = Text(" Ctrl+C . Ctrl+J newline . /help ", style=_DIM)
elif width >= 30:
subtitle = Text(" Ctrl+C . o . /help ", style=_DIM)
else:
Expand Down
8 changes: 4 additions & 4 deletions code_agent/utils/cli/textual_console.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,19 @@ ChatLog {
display: block;
}

Input {
height: 3;
Input, TextArea {
height: 5;
border: ascii #1a2438;
background: #0a1020;
color: #e2e8f0;
padding: 0 1;
}

Input:focus {
Input:focus, TextArea:focus {
border: ascii #00D4FF;
}

Input.deny-mode {
Input.deny-mode, TextArea.deny-mode {
border: ascii #EF4444;
}

Expand Down