feat: improve terminal navigation and UI behavior

- refine tree and history navigation behavior
- improve scrolling continuity in the terminal UI
- keep AI worklog and prompt log updated
- validate changes with project quality checks
This commit is contained in:
2026-03-16 00:28:27 +01:00
parent 6195f7b8a3
commit 60723c33bc
4 changed files with 336 additions and 55 deletions

View File

@@ -76,12 +76,34 @@ def build_history_lines(
]
def clamp_scroll_offset(
selected_index: int, scroll_offset: int, visible_count: int, total_count: int
) -> int:
"""Keep the selected row within the visible scrolling window."""
if visible_count <= 0 or total_count <= visible_count:
return 0
max_offset = total_count - visible_count
offset = max(0, min(scroll_offset, max_offset))
if selected_index < offset:
return selected_index
if selected_index >= offset + visible_count:
return selected_index - visible_count + 1
return offset
class TerminalUI:
"""Minimal curses UI for the directory navigator MVP."""
def __init__(self) -> None:
self._history_mode = False
self._history_selected_index = 0
self._tree_scroll_offset = 0
self._history_scroll_offset = 0
def run(self, navigator: Navigator) -> Path:
"""Start the interactive session and return the final directory path."""
@@ -93,13 +115,14 @@ class TerminalUI:
def _run_session(self, stdscr: curses.window, navigator: Navigator) -> Path:
curses.curs_set(0)
stdscr.keypad(True)
original_path = navigator.current_path
while True:
self._render(stdscr, navigator)
key = stdscr.getch()
if key == ESCAPE_KEY:
return navigator.current_path
return original_path
if key == ord("h"):
self._toggle_history_mode(navigator)
@@ -109,44 +132,9 @@ class TerminalUI:
self._handle_history_key(key, navigator)
continue
if key == curses.KEY_UP:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=-1,
entry_count=len(navigator.visible_entries),
)
)
continue
if key == curses.KEY_DOWN:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=1,
entry_count=len(navigator.visible_entries),
)
)
continue
if key == curses.KEY_RIGHT:
navigator.expand_selected_directory()
continue
if key == curses.KEY_LEFT:
navigator.collapse_selected_directory()
continue
if key == ord("b"):
navigator.go_back()
continue
if key == ord("f"):
navigator.go_forward()
continue
if key in (curses.KEY_ENTER, 10, 13):
navigator.enter_selected_directory()
tree_result = self._handle_tree_key(key, navigator)
if tree_result is not None:
return tree_result
def _render(self, stdscr: curses.window, navigator: Navigator) -> None:
stdscr.erase()
@@ -160,7 +148,19 @@ class TerminalUI:
path_text = str(navigator.current_path)
stdscr.addnstr(0, 0, path_text, width - 1)
for row, line in enumerate(build_render_lines(navigator), start=2):
lines = build_render_lines(navigator)
visible_count = max(0, height - 2)
self._tree_scroll_offset = clamp_scroll_offset(
navigator.selected_index,
self._tree_scroll_offset,
visible_count,
len(lines),
)
for row, line in enumerate(
lines[self._tree_scroll_offset : self._tree_scroll_offset + visible_count],
start=2,
):
if row >= height:
break
@@ -176,10 +176,23 @@ class TerminalUI:
navigator.history.entries(), self._history_selected_index
)
start_row = 1
visible_count = max(0, height - start_row - 1)
self._history_scroll_offset = clamp_scroll_offset(
self._history_selected_index,
self._history_scroll_offset,
visible_count,
len(history_lines),
)
stdscr.addnstr(start_row, 0, "History:", width - 1, curses.A_BOLD)
for index, line in enumerate(history_lines, start=1):
for index, line in enumerate(
history_lines[
self._history_scroll_offset : self._history_scroll_offset
+ visible_count
],
start=1,
):
row = start_row + index
if row >= height:
break
@@ -191,6 +204,7 @@ class TerminalUI:
self._history_mode = not self._history_mode
if self._history_mode:
self._history_selected_index = self._get_current_history_index(navigator)
self._history_scroll_offset = 0
def _handle_history_key(self, key: int, navigator: Navigator) -> None:
history_entries = navigator.history.entries()
@@ -214,6 +228,64 @@ class TerminalUI:
if key in (curses.KEY_ENTER, 10, 13) and history_entries:
navigator.select_history_entry(self._history_selected_index)
self._history_mode = False
self._tree_scroll_offset = 0
def _handle_tree_key(self, key: int, navigator: Navigator) -> Path | None:
if key == curses.KEY_UP:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=-1,
entry_count=len(navigator.visible_entries),
)
)
return None
if key == curses.KEY_DOWN:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=1,
entry_count=len(navigator.visible_entries),
)
)
return None
if key == curses.KEY_RIGHT:
was_expanded = navigator.selected_entry.is_expanded
if not navigator.expand_selected_directory():
navigator.enter_selected_directory()
self._tree_scroll_offset = 0
return None
if was_expanded:
navigator.enter_selected_directory()
self._tree_scroll_offset = 0
return None
if key == curses.KEY_LEFT:
if not navigator.collapse_selected_directory():
navigator.go_to_parent_directory()
self._tree_scroll_offset = 0
return None
if key == ord("b"):
navigator.go_back()
self._tree_scroll_offset = 0
return None
if key == ord("f"):
navigator.go_forward()
self._tree_scroll_offset = 0
return None
if key in (curses.KEY_ENTER, 10, 13):
return navigator.selected_entry.path
return None
def _get_current_history_index(self, navigator: Navigator) -> int:
history_entries = navigator.history.entries()