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:
2026-03-15 17:46:11 +01:00
parent 2bdfbe25d5
commit 65508d263f
5 changed files with 213 additions and 12 deletions

18
docs/ai-prompts.md Normal file
View File

@@ -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.

24
docs/ai-worklog.md Normal file
View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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: