Compare commits
19 Commits
v0.1.0
...
feature/na
| Author | SHA1 | Date | |
|---|---|---|---|
| d9cf164b2c | |||
| a64bc768b5 | |||
| f7f09b895d | |||
| 10c3849fb3 | |||
| f28fef7758 | |||
| ab689cfbb4 | |||
| 9688ed0974 | |||
| d9d412973b | |||
| 9704d3ef5b | |||
| 0118ad9314 | |||
| e766210920 | |||
| 6849631cc1 | |||
| 7c87d6f1f0 | |||
| 60723c33bc | |||
| 6195f7b8a3 | |||
| 801544951f | |||
| 65508d263f | |||
| 2bdfbe25d5 | |||
| 12d2577a31 |
48
CHANGELOG.md
48
CHANGELOG.md
@@ -4,16 +4,42 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on Keep a Changelog, and this project follows Semantic Versioning.
|
||||
|
||||
## [1.0.0] - 2026-03-14
|
||||
## [0.2.3] - 2026-04-02
|
||||
|
||||
### Added
|
||||
- Initial release of the Python AI Dev Template
|
||||
- Universal Python project structure using `src/`
|
||||
- `pyproject.toml`-based project configuration
|
||||
- Development tooling with `black`, `ruff`, `mypy`, and `pytest`
|
||||
- `pre-commit` integration
|
||||
- `Makefile` with development, quality, and diagnostic commands
|
||||
- `instructions-agent.md` for AI coding agent guidance
|
||||
- `.env.example` for environment-based configuration
|
||||
- README with setup, workflow, and template reuse instructions
|
||||
- MIT license
|
||||
- Add project demo GIF at `docs/demo.gif`
|
||||
- Show GIF demo in README
|
||||
|
||||
## [0.2.2] - 2026-04-02
|
||||
|
||||
### Fixed
|
||||
- Include `cd_browser_post_install.py` in wheel/sdist so `cd_browser_post_install` works after `pip install`
|
||||
|
||||
## [0.2.1] - 2026-04-02
|
||||
|
||||
### Fixed
|
||||
- Prevent crashes when scanning directories with restricted permissions
|
||||
- Handle non-interactive post-install runs without failing on EOF
|
||||
- Fix shell reload message interpolation in post-install output
|
||||
|
||||
### Improved
|
||||
- Make post-install shell integration idempotent (avoid duplicate `cd_()` entries)
|
||||
- Add test coverage for permission errors and post-install flows
|
||||
- Update default application name to `cd-browser`
|
||||
|
||||
## [0.2.0] - 2026-04-02
|
||||
|
||||
### Added
|
||||
- Interactive post-install script (`cd_browser_post_install`) for automatic shell integration
|
||||
- Option to automatically add `cd_()` function to ~/.bashrc or ~/.zshrc during installation
|
||||
- Improved uninstall instructions in README
|
||||
|
||||
### Improved
|
||||
- Installation UX: clearer setup process with user prompts
|
||||
- Shell integration: better guidance for enabling `cd_` command
|
||||
|
||||
### Added
|
||||
- Initial release of **cd-browser**
|
||||
- Terminal-based directory navigation
|
||||
- Interactive directory tree navigation
|
||||
- History navigation mode
|
||||
|
||||
66
README.md
66
README.md
@@ -1,46 +1,66 @@
|
||||
# cd-browser
|
||||
|
||||
`cd-browser` is a terminal-native directory navigator for fast filesystem browsing from the command line.
|
||||
**Stop typing paths. Browse them.**
|
||||
|
||||
It provides an interactive interface for exploring directories with the keyboard and returns the selected path at the end of the session so shell wrappers can change the current shell directory.
|
||||
`cd-browser` is a fast keyboard-driven directory navigator for the terminal.
|
||||
It lets you explore directory trees visually and jump to any folder instantly.
|
||||
|
||||

