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:
18
docs/ai-prompts.md
Normal file
18
docs/ai-prompts.md
Normal 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
24
docs/ai-worklog.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user