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.
|
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.
|
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.
|
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
|
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:
|
def move_selection(current_index: int, step: int, entry_count: int) -> int:
|
||||||
"""Move the selected index within the available entry range."""
|
"""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:
|
class TerminalUI:
|
||||||
"""Minimal curses UI for the directory navigator MVP."""
|
"""Minimal curses UI for the directory navigator MVP."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._show_history_debug = False
|
self._history_mode = False
|
||||||
|
self._history_selected_index = 0
|
||||||
|
|
||||||
def run(self, navigator: Navigator) -> Path:
|
def run(self, navigator: Navigator) -> Path:
|
||||||
"""Start the interactive session and return the final directory path."""
|
"""Start the interactive session and return the final directory path."""
|
||||||
@@ -81,6 +101,14 @@ class TerminalUI:
|
|||||||
if key == ESCAPE_KEY:
|
if key == ESCAPE_KEY:
|
||||||
return navigator.current_path
|
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:
|
if key == curses.KEY_UP:
|
||||||
navigator.set_selected_index(
|
navigator.set_selected_index(
|
||||||
move_selection(
|
move_selection(
|
||||||
@@ -117,10 +145,6 @@ class TerminalUI:
|
|||||||
navigator.go_forward()
|
navigator.go_forward()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if key == ord("h"):
|
|
||||||
self._show_history_debug = not self._show_history_debug
|
|
||||||
continue
|
|
||||||
|
|
||||||
if key in (curses.KEY_ENTER, 10, 13):
|
if key in (curses.KEY_ENTER, 10, 13):
|
||||||
navigator.enter_selected_directory()
|
navigator.enter_selected_directory()
|
||||||
|
|
||||||
@@ -128,6 +152,11 @@ class TerminalUI:
|
|||||||
stdscr.erase()
|
stdscr.erase()
|
||||||
height, width = stdscr.getmaxyx()
|
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)
|
path_text = str(navigator.current_path)
|
||||||
stdscr.addnstr(0, 0, path_text, width - 1)
|
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
|
attributes = curses.A_REVERSE if line.is_selected else curses.A_NORMAL
|
||||||
stdscr.addnstr(row, 0, line.text, width - 1, attributes)
|
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()
|
stdscr.refresh()
|
||||||
|
|
||||||
def _render_history_debug(
|
def _render_history_mode(
|
||||||
self, stdscr: curses.window, navigator: Navigator, width: int, height: int
|
self, stdscr: curses.window, navigator: Navigator, width: int, height: int
|
||||||
) -> None:
|
) -> None:
|
||||||
history_entries = navigator.history.entries()
|
history_lines = build_history_lines(
|
||||||
|
navigator.history.entries(), self._history_selected_index
|
||||||
|
)
|
||||||
start_row = 1
|
start_row = 1
|
||||||
|
|
||||||
stdscr.addnstr(start_row, 0, "History:", width - 1, curses.A_BOLD)
|
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
|
row = start_row + index
|
||||||
if row >= height:
|
if row >= height:
|
||||||
break
|
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
|
@contextmanager
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import curses
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.navigator import Navigator, VisibleEntry
|
from app.navigator import Navigator, VisibleEntry
|
||||||
from app.ui import (
|
from app.ui import (
|
||||||
|
HistoryLine,
|
||||||
RenderLine,
|
RenderLine,
|
||||||
|
TerminalUI,
|
||||||
|
build_history_lines,
|
||||||
build_render_lines,
|
build_render_lines,
|
||||||
format_entry,
|
format_entry,
|
||||||
move_selection,
|
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(
|
def test_open_terminal_streams_raises_without_terminal(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user