diff --git a/docs/ai-prompts.md b/docs/ai-prompts.md new file mode 100644 index 0000000..6335451 --- /dev/null +++ b/docs/ai-prompts.md @@ -0,0 +1,18 @@ +# AI Prompts Log + +This file stores relevant prompts used during AI-assisted development. + +Only store prompts that: +- change architecture +- introduce new behavior +- modify important logic +- affect project structure +- debug complex issues + +Do not store internal chain-of-thought reasoning. +Store only the user prompt and a short result summary. + +## 2026-03-15 + +- prompt summary: Implement a dedicated history mode in `src/app/ui.py` so pressing `h` hides the tree, shows only history entries, supports Up/Down navigation, and uses Enter to select a history entry and return to the normal tree view. +- result summary: Added a dedicated history mode with its own selection state, removed the temporary overlay behavior, preserved existing `b`/`f` and normal tree navigation behavior, and added focused UI tests. diff --git a/docs/ai-worklog.md b/docs/ai-worklog.md new file mode 100644 index 0000000..94a986c --- /dev/null +++ b/docs/ai-worklog.md @@ -0,0 +1,24 @@ +# AI Worklog + +This file records important AI-assisted development actions. + +Each entry should include: +- date +- task type (plan/build) +- objective +- files inspected or modified +- key decisions +- validation commands executed +- result +- open issues (if any) + +Entries must be concise and chronological. + +## 2026-03-15 - build + +- objective: Replace the temporary history debug overlay with a dedicated history mode in the terminal UI. +- files modified: `src/app/ui.py`, `tests/test_ui.py` +- key decisions: Keep `b` and `f` unchanged, add separate history-mode selection state inside the UI, hide the tree while history mode is active, and use Enter to select a history entry and return to the tree view. +- validation: `make fix`, `make quality` +- result: History mode now shows only the history list, supports Up/Down selection, allows Enter to jump to a prior directory, and exits cleanly back to normal navigation. +- open issues: None. diff --git a/instructions-agent.md b/instructions-agent.md index 8900952..cfa0f42 100644 --- a/instructions-agent.md +++ b/instructions-agent.md @@ -59,3 +59,27 @@ Agent rules: 3. If the specification conflicts with a previous assumption, the specification takes precedence. 4. Keep implementation aligned with the MVP scope unless explicitly asked to extend it. 5. After code changes, run the appropriate validation workflow defined by this repository. + +## AI Worklog Policy + +This repository maintains an AI-assisted development log. + +Files: +- `docs/ai-worklog.md` +- `docs/ai-prompts.md` + +Rules: +1. After every significant `plan` or `build` task, append a concise worklog entry to `docs/ai-worklog.md`. +2. Each worklog entry must include: + - date + - task type (`plan` or `build`) + - short objective + - files inspected or modified + - key decisions + - validation commands run + - result + - unresolved issues if any +3. When a prompt meaningfully changes architecture, behavior, workflow, debugging direction, or project structure, append the prompt (or a concise cleaned version of it) to `docs/ai-prompts.md`. +4. Do not store private chain-of-thought or internal reasoning. +5. Store only concise, user-facing summaries of what was done. +6. Keep entries chronological and easy to scan. diff --git a/src/app/ui.py b/src/app/ui.py index 496ffae..d3c422f 100644 --- a/src/app/ui.py +++ b/src/app/ui.py @@ -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 diff --git a/tests/test_ui.py b/tests/test_ui.py index ae9f6d7..7195909 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -1,10 +1,14 @@ +import curses from pathlib import Path import pytest from app.navigator import Navigator, VisibleEntry from app.ui import ( + HistoryLine, RenderLine, + TerminalUI, + build_history_lines, build_render_lines, format_entry, move_selection, @@ -65,6 +69,71 @@ def test_build_render_lines_marks_selected_entry(tmp_path: Path) -> None: ] +def test_build_history_lines_marks_selected_history_entry(tmp_path: Path) -> None: + first = tmp_path / "first" + second = tmp_path / "second" + + assert build_history_lines((first, second), selected_index=1) == [ + HistoryLine(text=str(first), is_selected=False), + HistoryLine(text=str(second), is_selected=True), + ] + + +def test_toggle_history_mode_selects_current_history_entry(tmp_path: Path) -> None: + root = tmp_path / "root" + child = root / "child" + root.mkdir() + child.mkdir() + + navigator = Navigator(root) + navigator.set_selected_index(1) + navigator.enter_selected_directory() + ui = TerminalUI() + + ui._toggle_history_mode(navigator) + + assert ui._history_mode is True + assert ui._history_selected_index == 1 + + +def test_history_mode_enter_selects_entry_and_exits(tmp_path: Path) -> None: + root = tmp_path / "root" + first = root / "first" + second = root / "second" + root.mkdir() + first.mkdir() + second.mkdir() + + navigator = Navigator(root) + navigator.set_selected_index(1) + navigator.enter_selected_directory() + navigator.go_to_parent_directory() + navigator.set_selected_index(2) + navigator.enter_selected_directory() + + ui = TerminalUI() + ui._toggle_history_mode(navigator) + ui._handle_history_key(curses.KEY_UP, navigator) + ui._handle_history_key(10, navigator) + + assert ui._history_mode is False + assert navigator.current_path == root.resolve() + assert navigator.selected_index == 0 + + +def test_history_mode_toggle_off_keeps_current_directory(tmp_path: Path) -> None: + root = tmp_path / "root" + root.mkdir() + navigator = Navigator(root) + ui = TerminalUI() + + ui._toggle_history_mode(navigator) + ui._toggle_history_mode(navigator) + + assert ui._history_mode is False + assert navigator.current_path == root.resolve() + + def test_open_terminal_streams_raises_without_terminal( monkeypatch: pytest.MonkeyPatch, ) -> None: