feat: implement history mode and AI development logging
- add dedicated history mode UI (toggle with `h`) - allow navigation inside history with arrows - allow selecting history entries with Enter - add tests for history mode behavior - introduce AI development logs (`ai-worklog.md`, `ai-prompts.md`) - add AI worklog policy to `instructions-agent.md`
This commit is contained in:
@@ -22,6 +22,14 @@ class RenderLine:
|
||||
is_selected: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HistoryLine:
|
||||
"""A rendered history row ready for terminal output."""
|
||||
|
||||
text: str
|
||||
is_selected: bool
|
||||
|
||||
|
||||
def move_selection(current_index: int, step: int, entry_count: int) -> int:
|
||||
"""Move the selected index within the available entry range."""
|
||||
|
||||
@@ -57,11 +65,23 @@ def build_render_lines(navigator: Navigator) -> list[RenderLine]:
|
||||
]
|
||||
|
||||
|
||||
def build_history_lines(
|
||||
entries: tuple[Path, ...], selected_index: int
|
||||
) -> list[HistoryLine]:
|
||||
"""Build all visible lines for history mode."""
|
||||
|
||||
return [
|
||||
HistoryLine(text=str(path), is_selected=index == selected_index)
|
||||
for index, path in enumerate(entries)
|
||||
]
|
||||
|
||||
|
||||
class TerminalUI:
|
||||
"""Minimal curses UI for the directory navigator MVP."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._show_history_debug = False
|
||||
self._history_mode = False
|
||||
self._history_selected_index = 0
|
||||
|
||||
def run(self, navigator: Navigator) -> Path:
|
||||
"""Start the interactive session and return the final directory path."""
|
||||
@@ -81,6 +101,14 @@ class TerminalUI:
|
||||
if key == ESCAPE_KEY:
|
||||
return navigator.current_path
|
||||
|
||||
if key == ord("h"):
|
||||
self._toggle_history_mode(navigator)
|
||||
continue
|
||||
|
||||
if self._history_mode:
|
||||
self._handle_history_key(key, navigator)
|
||||
continue
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
navigator.set_selected_index(
|
||||
move_selection(
|
||||
@@ -117,10 +145,6 @@ class TerminalUI:
|
||||
navigator.go_forward()
|
||||
continue
|
||||
|
||||
if key == ord("h"):
|
||||
self._show_history_debug = not self._show_history_debug
|
||||
continue
|
||||
|
||||
if key in (curses.KEY_ENTER, 10, 13):
|
||||
navigator.enter_selected_directory()
|
||||
|
||||
@@ -128,6 +152,11 @@ class TerminalUI:
|
||||
stdscr.erase()
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
if self._history_mode:
|
||||
self._render_history_mode(stdscr, navigator, width=width, height=height)
|
||||
stdscr.refresh()
|
||||
return
|
||||
|
||||
path_text = str(navigator.current_path)
|
||||
stdscr.addnstr(0, 0, path_text, width - 1)
|
||||
|
||||
@@ -138,25 +167,62 @@ class TerminalUI:
|
||||
attributes = curses.A_REVERSE if line.is_selected else curses.A_NORMAL
|
||||
stdscr.addnstr(row, 0, line.text, width - 1, attributes)
|
||||
|
||||
if self._show_history_debug:
|
||||
self._render_history_debug(stdscr, navigator, width=width, height=height)
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
def _render_history_debug(
|
||||
def _render_history_mode(
|
||||
self, stdscr: curses.window, navigator: Navigator, width: int, height: int
|
||||
) -> None:
|
||||
history_entries = navigator.history.entries()
|
||||
history_lines = build_history_lines(
|
||||
navigator.history.entries(), self._history_selected_index
|
||||
)
|
||||
start_row = 1
|
||||
|
||||
stdscr.addnstr(start_row, 0, "History:", width - 1, curses.A_BOLD)
|
||||
|
||||
for index, path in enumerate(history_entries, start=1):
|
||||
for index, line in enumerate(history_lines, start=1):
|
||||
row = start_row + index
|
||||
if row >= height:
|
||||
break
|
||||
|
||||
stdscr.addnstr(row, 0, str(path), width - 1)
|
||||
attributes = curses.A_REVERSE if line.is_selected else curses.A_NORMAL
|
||||
stdscr.addnstr(row, 0, line.text, width - 1, attributes)
|
||||
|
||||
def _toggle_history_mode(self, navigator: Navigator) -> None:
|
||||
self._history_mode = not self._history_mode
|
||||
if self._history_mode:
|
||||
self._history_selected_index = self._get_current_history_index(navigator)
|
||||
|
||||
def _handle_history_key(self, key: int, navigator: Navigator) -> None:
|
||||
history_entries = navigator.history.entries()
|
||||
|
||||
if key == curses.KEY_UP:
|
||||
self._history_selected_index = move_selection(
|
||||
self._history_selected_index,
|
||||
step=-1,
|
||||
entry_count=len(history_entries),
|
||||
)
|
||||
return
|
||||
|
||||
if key == curses.KEY_DOWN:
|
||||
self._history_selected_index = move_selection(
|
||||
self._history_selected_index,
|
||||
step=1,
|
||||
entry_count=len(history_entries),
|
||||
)
|
||||
return
|
||||
|
||||
if key in (curses.KEY_ENTER, 10, 13) and history_entries:
|
||||
navigator.select_history_entry(self._history_selected_index)
|
||||
self._history_mode = False
|
||||
|
||||
def _get_current_history_index(self, navigator: Navigator) -> int:
|
||||
history_entries = navigator.history.entries()
|
||||
|
||||
for index in range(len(history_entries) - 1, -1, -1):
|
||||
if history_entries[index] == navigator.current_path:
|
||||
return index
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
||||
Reference in New Issue
Block a user