7 Commits

Author SHA1 Message Date
7c87d6f1f0 docs: require timestamp in AI log entries
- enforce YYYY-MM-DD HH:MM format in AI logs
- update AI BUILD ENTRY and AI PLAN ENTRY examples
- improve chronological accuracy of worklog
2026-03-16 00:32:57 +01:00
60723c33bc feat: improve terminal navigation and UI behavior
- refine tree and history navigation behavior
- improve scrolling continuity in the terminal UI
- keep AI worklog and prompt log updated
- validate changes with project quality checks
2026-03-16 00:28:27 +01:00
6195f7b8a3 docs: introduce standard AI log entry format
- add standardized AI BUILD ENTRY and AI PLAN ENTRY format
- improve consistency of AI development logs
- update instructions-agent.md
2026-03-15 22:22:35 +01:00
801544951f docs: introduce mandatory AI logging system
- add AI worklog and prompt logs
- enforce logging rules in instructions-agent.md
- require AI agents to record plan and build tasks
- establish traceable AI-assisted development workflow
2026-03-15 18:26:22 +01:00
65508d263f 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`
2026-03-15 17:46:11 +01:00
2bdfbe25d5 debug: add temporary history overlay 2026-03-15 12:03:11 +01:00
12d2577a31 feat: improve navigation behaviour 2026-03-14 18:54:20 +01:00
5 changed files with 691 additions and 28 deletions

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

@@ -0,0 +1,43 @@
# 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.
## 2026-03-15
- prompt summary: Introduce a mandatory AI logging system by updating `instructions-agent.md` and adding repository logs for AI work entries and prompt summaries.
- result summary: Added enforceable AI logging rules to `instructions-agent.md`, created `docs/ai-worklog.md` and `docs/ai-prompts.md`, and established a traceable workflow for recording significant AI-assisted tasks.
## 2026-03-15
- prompt summary: Introduce a standard AI log entry format in `instructions-agent.md` and record the change in the repository AI logs.
- result summary: Added a standardized `AI BUILD ENTRY` and `AI PLAN ENTRY` format, improved consistency expectations for AI logs, and updated the log files to reflect the new format.
## 2026-03-15
- prompt summary: Improve keyboard navigation in `src/app/ui.py` so Right expands or enters, Left collapses or goes to the parent, Enter confirms the selected destination, and ESC cancels back to the starting directory.
- result summary: Updated the tree-mode key handling in the terminal UI, preserved history mode behavior, and added focused UI tests for the new navigation rules.
## 2026-03-15
- prompt summary: Improve vertical scrolling in `src/app/ui.py` so the selected entry always remains visible in tree mode and history mode when the list is taller than the screen.
- result summary: Added UI scroll offset handling for tree and history views, kept existing navigation behavior unchanged, and added focused tests for the scroll helper logic.
## 2026-03-16 00:27
- prompt summary: Improve terminal UI navigation by refining tree and history keyboard behavior, adding scrolling continuity so selection stays visible, and preserving confirmation, cancellation, and history mode behavior.
- result summary: Updated `src/app/ui.py` and `tests/test_ui.py` to support dedicated history selection, improved Right/Left/Enter/ESC behavior, and vertical scrolling for both tree and history views while keeping the AI logs current.

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

@@ -0,0 +1,164 @@
# 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.
### AI BUILD ENTRY
Date: 2026-03-15
Task: 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.
- 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.
### AI BUILD ENTRY
Date: 2026-03-16 00:27
Task: build
Objective: Consolidate recent terminal UI navigation improvements and document the updated interaction behavior.
Files Modified:
- `src/app/ui.py`
- `tests/test_ui.py`
- `docs/ai-worklog.md`
- `docs/ai-prompts.md`
Key Decisions:
- Keep dedicated history mode while refining tree-mode keyboard behavior for Right, Left, Enter, and ESC.
- Add scroll continuity for both tree and history views so the selected row remains visible on long lists.
- Cover the UI-focused behavior with helper-level tests instead of changing navigator or filesystem logic.
Validation:
- make fix (passed)
- make quality (passed)
Result:
- The terminal UI now combines dedicated history mode, clearer confirmation and cancellation behavior, improved tree navigation semantics, and vertical scrolling that keeps the current selection visible in both tree and history views.
Open Issues:
- None.
### AI BUILD ENTRY
Date: 2026-03-15
Task: build
Objective: Introduce mandatory AI logging rules and document the repository logging workflow.
Files Modified:
- `instructions-agent.md`
- `docs/ai-worklog.md`
- `docs/ai-prompts.md`
Key Decisions:
- Require AI agents to log significant plan and build tasks.
- Preserve separate logs for work performed and prompts received.
- Keep entries concise and user-facing.
Validation:
- not run
Result:
- The repository now defines mandatory AI logging in `instructions-agent.md` and includes dedicated log files for AI work history and prompt summaries.
Open Issues:
- None.
### AI BUILD ENTRY
Date: 2026-03-15
Task: build
Objective: Record the introduction of the standard AI log entry format and improve AI logging consistency.
Files Modified:
- `instructions-agent.md`
- `docs/ai-worklog.md`
- `docs/ai-prompts.md`
Key Decisions:
- Add a dedicated "Standard AI Log Entry Format" section to define `AI BUILD ENTRY` and `AI PLAN ENTRY` blocks.
- Use the new standardized format for this worklog update to establish the pattern in the repository logs.
Validation:
- make fix (passed)
- make quality (passed)
Result:
- The repository now documents a standard AI log entry format and uses it to improve consistency of AI-assisted development logs.
Open Issues:
- None.
### AI BUILD ENTRY
Date: 2026-03-15
Task: build
Objective: Improve vertical scrolling in the terminal UI so the selected row remains visible in tree mode and history mode.
Files Modified:
- `src/app/ui.py`
- `tests/test_ui.py`
- `docs/ai-worklog.md`
- `docs/ai-prompts.md`
Key Decisions:
- Add separate scroll offsets for tree mode and history mode inside the UI state.
- Introduce a small helper to clamp scroll offsets so the selected row always stays within the visible window.
- Keep all existing navigation semantics unchanged while limiting the change to UI rendering behavior.
Validation:
- make fix (passed)
- make quality (passed)
Result:
- The terminal UI now scrolls vertically in both tree mode and history mode to keep the current selection visible when the list is taller than the screen.
Open Issues:
- None.
### AI BUILD ENTRY
Date: 2026-03-15
Task: build
Objective: Improve keyboard navigation behavior in the terminal UI for tree mode.
Files Modified:
- `src/app/ui.py`
- `tests/test_ui.py`
Key Decisions:
- Make Right expand collapsed directories and enter already expanded directories.
- Make Left collapse expanded directories and otherwise navigate to the parent directory.
- Make Enter confirm the selected path as the final destination and make ESC return the original starting directory.
Validation:
- make fix (passed)
- make quality (passed)
Result:
- Tree-mode keyboard navigation now supports expand-or-enter on Right, collapse-or-parent on Left, explicit selection confirmation on Enter, and cancel-to-start behavior on ESC.
Open Issues:
- None.

View File

@@ -59,3 +59,132 @@ 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.
## Mandatory AI Logging
Every significant AI interaction in this repository must be logged.
This applies to:
- `plan` tasks
- `build` tasks
- debugging tasks
- documentation tasks
- architectural discussions that affect the project
Required files:
- `docs/ai-worklog.md`
- `docs/ai-prompts.md`
Mandatory rules:
1. After every significant `plan` task, append a concise entry to `docs/ai-worklog.md`.
2. After every significant `build` task, append a concise entry to `docs/ai-worklog.md`.
3. If the task was driven by a meaningful prompt, also append an entry to `docs/ai-prompts.md`.
4. Logging is part of the task itself and must not be skipped.
5. The task is not complete until the logs are updated.
Each worklog entry must include:
- date
- task type (`plan` or `build`)
- short objective
- files inspected or modified
- key decisions
- validation commands run, if any
- result
- unresolved issues, if any
Validation logging rule:
- If validation commands such as `make fix`, `make quality`, `pytest`, or other checks are executed during the task, the worklog entry must record them explicitly.
- The value `validation: not run` may only be used if no validation commands were executed.
- The worklog entry must reflect the actual commands run during the task.
Each prompt log entry must include:
- date
- task type (`plan` or `build`)
- short prompt summary
- scope
- result summary
Rules:
- Do not store chain-of-thought or private reasoning.
- Store only concise user-facing summaries.
- Keep entries chronological.
- Even small but meaningful tasks must be logged.
## Standard AI Log Entry Format
To ensure consistency and readability of long AI-assisted development histories, all log entries must follow a standardized block format.
Agents must write log entries using the following structure.
Example for build tasks:
### AI BUILD ENTRY
Date: YYYY-MM-DD HH:MM
Task: build
Objective: short description of the task
Files Modified:
- file/path/example.py
- another/file.md
Key Decisions:
- short bullet explaining important choices
Validation:
- make fix (passed)
- make quality (passed)
Result:
- short description of what changed or was achieved
Open Issues:
- optional list of unresolved problems
Example for plan tasks:
### AI PLAN ENTRY
Date: YYYY-MM-DD HH:MM
Task: plan
Objective: short description of planning objective
Files Inspected:
- src/app/example.py
- specs/example_spec.md
Proposed Changes:
- summary of planned changes
Notes:
- optional relevant observations
Rules:
- Always use these headers exactly (`AI BUILD ENTRY` or `AI PLAN ENTRY`).
- Keep entries concise and structured.
- Do not include chain-of-thought reasoning.
- Use bullet points when possible.
- This format must be used when updating `docs/ai-worklog.md`.
- The Date field must include both date and time using the format YYYY-MM-DD HH:MM.

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,9 +65,46 @@ 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)
]
def clamp_scroll_offset(
selected_index: int, scroll_offset: int, visible_count: int, total_count: int
) -> int:
"""Keep the selected row within the visible scrolling window."""
if visible_count <= 0 or total_count <= visible_count:
return 0
max_offset = total_count - visible_count
offset = max(0, min(scroll_offset, max_offset))
if selected_index < offset:
return selected_index
if selected_index >= offset + visible_count:
return selected_index - visible_count + 1
return offset
class TerminalUI:
"""Minimal curses UI for the directory navigator MVP."""
def __init__(self) -> None:
self._history_mode = False
self._history_selected_index = 0
self._tree_scroll_offset = 0
self._history_scroll_offset = 0
def run(self, navigator: Navigator) -> Path:
"""Start the interactive session and return the final directory path."""
@@ -70,53 +115,52 @@ class TerminalUI:
def _run_session(self, stdscr: curses.window, navigator: Navigator) -> Path:
curses.curs_set(0)
stdscr.keypad(True)
original_path = navigator.current_path
while True:
self._render(stdscr, navigator)
key = stdscr.getch()
if key == ESCAPE_KEY:
return navigator.current_path
return original_path
if key == curses.KEY_UP:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=-1,
entry_count=len(navigator.visible_entries),
)
)
if key == ord("h"):
self._toggle_history_mode(navigator)
continue
if key == curses.KEY_DOWN:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=1,
entry_count=len(navigator.visible_entries),
)
)
if self._history_mode:
self._handle_history_key(key, navigator)
continue
if key == curses.KEY_RIGHT:
navigator.expand_selected_directory()
continue
if key == curses.KEY_LEFT:
navigator.collapse_selected_directory()
continue
if key in (curses.KEY_ENTER, 10, 13):
navigator.enter_selected_directory()
tree_result = self._handle_tree_key(key, navigator)
if tree_result is not None:
return tree_result
def _render(self, stdscr: curses.window, navigator: Navigator) -> None:
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)
for row, line in enumerate(build_render_lines(navigator), start=2):
lines = build_render_lines(navigator)
visible_count = max(0, height - 2)
self._tree_scroll_offset = clamp_scroll_offset(
navigator.selected_index,
self._tree_scroll_offset,
visible_count,
len(lines),
)
for row, line in enumerate(
lines[self._tree_scroll_offset : self._tree_scroll_offset + visible_count],
start=2,
):
if row >= height:
break
@@ -125,6 +169,133 @@ class TerminalUI:
stdscr.refresh()
def _render_history_mode(
self, stdscr: curses.window, navigator: Navigator, width: int, height: int
) -> None:
history_lines = build_history_lines(
navigator.history.entries(), self._history_selected_index
)
start_row = 1
visible_count = max(0, height - start_row - 1)
self._history_scroll_offset = clamp_scroll_offset(
self._history_selected_index,
self._history_scroll_offset,
visible_count,
len(history_lines),
)
stdscr.addnstr(start_row, 0, "History:", width - 1, curses.A_BOLD)
for index, line in enumerate(
history_lines[
self._history_scroll_offset : self._history_scroll_offset
+ visible_count
],
start=1,
):
row = start_row + index
if row >= height:
break
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)
self._history_scroll_offset = 0
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
self._tree_scroll_offset = 0
def _handle_tree_key(self, key: int, navigator: Navigator) -> Path | None:
if key == curses.KEY_UP:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=-1,
entry_count=len(navigator.visible_entries),
)
)
return None
if key == curses.KEY_DOWN:
navigator.set_selected_index(
move_selection(
navigator.selected_index,
step=1,
entry_count=len(navigator.visible_entries),
)
)
return None
if key == curses.KEY_RIGHT:
was_expanded = navigator.selected_entry.is_expanded
if not navigator.expand_selected_directory():
navigator.enter_selected_directory()
self._tree_scroll_offset = 0
return None
if was_expanded:
navigator.enter_selected_directory()
self._tree_scroll_offset = 0
return None
if key == curses.KEY_LEFT:
if not navigator.collapse_selected_directory():
navigator.go_to_parent_directory()
self._tree_scroll_offset = 0
return None
if key == ord("b"):
navigator.go_back()
self._tree_scroll_offset = 0
return None
if key == ord("f"):
navigator.go_forward()
self._tree_scroll_offset = 0
return None
if key in (curses.KEY_ENTER, 10, 13):
return navigator.selected_entry.path
return None
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
def open_terminal_streams() -> Iterator[tuple[TextIO, TextIO]]:

View File

@@ -1,11 +1,16 @@
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,
clamp_scroll_offset,
format_entry,
move_selection,
open_terminal_streams,
@@ -65,6 +70,157 @@ 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_clamp_scroll_offset_keeps_selection_visible_when_moving_down() -> None:
assert clamp_scroll_offset(5, scroll_offset=0, visible_count=3, total_count=10) == 3
def test_clamp_scroll_offset_keeps_selection_visible_when_moving_up() -> None:
assert clamp_scroll_offset(1, scroll_offset=4, visible_count=3, total_count=10) == 1
def test_clamp_scroll_offset_returns_zero_when_all_items_fit() -> None:
assert clamp_scroll_offset(2, scroll_offset=1, visible_count=5, total_count=3) == 0
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_right_arrow_expands_collapsed_directory(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
scripts = workspace / "scripts"
build = scripts / "build"
workspace.mkdir()
scripts.mkdir()
build.mkdir()
navigator = Navigator(workspace)
navigator.set_selected_index(1)
ui = TerminalUI()
assert ui._handle_tree_key(curses.KEY_RIGHT, navigator) is None
assert navigator.current_path == workspace.resolve()
assert [entry.name for entry in navigator.visible_entries] == [
"..",
"scripts",
"build",
]
def test_right_arrow_enters_already_expanded_directory(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
scripts = workspace / "scripts"
build = scripts / "build"
workspace.mkdir()
scripts.mkdir()
build.mkdir()
navigator = Navigator(workspace)
navigator.set_selected_index(1)
navigator.expand_selected_directory()
ui = TerminalUI()
assert ui._handle_tree_key(curses.KEY_RIGHT, navigator) is None
assert navigator.current_path == scripts.resolve()
assert navigator.selected_index == 0
def test_left_arrow_goes_to_parent_when_selected_entry_is_collapsed(
tmp_path: Path,
) -> None:
parent = tmp_path / "parent"
current = parent / "current"
child = current / "child"
parent.mkdir()
current.mkdir()
child.mkdir()
navigator = Navigator(current)
navigator.set_selected_index(1)
ui = TerminalUI()
assert ui._handle_tree_key(curses.KEY_LEFT, navigator) is None
assert navigator.current_path == parent.resolve()
assert navigator.selected_index == 0
def test_enter_returns_selected_path_without_navigating(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
child = workspace / "child"
workspace.mkdir()
child.mkdir()
navigator = Navigator(workspace)
navigator.set_selected_index(1)
ui = TerminalUI()
result = ui._handle_tree_key(10, navigator)
assert result == child.resolve()
assert navigator.current_path == workspace.resolve()
def test_open_terminal_streams_raises_without_terminal(
monkeypatch: pytest.MonkeyPatch,
) -> None: