docs: update documentation and fix lint issues for v1.0.0

Documentation:
- Rewrite README.md with current features and git operations
- Rewrite docs/ARCHITECTURE.md with layered architecture details
- Rewrite docs/tui-ide-spec.md with Alt-key shortcuts
- Add docs/code-organization.md for component architecture
- Add docs/user-manual.md for end users
- Update TODO.md to mark completed items

Code fixes:
- Fix undefined 'event' variable in diff_pane.py (was _event)
- Use ternary operator in editor.py save_file method
- Clean up imports in claude_events.py and syntax_service.py
- Auto-fix import sorting across multiple files

Config:
- Add snapshot report path to pyproject.toml pytest options
- Exclude clide/vendor from ruff linting
- Ignore TCH002/TCH003 type-checking import rules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-02-01 22:02:00 +01:00
co-authored by Claude Opus 4.5
parent 2d2e5f5648
commit 8b1a84e7e2
20 changed files with 1574 additions and 834 deletions
+273 -256
View File
@@ -1,11 +1,11 @@
# Clide Architecture Documentation
# Clide Architecture
Comprehensive documentation of architecture patterns, best practices, and implementation guidelines.
Technical documentation covering Clide's architecture, the frameworks it builds on, and implementation patterns.
## Table of Contents
- [Application Architecture](#application-architecture)
- [Textual TUI Framework](#textual-tui-framework)
- [Typer CLI Framework](#typer-cli-framework)
- [Pydantic Data Validation](#pydantic-data-validation)
- [Extension System](#extension-system)
- [Testing Strategy](#testing-strategy)
@@ -13,199 +13,238 @@ Comprehensive documentation of architecture patterns, best practices, and implem
---
## Application Architecture
Clide follows a layered architecture with clear separation between UI, business logic, and data.
### Layer Overview
```
┌─────────────────────────────────────────────────────────┐
│ ClideApp (app.py) │
│ Main application, layout, keybindings │
├─────────────────────────────────────────────────────────┤
│ Widgets Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Panels │ │ Components │ │ Themes │ │
│ │ (layout) │ │ (reusable) │ │ (styling) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Controllers Layer │
│ Business logic, state management │
├─────────────────────────────────────────────────────────┤
│ Services Layer │
│ Git, files, scanning, settings, skills │
├─────────────────────────────────────────────────────────┤
│ Models Layer │
│ Pydantic data structures │
└─────────────────────────────────────────────────────────┘
```
### Data Flow
```
User Action (click, keypress)
Widget Event
Message Bubbles Up
App Event Handler
Controller Method
Service Call
Return Data/Status
Update UI State
Reactive UI Update
```
### Key Patterns
**Message-based communication** — Widgets emit messages that bubble up. Parent widgets or the app handle messages and coordinate responses.
**Reactive properties** — UI state uses Textual's `reactive` type. Changes automatically trigger `watch_*` methods.
**Background workers** — Long operations use `@work(thread=True)` to avoid blocking the UI.
**State preservation** — Hiding panels uses `display: none`, never destroying widgets. All state persists.
For detailed code organization, see [Code Organization](code-organization.md).
---
## Textual TUI Framework
Textual models TUIs as a reactive tree of widgets, similar to React's component tree but grid-based on character cells.
Textual provides the foundation for Clide's terminal UI.
### Key Concepts
### Core Concepts
**Widgets and Containers**
- Widgets are the building blocks of the UI
- Containers are widgets that hold other widgets
- Default layout stacks widgets vertically from top of screen
**Widgets** — Building blocks of the UI. Everything visible is a widget.
**Reactive Programming**
- State changes trigger automatic UI updates
- No manual refresh loops needed
- Use reactive attributes for state management
**Containers** — Widgets that hold other widgets (Vertical, Horizontal, Container).
**Event-Driven Model**
- Define callbacks for key presses, mouse clicks, timer ticks
- Actions are functions callable via keystroke or text link
**Reactive Programming** — State changes trigger automatic UI updates.
### Best Practices
**CSS Styling** — Layout and appearance defined in CSS, similar to web development.
1. **Use Immutable Objects**
- Prefer tuples, NamedTuples, or frozen dataclasses
- Easier to reason about, cache, and test
- Enables side-effect-free code
### Layout System
2. **Separate Styles**
- Keep CSS in `.tcss` files, not inline
- Python code stays clean and focused on logic
Clide uses CSS Grid for the main layout:
3. **Async-First**
- Textual is async under the hood
- Use `async`/`await` for I/O operations
- Can integrate with async libraries if needed
### Layout Management
```python
# Grid layout example
CSS = """
```css
Screen {
layout: grid;
grid-size: 3 1;
grid-columns: 1fr 2fr 1fr;
grid-columns: 20% 1fr 25%;
}
"""
```
Panels use percentage widths with minimum sizes:
```css
#panel-sidebar {
width: 20%;
min-width: 25;
}
```
### Widget Lifecycle
```python
class MyWidget(Widget):
def __init__(self):
super().__init__()
# Initialize instance variables
def compose(self) -> ComposeResult:
# Yield child widgets
yield Label("Hello")
def on_mount(self) -> None:
# Called after widget is added to DOM
# Safe to query other widgets here
def on_unmount(self) -> None:
# Cleanup when removed
```
### Event Handling
Events bubble up through the widget tree:
```python
# Define a message
class FileSelected(Message):
def __init__(self, path: Path):
self.path = path
super().__init__()
# Emit the message
self.post_message(self.FileSelected(path))
# Handle in parent (naming convention: on_<widget>_<message>)
def on_files_view_file_selected(self, event: FilesView.FileSelected):
self.open_file(event.path)
```
### Background Tasks
Use `@work` for operations that shouldn't block the UI:
```python
from textual import work
@work(thread=True)
def fetch_data(self) -> dict:
"""Runs in thread pool."""
result = expensive_operation()
return result
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
if event.state == WorkerState.SUCCESS:
self.update_ui(event.worker.result)
```
### References
- [Textual Documentation](https://textual.textualize.io/)
- [Textual Tutorial](https://textual.textualize.io/tutorial/)
- [Real Python Textual Guide](https://realpython.com/python-textual/)
- [Textual GitHub](https://github.com/Textualize/textual)
---
## Typer CLI Framework
Typer is built on Click with Python type hints for automatic argument parsing.
### Project Structure Pattern
```
app/
├── __init__.py
├── main.py # Root Typer app
├── commands/ # Subcommand modules
│ ├── users.py
│ └── tasks.py
└── helpers/ # Shared utilities
└── validate.py
```
### Best Practices
1. **Organize Commands**
- Use `add_typer()` to group commands
- Avoid giant files with dozens of commands
- Each command function should orchestrate, not contain all logic
2. **Entry Point Support**
- Add `__main__.py` for `python -m` support
- Define entry points in pyproject.toml for CLI scripts
3. **Standard Exit Codes**
- `0` for success
- Non-zero for errors
- Crucial for CI/CD integration
4. **Type Hints for Validation**
- Use Enum for dropdown-style restrictions
- Type hints provide editor autocompletion
### Subcommand Example
```python
# commands/users.py
import typer
app = typer.Typer()
@app.command()
def create(name: str):
"""Create a new user."""
...
# main.py
from commands import users
main_app = typer.Typer()
main_app.add_typer(users.app, name="users")
```
### References
- [Typer Documentation](https://typer.tiangolo.com/)
- [Typer Subcommands](https://typer.tiangolo.com/tutorial/subcommands/)
- [Building a Package](https://typer.tiangolo.com/tutorial/package/)
- [Textual Widgets](https://textual.textualize.io/widgets/)
- [Textual CSS](https://textual.textualize.io/guide/CSS/)
---
## Pydantic Data Validation
Pydantic v2 with strict mode ensures type safety and validation.
All data models use Pydantic v2 with strict mode.
### Strict Mode Configuration
### Model Configuration
```python
from pydantic import BaseModel, ConfigDict
class MyModel(BaseModel):
class GitChange(BaseModel):
model_config = ConfigDict(strict=True, frozen=True)
name: str
count: int # Will reject "123" string
path: str
status: Literal["added", "modified", "deleted"]
staged: bool
```
**strict=True** — No type coercion. `"123"` won't become `123`.
**frozen=True** — Immutable instances. Enables hashing for use as dict keys.
### Settings Management
Settings have moved to `pydantic-settings` package:
Application settings use `pydantic-settings`:
```python
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
class ClideSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
env_prefix="CLIDE_",
env_file=".env",
env_nested_delimiter="__",
)
database_url: str
debug: bool = False
theme: str = "summer-night"
jira_enabled: bool = False
```
### Best Practices
1. **Use `frozen=True` for Immutability**
- Prevents accidental mutation
- Enables hashing for use as dict keys
2. **Explicit Strict Types**
- `StrictInt`, `StrictStr` for field-level strictness
- Or use `model_config` for model-wide strictness
3. **Validation vs Parsing**
- Strict mode rejects type coercion
- JSON parsing allows some conversion (ISO8601 → datetime)
Settings load from (in priority order):
1. Environment variables (`CLIDE_THEME=dracula`)
2. `.env` file
3. Default values
### References
- [Pydantic v2 Documentation](https://docs.pydantic.dev/latest/)
- [Pydantic Documentation](https://docs.pydantic.dev/latest/)
- [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
- [Pydantic Configuration](https://docs.pydantic.dev/latest/api/config/)
- [Migration Guide](https://docs.pydantic.dev/latest/migration/)
---
## Extension System
The plugin system uses Pluggy for hook-based extensibility.
Clide uses Pluggy for hook-based extensibility.
### Pluggy Concepts
### Hook Specifications
1. **Hook Specifications** - Define the interface extensions implement
2. **Hook Implementations** - Extension code implementing hooks
3. **Plugin Manager** - Discovers and calls implementations
### Architecture
Hooks define extension points:
```python
# hookspecs.py - Define hooks
# clide/extensions/hookspecs.py
import pluggy
hookspec = pluggy.HookspecMarker("clide")
@@ -213,18 +252,36 @@ hookimpl = pluggy.HookimplMarker("clide")
class ClideHookSpec:
@hookspec
def register_panel(self) -> dict: ...
def clide_startup(self, app: App) -> None:
"""Called when the app starts."""
@hookspec
def clide_on_file_changed(self, event: FileEvent) -> None:
"""Called when a file changes."""
```
### Implementing Hooks
Extensions implement hooks with the `@hookimpl` decorator:
```python
from clide.extensions import hookimpl
# extension.py - Implement hooks
class MyExtension:
@hookimpl
def register_panel(self) -> dict:
return {"name": "custom", "widget": CustomWidget}
def clide_startup(self, app: App) -> None:
app.notify("Extension loaded!")
@hookimpl
def clide_on_file_changed(self, event: FileEvent) -> None:
if event.path.suffix == ".py":
# React to Python file changes
pass
```
### Distribution
Extensions can be distributed as packages using entry points:
Extensions can be packaged and distributed via entry points:
```toml
# pyproject.toml of extension package
@@ -232,55 +289,73 @@ Extensions can be distributed as packages using entry points:
my_extension = "my_package:MyExtension"
```
### Hook Execution Order
### Available Hooks
- Multiple implementations called in LIFO (Last In, First Out) order
- Use `hookimpl(tryfirst=True)` or `hookimpl(trylast=True)` for ordering
### Alternatives
- **Stevedore** - Better for driver/extension patterns, uses entry points
- Choose Pluggy for hook-based systems (like pytest uses)
| Hook | When Called |
|------|-------------|
| `clide_startup` | App initialization |
| `clide_shutdown` | App cleanup |
| `clide_on_file_changed` | File created/modified/deleted |
| `clide_on_file_saved` | File saved in editor |
### References
- [Pluggy Documentation](https://pluggy.readthedocs.io/)
- [Stevedore Documentation](https://docs.openstack.org/stevedore/latest/)
- [Creating Plugins with Stevedore](https://docs.openstack.org/stevedore/latest/user/tutorial/creating_plugins.html)
---
## Testing Strategy
### pytest-asyncio
### Test Organization
Configure auto mode for automatic async test discovery:
```
tests/
├── unit/ # Isolated component tests
├── integration/ # Component interaction tests
└── snapshots/ # Visual regression tests
```
### Async Testing
Configure pytest-asyncio in auto mode:
```toml
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```
### Async Test Patterns
Tests can be async without decorators:
```python
import pytest
# Auto mode - no decorator needed
async def test_async_operation():
result = await some_async_function()
assert result == expected
# Async fixtures
@pytest.fixture
async def database_connection():
conn = await create_connection()
yield conn
await conn.close()
```
### Async Mocking
### Snapshot Testing
Visual regression testing with pytest-textual-snapshot:
```python
def test_layout(snap_compare):
assert snap_compare(ClideApp(), terminal_size=(120, 40))
def test_with_interaction(snap_compare):
async def setup(pilot):
await pilot.press("tab", "enter")
assert snap_compare(ClideApp(), run_before=setup)
```
Update snapshots after intentional changes:
```bash
pytest tests/snapshots/ --snapshot-update
```
### Mocking
Use `AsyncMock` for async dependencies:
```python
from unittest.mock import AsyncMock
@@ -291,73 +366,43 @@ async def test_with_mock():
assert result["status"] == "ok"
```
### Snapshot Testing
Visual regression with pytest-textual-snapshot:
```python
def test_layout(snap_compare):
assert snap_compare("app.py", terminal_size=(120, 40))
def test_with_interaction(snap_compare):
async def setup(pilot):
await pilot.press("tab", "enter")
assert snap_compare("app.py", run_before=setup)
```
Update snapshots after intentional changes:
```bash
pytest tests/snapshots/ --snapshot-update
```
### Test Harness Pattern
Harnesses provide isolated test environments:
```python
class AppHarness:
async def start(self) -> tuple[App, Pilot]:
"""Start app with mocked dependencies."""
async def stop(self) -> None:
"""Clean shutdown."""
```
### Best Practices
1. **Always use `@pytest.mark.asyncio`** (or auto mode)
2. **Use async fixtures** for async setup/teardown
3. **Mock external services** - don't hit real APIs
4. **Choose appropriate fixture scopes** for performance
5. **Avoid blocking the event loop** in async tests
### References
- [pytest-asyncio Documentation](https://pytest-asyncio.readthedocs.io/en/latest/)
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/)
- [pytest-textual-snapshot](https://github.com/Textualize/pytest-textual-snapshot)
- [Textual Testing Guide](https://textual.textualize.io/guide/testing/)
- [pytest Fixtures](https://docs.pytest.org/en/stable/how-to/fixtures.html)
---
## Build and Distribution
### PyInstaller Limitations
### Development
**Critical: PyInstaller cannot cross-compile.**
- Build on the target OS
- Use CI/CD for multi-platform builds
```bash
make setup # Create venv, install deps
make run # Run application
make test # Run all tests
make typecheck # Run mypy
make lint # Run ruff
make format # Format code
```
### CI/CD Multi-Platform Build
### PyInstaller
Use Gitea Actions (or compatible CI) for multi-platform builds:
Build standalone executables:
```bash
pip install -e ".[build]"
pyinstaller clide.spec --clean
```
**Important**: PyInstaller cannot cross-compile. Build on each target platform.
### CI/CD
Multi-platform builds via Gitea Actions:
```yaml
# .gitea/workflows/build.yml
name: Build
on: [push, tag]
jobs:
build-linux:
runs-on: ubuntu-latest
@@ -371,45 +416,17 @@ jobs:
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -e ".[build]"
- run: pyinstaller clide.spec --clean
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -e ".[build]"
- run: pyinstaller clide.spec --clean
# ... same steps
```
### Optimization Tips
### Optimization
1. **Use `--onefile`** for single executable
2. **Apply `--strip`** to reduce binary size
3. **Use UPX compression** (460 MB → ~130 MB possible)
4. **Exclude unused modules** with `--exclude-module`
5. **Lazy imports** for large libraries
### Platform-Specific Output
- **Windows**: `.exe` or MSIX installer
- **macOS**: `.app` bundle in `.dmg`
- **Linux**: AppImage or native package
### Linux Compatibility
Build on the oldest target distro version. Newer systems may produce incompatible binaries.
- Use `--onefile` for single executable
- Apply `--strip` to reduce size
- Use UPX compression for further reduction
- Exclude unused modules with `--exclude-module`
### References
- [PyInstaller Documentation](https://pyinstaller.org/)
- [Building the Bootloader](https://pyinstaller.org/en/latest/bootloader-building.html)
- [Gitea Actions](https://docs.gitea.com/usage/actions/overview)