|
||||
|
||||
## Why cd-browser?
|
||||
|
||||
Working in the terminal often means:
|
||||
|
||||
- typing long directory paths
|
||||
- navigating deep folder trees
|
||||
- repeating `cd ..` multiple times
|
||||
|
||||
`cd-browser` provides an **interactive terminal UI** that allows you to browse directories with the keyboard and return the selected path directly to your shell.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 Fast visual navigation of directory trees
|
||||
- ⌨️ Fully keyboard-driven workflow
|
||||
- 🌲 Expand and collapse directories
|
||||
- 📜 Navigation history inside the session
|
||||
- 🖥 Native terminal interface
|
||||
- 🔁 Works with Bash, Zsh and other shells
|
||||
- ⚡ Returns the selected path to the shell
|
||||
- 🔎 Press `.` to toggle hidden directories
|
||||
|
||||
## Quick Demo
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd_
|
||||
```
|
||||
|
||||
Browse directories using the arrow keys and press **Enter** to jump directly to the selected folder.
|
||||
|
||||
## Documentation
|
||||
|
||||
See the documentation index:
|
||||
|
||||
```
|
||||
docs/index.md
|
||||
|
||||
## Features
|
||||
|
||||
- Interactive terminal directory browser
|
||||
- Keyboard-driven navigation
|
||||
- Parent directory entry via `..`
|
||||
- Expand and collapse directory trees
|
||||
- Session navigation history support in the application state
|
||||
- Installable CLI command: `cd_browser`
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Install the project in user mode:
|
||||
|
||||
```bash
|
||||
pip install .
|
||||
pip install cd-browser
|
||||
```
|
||||
|
||||
For editable local development:
|
||||
**Important**: After installation, run this to set up the `cd_` command:
|
||||
|
||||
```bash
|
||||
pip install -e '.[dev]'
|
||||
cd_browser_post_install
|
||||
```
|
||||
|
||||
After installation, the application command is:
|
||||
|
||||
```bash
|
||||
cd_browser
|
||||
```
|
||||
|
||||
When the interactive session exits, the program prints the final selected directory path.
|
||||
This interactive script will guide you through enabling `cd_` in your shell.
|
||||
|
||||
## Shell Integration For `cd_`
|
||||
|
||||
@@ -77,6 +97,8 @@ Then use:
|
||||
cd_
|
||||
```
|
||||
|
||||
- Dentro de `cd_browser`, presiona `.` para alternar la visualización de carpetas ocultas.
|
||||
|
||||
## Uninstall
|
||||
|
||||
If the project was installed with `pip`, remove it with:
|
||||
@@ -85,7 +107,7 @@ If the project was installed with `pip`, remove it with:
|
||||
pip uninstall cd-browser
|
||||
```
|
||||
|
||||
If you also added the `cd_` shell wrapper, remove that function from your shell profile and reload the shell configuration.
|
||||
**Important**: After uninstalling, remove the `cd_()` function from your shell profile (~/.bashrc or ~/.zshrc) to clean up completely.
|
||||
|
||||
## Developer Setup
|
||||
|
||||
|
||||
219
docs/ai-execution-log.md
Normal file
219
docs/ai-execution-log.md
Normal file
@@ -0,0 +1,219 @@
|
||||
|
||||
# AI Execution Log
|
||||
|
||||
This file stores the **full execution traces** of AI-assisted tasks.
|
||||
|
||||
Unlike:
|
||||
- `docs/ai-worklog.md` → concise technical task summaries
|
||||
- `docs/ai-prompts.md` → concise prompt summaries
|
||||
|
||||
this file preserves the **complete operational record** of an AI session.
|
||||
|
||||
The purpose of this file is to make it possible to reconstruct:
|
||||
- the exact prompt given to the agent
|
||||
- the execution plan or todo list generated by the agent
|
||||
- the final execution report returned by the agent
|
||||
|
||||
This file is intended for **full traceability** of AI-assisted development.
|
||||
|
||||
Entries must be appended in chronological order.
|
||||
|
||||
---
|
||||
|
||||
## Standard Entry Structure
|
||||
|
||||
Each execution entry should follow this structure:
|
||||
|
||||
```md
|
||||
## BUILD-YYYYMMDD-XXX
|
||||
|
||||
Date: YYYY-MM-DD HH:MM
|
||||
Agent: OpenCode
|
||||
Task Type: BUILD
|
||||
|
||||
---
|
||||
|
||||
### Prompt Provided to Agent
|
||||
|
||||
<full prompt text>
|
||||
|
||||
---
|
||||
|
||||
### Agent Todos
|
||||
|
||||
# Todos
|
||||
[ ] item 1
|
||||
[ ] item 2
|
||||
|
||||
---
|
||||
|
||||
### Agent Execution Report
|
||||
|
||||
<final report returned by the agent>
|
||||
```
|
||||
|
||||
For plan tasks, use:
|
||||
|
||||
```md
|
||||
## PLAN-YYYYMMDD-XXX
|
||||
|
||||
Date: YYYY-MM-DD HH:MM
|
||||
Agent: OpenCode
|
||||
Task Type: PLAN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging Rules
|
||||
|
||||
When a significant AI task is executed, this file should record:
|
||||
|
||||
1. The prompt that was actually given to the agent.
|
||||
2. The todo or execution plan shown by the agent.
|
||||
3. The final summary or execution report returned by the agent.
|
||||
|
||||
Do not store private chain-of-thought.
|
||||
Only store user-visible execution artifacts.
|
||||
|
||||
---
|
||||
|
||||
## Relationship With Other Logs
|
||||
|
||||
- `docs/ai-prompts.md` stores a concise summary of the prompt.
|
||||
- `docs/ai-worklog.md` stores a concise technical summary of what was done.
|
||||
- `docs/ai-execution-log.md` stores the complete visible execution trace.
|
||||
|
||||
Together, the three files provide:
|
||||
|
||||
1. Prompt intent
|
||||
2. Technical result
|
||||
3. Full execution trace
|
||||
|
||||
---
|
||||
|
||||
## First Use
|
||||
|
||||
The next significant OpenCode task should append the first real execution entry to this file using the structure above.
|
||||
|
||||
---
|
||||
|
||||
## BUILD-20260316-005
|
||||
|
||||
Date: 2026-03-16 02:51
|
||||
Agent: OpenCode
|
||||
Task Type: BUILD
|
||||
|
||||
---
|
||||
|
||||
### Prompt Provided to Agent
|
||||
|
||||
Task Type:
|
||||
BUILD
|
||||
|
||||
Task ID:
|
||||
BUILD-20260316-005
|
||||
|
||||
Date:
|
||||
2026-03-16 02:51
|
||||
|
||||
Objective:
|
||||
Finalize AI logging framework and repository workflow updates
|
||||
|
||||
Context:
|
||||
The repository contains pending changes related to the AI logging framework, execution logging, and AI task workflow. These changes introduce a three-level logging system (`ai-worklog.md`, `ai-prompts.md`, and `ai-execution-log.md`), improved agent workflow rules, and enhancements to the `scripts/ai-task` tooling. The goal of this task is to validate these changes, ensure logs are consistent, and publish the finalized framework.
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
- docs/ai-execution-log.md
|
||||
|
||||
Requirements:
|
||||
1. Inspect the current working tree and confirm the changes in scope.
|
||||
2. Verify that the AI logging framework is consistent across:
|
||||
- instructions-agent.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
3. Ensure that `scripts/ai-task` supports the three logging files:
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
- docs/ai-execution-log.md
|
||||
4. Append a new AI BUILD ENTRY to `docs/ai-worklog.md`.
|
||||
5. Append a new BUILD PROMPT ENTRY to `docs/ai-prompts.md`.
|
||||
6. Append a new execution entry to `docs/ai-execution-log.md` containing:
|
||||
- the full prompt
|
||||
- the agent todos
|
||||
- the final execution report
|
||||
7. Ensure all three logs remain in chronological order.
|
||||
8. Stage all modified files within the defined scope.
|
||||
9. Create a single commit describing the finalized AI workflow and logging framework.
|
||||
10. Push the commit to the current branch.
|
||||
|
||||
Constraints:
|
||||
- follow AGENT_WORKFLOW.md
|
||||
- keep scope limited to the files listed above
|
||||
- preserve existing project behavior unless explicitly changed
|
||||
- logs must be written before creating the commit
|
||||
- logs must be included in the same commit
|
||||
|
||||
Testing:
|
||||
- run `make fix`
|
||||
- run `make quality`
|
||||
- verify that `scripts/ai-task` still generates a valid BUILD task prompt
|
||||
- verify that log files remain chronologically ordered
|
||||
|
||||
Validation:
|
||||
make fix
|
||||
make quality
|
||||
|
||||
Logging requirements:
|
||||
- append entry to docs/ai-worklog.md
|
||||
- append entry to docs/ai-prompts.md
|
||||
- include Date, Task ID, Agent, Scope
|
||||
- use Task ID: BUILD-20260316-005
|
||||
- maintain chronological Task ID order in ai-worklog.md, ai-prompts.md and ai-execution-log.md
|
||||
- append entry to docs/ai-execution-log.md
|
||||
|
||||
Execution Logging requirements:
|
||||
- append entry to docs/ai-execution-log.md
|
||||
- include:
|
||||
- full prompt
|
||||
- agent todos
|
||||
- final execution report
|
||||
- use the standard execution log format defined in docs/ai-execution-log.md
|
||||
|
||||
Git requirements:
|
||||
- create a single commit
|
||||
- include all updated logs in the commit
|
||||
- push the commit to the current branch
|
||||
|
||||
Expected Report:
|
||||
- files committed
|
||||
- commit hash
|
||||
- validation results
|
||||
- confirmation that logs were updated before commit
|
||||
- confirmation that push succeeded
|
||||
|
||||
---
|
||||
|
||||
### Agent Todos
|
||||
|
||||
# Todos
|
||||
[x] Inspect scoped files and current logging framework changes
|
||||
[x] Update worklog, prompt log, and execution log for BUILD-20260316-005
|
||||
[x] Validate `scripts/ai-task` output and run `make fix` and `make quality`
|
||||
[x] Stage scoped files, create single commit, and push branch
|
||||
|
||||
---
|
||||
|
||||
### Agent Execution Report
|
||||
|
||||
- Verified that `instructions-agent.md`, `AGENT_WORKFLOW.md`, `AI_TASK_TEMPLATE.md`, and `scripts/ai-task` describe the same three-level logging workflow.
|
||||
- Updated the task template and logs so the worklog, prompt log, and execution log stay aligned and chronological.
|
||||
- Confirmed `scripts/ai-task build "test task" src/app/ui.py` still produces a valid BUILD task prompt that references all three logs.
|
||||
- Ran `make fix` and `make quality` successfully before commit creation.
|
||||
- Prepared the scoped files for a single commit and push covering the finalized AI workflow and logging framework.
|
||||
221
docs/ai-prompts.md
Normal file
221
docs/ai-prompts.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# AI Prompts Log
|
||||
|
||||
This file stores the prompts that triggered significant AI-driven development tasks.
|
||||
|
||||
The goal is to preserve traceability between:
|
||||
- the prompt given to the AI
|
||||
- the scope of the change
|
||||
- the resulting work recorded in `ai-worklog.md`
|
||||
|
||||
Each entry must include:
|
||||
- Date (YYYY-MM-DD HH:MM)
|
||||
- Task ID
|
||||
- Agent
|
||||
- Task type (plan/build)
|
||||
- Scope
|
||||
- Prompt summary
|
||||
- Result summary
|
||||
|
||||
Only record prompts that:
|
||||
- change architecture
|
||||
- introduce new behavior
|
||||
- modify important logic
|
||||
- affect project structure
|
||||
- debug complex issues
|
||||
|
||||
Do not store chain-of-thought reasoning. Only store the prompt intent and a concise result summary.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-15 18:05
|
||||
Task ID: BUILD-20260315-001
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-15 19:25
|
||||
Task ID: BUILD-20260315-002
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-15 20:00
|
||||
Task ID: BUILD-20260315-003
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
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 standardized `AI BUILD ENTRY` and `AI PLAN ENTRY` formats, improving consistency and long-term traceability of AI-assisted development.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-15 21:35
|
||||
Task ID: BUILD-20260315-004
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-15 22:20
|
||||
Task ID: BUILD-20260315-005
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-16 00:27
|
||||
Task ID: BUILD-20260316-001
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
Prompt Summary:
|
||||
Refine terminal UI navigation, maintain dedicated history mode, and ensure scrolling continuity so selections remain visible.
|
||||
|
||||
Result Summary:
|
||||
Updated terminal UI behavior, consolidated navigation improvements, and ensured consistent scrolling and selection visibility across tree and history modes.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-16 01:26
|
||||
Task ID: BUILD-20260316-002
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Prompt Summary:
|
||||
Finalize the AI logging format system by requiring timestamped structured log entries and aligning the repository worklog and prompt log with the current logging framework.
|
||||
|
||||
Result Summary:
|
||||
Updated the logging instructions, normalized the prompt log to the structured entry format, and added matching repository log entries for the finalized AI logging system.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-16 01:34
|
||||
Task ID: BUILD-20260316-003
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- AGENT.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
|
||||
Prompt Summary:
|
||||
Introduce the AI agent operational framework by adding agent guidance, workflow rules, a reusable task template, and a CLI tool that generates standardized AI task prompts.
|
||||
|
||||
Result Summary:
|
||||
Added the repository agent framework documents, introduced the `scripts/ai-task` helper, aligned the framework with validation and logging rules, and recorded the change in the AI logs.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-16 01:45
|
||||
Task ID: BUILD-20260316-004
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- AGENT.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Prompt Summary:
|
||||
Finalize and publish the repository AI agent workflow and instruction framework by strengthening workflow and logging rules, documenting the agent framework files, and shipping the task generation tooling.
|
||||
|
||||
Result Summary:
|
||||
Updated the workflow and instruction rules, recorded the framework rollout in the AI logs, and prepared the branch so the agent framework documents and `scripts/ai-task` tooling are published together.
|
||||
|
||||
---
|
||||
|
||||
### BUILD PROMPT ENTRY
|
||||
Date: 2026-03-16 02:51
|
||||
Task ID: BUILD-20260316-005
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
- docs/ai-execution-log.md
|
||||
|
||||
Prompt Summary:
|
||||
Finalize the AI logging framework by aligning instructions, workflow rules, task templates, generator tooling, and all three repository logs, then validate and publish the changes.
|
||||
|
||||
Result Summary:
|
||||
Updated the repository workflow documents and task template for the three-level logging model, verified `scripts/ai-task`, and recorded synchronized worklog, prompt log, and execution log entries.
|
||||
377
docs/ai-worklog.md
Normal file
377
docs/ai-worklog.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# AI Worklog
|
||||
|
||||
This file records important AI‑assisted development actions.
|
||||
|
||||
The log is designed for long‑term traceability of AI‑driven development.
|
||||
|
||||
Each entry must include:
|
||||
- Date (YYYY-MM-DD HH:MM)
|
||||
- Task ID
|
||||
- Agent
|
||||
- Task type (plan/build)
|
||||
- Objective
|
||||
- Scope
|
||||
- Files Modified / Files Inspected
|
||||
- Key Decisions or Proposed Changes
|
||||
- Validation
|
||||
- Result
|
||||
- Open Issues
|
||||
|
||||
Entries must remain chronological.
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15 18:10
|
||||
Task ID: BUILD-20260315-001
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Replace the temporary history debug overlay with a dedicated history mode in the terminal UI.
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
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 and allows navigation and selection of past directories.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15 19:30
|
||||
Task ID: BUILD-20260315-002
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Introduce mandatory AI logging rules and repository logging workflow.
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Files Modified:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Key Decisions:
|
||||
- Require AI agents to log plan and build tasks.
|
||||
- Separate logs for work results and prompts.
|
||||
- Keep entries concise and user‑focused.
|
||||
|
||||
Validation:
|
||||
- not run
|
||||
|
||||
Result:
|
||||
- Repository now enforces AI logging via instructions-agent.md.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15 20:05
|
||||
Task ID: BUILD-20260315-003
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Introduce the standard AI log entry format and improve logging consistency.
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Files Modified:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Key Decisions:
|
||||
- Define AI BUILD ENTRY and AI PLAN ENTRY blocks.
|
||||
- Standardize structure for long‑term repository traceability.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- Repository now uses a standardized AI logging format.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15 21:40
|
||||
Task ID: BUILD-20260315-004
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Improve keyboard navigation behavior in the terminal UI for tree mode.
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
Files Modified:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
Key Decisions:
|
||||
- Right expands collapsed directories and enters expanded directories.
|
||||
- Left collapses expanded directories or navigates to the parent directory.
|
||||
- Enter confirms the selected path.
|
||||
- ESC cancels and returns to the original directory.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- Tree navigation semantics improved with clearer expand, enter, and cancel behavior.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-15 22:25
|
||||
Task ID: BUILD-20260315-005
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Implement vertical scrolling so the selected entry always remains visible.
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
Files Modified:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
|
||||
Key Decisions:
|
||||
- Introduce scroll offsets in the UI state.
|
||||
- Clamp offsets to keep the selected row visible.
|
||||
- Apply the same mechanism to history mode.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- Tree and history views now scroll when navigating beyond the visible screen area.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-16 00:27
|
||||
Task ID: BUILD-20260316-001
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Consolidate terminal UI navigation improvements and document updated behavior.
|
||||
|
||||
Scope:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Files Modified:
|
||||
- src/app/ui.py
|
||||
- tests/test_ui.py
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Key Decisions:
|
||||
- Maintain dedicated history mode.
|
||||
- Refine tree-mode keyboard behavior.
|
||||
- Keep UI logic isolated from navigator logic.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- Terminal UI now includes history mode, improved navigation semantics, and reliable vertical scrolling.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-16 01:26
|
||||
Task ID: BUILD-20260316-002
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Finalize the AI logging format system and align repository log files with the structured logging framework.
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Files Modified:
|
||||
- instructions-agent.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Key Decisions:
|
||||
- Require timestamped log entries with date and time, Task ID, Agent, and Scope fields.
|
||||
- Keep the worklog and prompt log aligned with the current structured logging framework.
|
||||
- Normalize prompt logging into a single consistent `BUILD PROMPT ENTRY` format.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- The AI logging system now uses consistent structured entries across repository instructions, worklog history, and prompt history.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-16 01:34
|
||||
Task ID: BUILD-20260316-003
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Introduce the AI agent operational framework and task generation tooling for standardized AI-assisted repository work.
|
||||
|
||||
Scope:
|
||||
- AGENT.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
|
||||
Files Modified:
|
||||
- AGENT.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Key Decisions:
|
||||
- Define separate documents for agent behavior, operational workflow, and reusable task structure.
|
||||
- Keep the task generator focused on producing standardized PLAN and BUILD prompts with task IDs and validation requirements.
|
||||
- Align the new framework with the repository logging rules and validation workflow.
|
||||
|
||||
Validation:
|
||||
- scripts/ai-task build "test task" src/app/ui.py (passed)
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- The repository now includes a documented AI agent framework and an executable helper tool for generating standardized AI task prompts.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-16 01:45
|
||||
Task ID: BUILD-20260316-004
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Finalize and publish the AI agent workflow, instruction framework, and task tooling updates.
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- AGENT.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Files Modified:
|
||||
- instructions-agent.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
|
||||
Key Decisions:
|
||||
- Strengthen commit and logging order rules so AI logs must be written before creating commits.
|
||||
- Keep the new agent framework documents and task tooling aligned with the repository validation and traceability model.
|
||||
- Publish the previously prepared framework files together with the finalized logging guidance on the current branch.
|
||||
|
||||
Validation:
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- The repository now has a finalized AI instruction and workflow framework, standardized task tooling, and matching logs documenting the framework rollout.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
### AI BUILD ENTRY
|
||||
Date: 2026-03-16 02:51
|
||||
Task ID: BUILD-20260316-005
|
||||
Agent: OpenCode
|
||||
Task: build
|
||||
Objective: Finalize the three-level AI logging framework and repository workflow updates.
|
||||
|
||||
Scope:
|
||||
- instructions-agent.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
- docs/ai-execution-log.md
|
||||
|
||||
Files Modified:
|
||||
- instructions-agent.md
|
||||
- AGENT_WORKFLOW.md
|
||||
- AI_TASK_TEMPLATE.md
|
||||
- scripts/ai-task
|
||||
- docs/ai-worklog.md
|
||||
- docs/ai-prompts.md
|
||||
- docs/ai-execution-log.md
|
||||
|
||||
Key Decisions:
|
||||
- Keep the logging framework aligned across instructions, workflow documentation, template guidance, and task-generation tooling.
|
||||
- Require all three logs to be updated together so prompt intent, technical summary, and execution trace stay correlated.
|
||||
- Preserve project behavior while improving workflow traceability and repository process consistency.
|
||||
|
||||
Validation:
|
||||
- scripts/ai-task build "test task" src/app/ui.py (passed)
|
||||
- make fix (passed)
|
||||
- make quality (passed)
|
||||
|
||||
Result:
|
||||
- The repository now documents and validates a complete three-level AI logging workflow backed by updated instructions, workflow rules, task templates, and generator output.
|
||||
|
||||
Open Issues:
|
||||
- None
|
||||
|
||||
---
|
||||
BIN
docs/demo.gif
Normal file
BIN
docs/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
@@ -1,61 +0,0 @@
|
||||
# Agent Instructions: Universal Python Project Template
|
||||
|
||||
## Project Overview
|
||||
This repository is a universal and adaptable Python template intended to serve as a professional starting point for new projects.
|
||||
|
||||
## Core Directives
|
||||
1. All code, comments, variables, string literals, commit messages, and documentation must be written in English.
|
||||
2. Use Python 3.11+ with type hints whenever possible.
|
||||
3. Follow clean code principles and PEP 8.
|
||||
4. Keep modules small, cohesive, and maintainable.
|
||||
5. Use consistent docstrings for public classes and functions.
|
||||
6. Prefer explicit error handling over silent failures.
|
||||
|
||||
## Architecture Guidelines
|
||||
1. Keep business logic separate from infrastructure concerns.
|
||||
2. Avoid mixing configuration, I/O, and domain logic in the same module.
|
||||
3. Prefer reusable services over duplicated logic.
|
||||
4. Do not introduce unnecessary complexity or premature abstractions.
|
||||
|
||||
## Quality Standards
|
||||
1. Every meaningful feature should include tests.
|
||||
2. Run formatting, linting, and type checks before considering a task complete.
|
||||
3. Keep the README updated whenever setup, commands, or structure changes.
|
||||
4. Do not leave placeholder code unless clearly marked.
|
||||
|
||||
## Tooling
|
||||
- Testing: pytest
|
||||
- Formatting: black
|
||||
- Linting: ruff
|
||||
- Static typing: mypy
|
||||
|
||||
## Expected Project Structure
|
||||
src/app/
|
||||
tests/
|
||||
scripts/
|
||||
README.md
|
||||
instructions-agent.md
|
||||
pyproject.toml
|
||||
|
||||
## Agent Workflow
|
||||
1. First understand the repository structure.
|
||||
2. Propose a short implementation plan before major changes.
|
||||
3. Reuse existing modules whenever possible.
|
||||
4. Keep changes minimal, coherent, and production-oriented.
|
||||
5. Validate changes with tests and quality tools when possible.
|
||||
|
||||
## Application Specification
|
||||
|
||||
This repository includes an application specification that defines the expected behavior of the project.
|
||||
|
||||
Primary specification file:
|
||||
|
||||
- `specs/cd_browser_spec.md`
|
||||
|
||||
Agent rules:
|
||||
|
||||
1. Before implementing any feature, read the application specification.
|
||||
2. If implementation details are unclear, follow the specification first.
|
||||
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.
|
||||
@@ -4,17 +4,31 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "cd-browser"
|
||||
version = "0.1.0"
|
||||
description = "A universal and adaptable Python project template for AI-assisted development."
|
||||
version = "0.2.3"
|
||||
description = "A fast keyboard-driven directory navigator for the terminal."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.8"
|
||||
authors = [
|
||||
{ name = "Saky" }
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[project.scripts]
|
||||
cd_browser = "app.main:main"
|
||||
cd_browser_post_install = "cd_browser_post_install:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
@@ -27,6 +41,7 @@ dev = [
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
py-modules = ["cd_browser_post_install"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
149
scripts/ai-task
Executable file
149
scripts/ai-task
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ai-task
|
||||
# Helper CLI to generate AI task prompts based on AI_TASK_TEMPLATE.md
|
||||
# Usage:
|
||||
# scripts/ai-task build "Objective text" [scope...]
|
||||
# scripts/ai-task plan "Objective text" [scope...]
|
||||
|
||||
set -e
|
||||
|
||||
TYPE="$1"
|
||||
OBJECTIVE="$2"
|
||||
shift 2 || true
|
||||
SCOPE=("$@")
|
||||
|
||||
if [[ -z "$TYPE" || -z "$OBJECTIVE" ]]; then
|
||||
echo "Usage: ai-task <plan|build> \"Objective\" [scope files...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TYPE_LOWER=$(echo "$TYPE" | tr '[:upper:]' '[:lower:]')
|
||||
TYPE_UPPER=$(echo "$TYPE_LOWER" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
DATE=$(date "+%Y-%m-%d %H:%M")
|
||||
DAY=$(date "+%Y%m%d")
|
||||
|
||||
case "$TYPE_LOWER" in
|
||||
build)
|
||||
PREFIX="BUILD"
|
||||
;;
|
||||
plan)
|
||||
PREFIX="PLAN"
|
||||
;;
|
||||
*)
|
||||
echo "Error: task type must be 'plan' or 'build'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
WORKLOG_FILE="docs/ai-worklog.md"
|
||||
PROMPTS_FILE="docs/ai-prompts.md"
|
||||
EXECUTION_LOG_FILE="docs/ai-execution-log.md"
|
||||
|
||||
check_task_id_order() {
|
||||
local file="$1"
|
||||
local prefix="$2"
|
||||
local previous=""
|
||||
local current=""
|
||||
|
||||
[[ -f "$file" ]] || return 0
|
||||
|
||||
while IFS= read -r current; do
|
||||
if [[ -n "$previous" && "$current" < "$previous" ]]; then
|
||||
echo "Error: task IDs in $file are out of chronological order for prefix $prefix" >&2
|
||||
echo "Previous: $previous" >&2
|
||||
echo "Current: $current" >&2
|
||||
echo "Please fix the log order before generating a new task." >&2
|
||||
exit 1
|
||||
fi
|
||||
previous="$current"
|
||||
done < <(grep -Eo "${prefix}-[0-9]{8}-[0-9]{3}" "$file" || true)
|
||||
}
|
||||
|
||||
check_task_id_order "$WORKLOG_FILE" "$PREFIX"
|
||||
check_task_id_order "$PROMPTS_FILE" "$PREFIX"
|
||||
check_task_id_order "$EXECUTION_LOG_FILE" "$PREFIX"
|
||||
|
||||
LAST_SEQ=0
|
||||
|
||||
if [[ -f "$WORKLOG_FILE" ]]; then
|
||||
LAST_MATCH=$(grep -Eo "${PREFIX}-${DAY}-[0-9]{3}" "$WORKLOG_FILE" | tail -n 1 || true)
|
||||
if [[ -n "$LAST_MATCH" ]]; then
|
||||
LAST_SEQ=${LAST_MATCH##*-}
|
||||
LAST_SEQ=$((10#$LAST_SEQ))
|
||||
fi
|
||||
fi
|
||||
|
||||
NEXT_SEQ=$(printf "%03d" $((LAST_SEQ + 1)))
|
||||
TASK_ID="${PREFIX}-${DAY}-${NEXT_SEQ}"
|
||||
|
||||
# Build scope block
|
||||
cat <<EOF
|
||||
Task Type:
|
||||
${TYPE_UPPER}
|
||||
|
||||
Task ID:
|
||||
${TASK_ID}
|
||||
|
||||
Date:
|
||||
${DATE}
|
||||
|
||||
Objective:
|
||||
${OBJECTIVE}
|
||||
|
||||
Context:
|
||||
<describe repository state if needed>
|
||||
|
||||
Scope:
|
||||
EOF
|
||||
|
||||
if [ ${#SCOPE[@]} -gt 0 ]; then
|
||||
for item in "${SCOPE[@]}"; do
|
||||
printf '%s\n' "- ${item}"
|
||||
done
|
||||
else
|
||||
printf '%s\n' "- <fill scope>"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Requirements:
|
||||
<describe expected behavior>
|
||||
|
||||
Constraints:
|
||||
- follow AGENT_WORKFLOW.md
|
||||
- keep scope minimal
|
||||
- preserve existing behavior unless explicitly changed
|
||||
|
||||
Testing:
|
||||
<describe expected tests>
|
||||
|
||||
Validation:
|
||||
make fix
|
||||
make quality
|
||||
|
||||
Logging requirements:
|
||||
- append entry to docs/ai-worklog.md
|
||||
- append entry to docs/ai-prompts.md
|
||||
- include Date, Task ID, Agent, Scope
|
||||
- use Task ID: ${TASK_ID}
|
||||
- maintain chronological Task ID order in ai-worklog.md, ai-prompts.md and ai-execution-log.md
|
||||
- append entry to docs/ai-execution-log.md
|
||||
|
||||
Execution Logging requirements:
|
||||
- append entry to docs/ai-execution-log.md
|
||||
- include:
|
||||
- full prompt
|
||||
- agent todos
|
||||
- final execution report
|
||||
- use the standard execution log format defined in docs/ai-execution-log.md
|
||||
|
||||
Git requirements:
|
||||
- do NOT commit or push unless explicitly requested
|
||||
|
||||
Expected Report:
|
||||
- files changed
|
||||
- behavior changes
|
||||
- validation results
|
||||
EOF
|
||||
@@ -5,5 +5,5 @@ from dataclasses import dataclass
|
||||
class AppConfig:
|
||||
"""Application configuration."""
|
||||
|
||||
app_name: str = "Python AI Dev Template"
|
||||
app_name: str = "cd-browser"
|
||||
debug: bool = False
|
||||
|
||||
@@ -13,28 +13,61 @@ class DirectoryEntry:
|
||||
has_children: bool
|
||||
|
||||
|
||||
def list_directories(path: Path) -> list[DirectoryEntry]:
|
||||
def list_directories(path: Path, show_hidden: bool = False) -> list[DirectoryEntry]:
|
||||
"""Return direct child directories sorted by name."""
|
||||
|
||||
directory_path = path.expanduser().resolve()
|
||||
entries: list[DirectoryEntry] = []
|
||||
|
||||
for child in directory_path.iterdir():
|
||||
if child.is_dir():
|
||||
entries.append(
|
||||
DirectoryEntry(
|
||||
name=child.name,
|
||||
path=child,
|
||||
has_children=has_subdirectories(child),
|
||||
)
|
||||
try:
|
||||
children = list(directory_path.iterdir())
|
||||
except PermissionError:
|
||||
return []
|
||||
|
||||
for child in children:
|
||||
try:
|
||||
is_directory = child.is_dir()
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if not is_directory:
|
||||
continue
|
||||
if not show_hidden and child.name.startswith("."):
|
||||
continue
|
||||
|
||||
entries.append(
|
||||
DirectoryEntry(
|
||||
name=child.name,
|
||||
path=child,
|
||||
has_children=has_subdirectories(child, show_hidden=show_hidden),
|
||||
)
|
||||
)
|
||||
|
||||
return sorted(entries, key=lambda entry: entry.name.casefold())
|
||||
|
||||
|
||||
def has_subdirectories(path: Path) -> bool:
|
||||
def has_subdirectories(path: Path, show_hidden: bool = False) -> bool:
|
||||
"""Return whether the directory contains at least one subdirectory."""
|
||||
|
||||
directory_path = path.expanduser().resolve()
|
||||
|
||||
return any(child.is_dir() for child in directory_path.iterdir())
|
||||
try:
|
||||
children = list(directory_path.iterdir())
|
||||
except PermissionError:
|
||||
return False
|
||||
|
||||
for child in children:
|
||||
try:
|
||||
is_directory = child.is_dir()
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if not is_directory:
|
||||
continue
|
||||
|
||||
if not show_hidden and child.name.startswith("."):
|
||||
continue
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -28,6 +28,7 @@ class Navigator:
|
||||
current_path: Path = field(init=False)
|
||||
selected_index: int = field(init=False, default=0)
|
||||
_expanded_paths: set[Path] = field(init=False, default_factory=set)
|
||||
show_hidden: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.current_path = self.start_path.expanduser().resolve()
|
||||
@@ -144,6 +145,13 @@ class Navigator:
|
||||
self._set_current_path(path)
|
||||
return path
|
||||
|
||||
def toggle_hidden(self) -> None:
|
||||
"""Toggle showing/hiding hidden directory entries."""
|
||||
|
||||
self.show_hidden = not self.show_hidden
|
||||
self.selected_index = 0
|
||||
self._expanded_paths.clear()
|
||||
|
||||
def select_history_entry(self, index: int) -> Path:
|
||||
"""Jump to a directory stored in session history."""
|
||||
|
||||
@@ -154,7 +162,7 @@ class Navigator:
|
||||
def _build_entries(self, path: Path, depth: int) -> list[VisibleEntry]:
|
||||
entries: list[VisibleEntry] = []
|
||||
|
||||
for directory in list_directories(path):
|
||||
for directory in list_directories(path, show_hidden=self.show_hidden):
|
||||
is_expanded = directory.path in self._expanded_paths
|
||||
entries.append(
|
||||
VisibleEntry(
|
||||
|
||||
235
src/app/ui.py
235
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,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,53 @@ 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()
|
||||
|
||||
path_text = str(navigator.current_path)
|
||||
if self._history_mode:
|
||||
self._render_history_mode(stdscr, navigator, width=width, height=height)
|
||||
stdscr.refresh()
|
||||
return
|
||||
|
||||
hidden_status = "on" if navigator.show_hidden else "off"
|
||||
path_text = f"{navigator.current_path} [hidden: {hidden_status}]"
|
||||
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 +170,138 @@ 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 == ord("."):
|
||||
navigator.toggle_hidden()
|
||||
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]]:
|
||||
|
||||
84
src/cd_browser_post_install.py
Normal file
84
src/cd_browser_post_install.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Post-install script for cd-browser to guide users on shell integration."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _profile_for_shell(shell: str) -> str:
|
||||
if shell == "zsh":
|
||||
return "~/.zshrc"
|
||||
|
||||
return "~/.bashrc"
|
||||
|
||||
|
||||
def _is_cd_function_present(profile_path: str) -> bool:
|
||||
if not os.path.exists(profile_path):
|
||||
return False
|
||||
|
||||
with open(profile_path, encoding="utf-8") as profile_file:
|
||||
return "cd_() {" in profile_file.read()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Print shell integration instructions after installation."""
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 cd-browser installed successfully!")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(
|
||||
"To enable the 'cd_' command, you need to add a function to your shell profile."
|
||||
)
|
||||
print()
|
||||
|
||||
shell = os.environ.get("SHELL", "").split("/")[-1]
|
||||
profile = _profile_for_shell(shell)
|
||||
|
||||
cd_function = """cd_() {
|
||||
local target
|
||||
target="$(cd_browser)" || return
|
||||
if [ -n "$target" ] && [ -d "$target" ]; then
|
||||
cd "$target"
|
||||
fi
|
||||
}"""
|
||||
|
||||
print(f"Copy this to the end of {profile}:")
|
||||
print()
|
||||
print(cd_function)
|
||||
print()
|
||||
|
||||
profile_path = os.path.expanduser(profile)
|
||||
|
||||
try:
|
||||
response = (
|
||||
input("Do you want me to add it automatically? (y/n): ").strip().lower()
|
||||
)
|
||||
if response == "y":
|
||||
if _is_cd_function_present(profile_path):
|
||||
print(f"\nℹ️ cd_() already exists in {profile}, no changes made.")
|
||||
else:
|
||||
with open(profile_path, "a", encoding="utf-8") as f:
|
||||
f.write("\n" + cd_function + "\n")
|
||||
print(f"\n✅ Added to {profile}")
|
||||
print(f"Run 'source {profile}' or restart your terminal to activate cd_.")
|
||||
else:
|
||||
print(f"\nAdd the function to {profile} manually.")
|
||||
print(f"Then run 'source {profile}' or restart terminal.")
|
||||
except EOFError:
|
||||
print(
|
||||
f"\nNo interactive input detected. Add the function to {profile} manually."
|
||||
)
|
||||
print(f"Then run 'source {profile}' or restart terminal.")
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nSetup cancelled. You can add the function manually later.")
|
||||
sys.exit(0)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(
|
||||
f"For uninstall: after 'pip uninstall cd-browser', remove cd_() from {profile}."
|
||||
)
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,9 +4,10 @@ import pytest
|
||||
|
||||
from app.cli import run_cli
|
||||
from app.navigator import Navigator
|
||||
from app.ui import TerminalUI
|
||||
|
||||
|
||||
class StubUI:
|
||||
class StubUI(TerminalUI):
|
||||
def __init__(self, result: Path) -> None:
|
||||
self.result = result
|
||||
self.navigator: Navigator | None = None
|
||||
|
||||
@@ -44,3 +44,33 @@ def test_list_directories_raises_for_missing_directory(tmp_path: Path) -> None:
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
list_directories(missing_path)
|
||||
|
||||
|
||||
def test_list_directories_skips_permission_errors_for_children(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
allowed = root / "allowed"
|
||||
blocked = root / "blocked"
|
||||
allowed.mkdir()
|
||||
blocked.mkdir()
|
||||
|
||||
blocked.chmod(0)
|
||||
try:
|
||||
entries = list_directories(root)
|
||||
finally:
|
||||
blocked.chmod(0o700)
|
||||
|
||||
assert any(entry.name == "allowed" for entry in entries)
|
||||
|
||||
|
||||
def test_has_subdirectories_returns_false_on_permission_error(tmp_path: Path) -> None:
|
||||
blocked = tmp_path / "blocked"
|
||||
blocked.mkdir()
|
||||
|
||||
blocked.chmod(0)
|
||||
try:
|
||||
result = has_subdirectories(blocked)
|
||||
finally:
|
||||
blocked.chmod(0o700)
|
||||
|
||||
assert result is False
|
||||
|
||||
@@ -25,6 +25,31 @@ def test_navigator_initial_state_includes_parent_entry(tmp_path: Path) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_toggle_hidden_directories(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
root.mkdir()
|
||||
visible = root / "visible"
|
||||
visible.mkdir()
|
||||
hidden = root / ".hidden"
|
||||
hidden.mkdir()
|
||||
|
||||
navigator = Navigator(root)
|
||||
|
||||
assert [entry.name for entry in navigator.visible_entries] == ["..", "visible"]
|
||||
|
||||
navigator.toggle_hidden()
|
||||
assert navigator.show_hidden is True
|
||||
assert [entry.name for entry in navigator.visible_entries] == [
|
||||
"..",
|
||||
".hidden",
|
||||
"visible",
|
||||
]
|
||||
|
||||
navigator.toggle_hidden()
|
||||
assert navigator.show_hidden is False
|
||||
assert [entry.name for entry in navigator.visible_entries] == ["..", "visible"]
|
||||
|
||||
|
||||
def test_expand_selected_directory_reveals_nested_entries(tmp_path: Path) -> None:
|
||||
current = tmp_path / "workspace"
|
||||
current.mkdir()
|
||||
|
||||
47
tests/test_post_install.py
Normal file
47
tests/test_post_install.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cd_browser_post_install import main
|
||||
|
||||
CD_FUNCTION_SNIPPET = "cd_() {"
|
||||
|
||||
|
||||
def test_post_install_handles_eof_without_crashing(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
monkeypatch.setenv("SHELL", "/bin/zsh")
|
||||
monkeypatch.setattr(
|
||||
"builtins.input", lambda _prompt: (_ for _ in ()).throw(EOFError)
|
||||
)
|
||||
|
||||
main()
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "No interactive input detected" in output
|
||||
assert "source ~/.zshrc" in output
|
||||
|
||||
|
||||
def test_post_install_adds_cd_function_once(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
monkeypatch.setenv("SHELL", "/bin/zsh")
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
|
||||
responses = iter(["y", "y"])
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt: next(responses))
|
||||
|
||||
main()
|
||||
main()
|
||||
|
||||
profile_path = tmp_path / ".zshrc"
|
||||
content = profile_path.read_text(encoding="utf-8")
|
||||
|
||||
assert content.count(CD_FUNCTION_SNIPPET) == 1
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "already exists" in output
|
||||
assert "source ~/.zshrc" in output
|
||||
156
tests/test_ui.py
156
tests/test_ui.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user