Add Clide project structure and initial implementation

Set up the TUI IDE wrapper for Claude Code CLI with:
- Core app structure using Textual framework
- Panel architecture (sidebar, workspace, claude, context)
- Theme system with 22 built-in themes (Summer Night default)
- Pydantic models for configuration and data
- Makefile for development commands
- Project documentation and specs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 21:04:44 +01:00
co-authored by Claude Opus 4.5
parent 12825e15e2
commit c5f8b2e615
103 changed files with 11660 additions and 11 deletions
+6
View File
@@ -0,0 +1,6 @@
"""Test harnesses for Clide testing."""
from tests.harnesses.app_harness import AppHarness
from tests.harnesses.controller_harness import ControllerHarness
__all__ = ["AppHarness", "ControllerHarness"]
+84
View File
@@ -0,0 +1,84 @@
"""Full application test harness for Clide."""
from pathlib import Path
from typing import Optional
from textual.pilot import Pilot
from clide.app import ClideApp
from clide.extensions.manager import ExtensionManager
from clide.models.config import ClideSettings
class AppHarness:
"""Test harness for running the full Clide application.
Provides a controlled environment for integration testing with
mocked services and isolated file systems.
Usage:
harness = AppHarness(workdir=tmp_path)
app, pilot = await harness.start()
await pilot.press("ctrl+q")
await harness.stop()
"""
def __init__(
self,
workdir: Path,
settings: Optional[ClideSettings] = None,
extension_manager: Optional[ExtensionManager] = None,
) -> None:
self.workdir = workdir
self.settings = settings or ClideSettings()
self.extension_manager = extension_manager or ExtensionManager()
self._app: Optional[ClideApp] = None
self._pilot: Optional[Pilot] = None
async def start(self) -> tuple[ClideApp, Pilot]:
"""Start the application and return app and pilot for testing.
Returns:
Tuple of (ClideApp instance, Pilot for simulating input)
"""
self._app = ClideApp(workdir=self.workdir)
# Inject test dependencies
self._app.extension_manager = self.extension_manager
# Start app in test mode
async with self._app.run_test() as pilot:
self._pilot = pilot
return self._app, pilot
async def stop(self) -> None:
"""Clean shutdown of the application."""
if self._app:
await self._app.action_quit()
self._app = None
self._pilot = None
@property
def app(self) -> ClideApp:
"""Get the running app instance."""
if self._app is None:
raise RuntimeError("App not started. Call start() first.")
return self._app
@property
def pilot(self) -> Pilot:
"""Get the pilot for simulating user input."""
if self._pilot is None:
raise RuntimeError("App not started. Call start() first.")
return self._pilot
async def press_keys(self, *keys: str) -> None:
"""Simulate pressing a sequence of keys."""
await self.pilot.press(*keys)
async def click(self, selector: str) -> None:
"""Click on a widget by CSS selector."""
await self.pilot.click(selector)
async def wait_for_animation(self) -> None:
"""Wait for any running animations to complete."""
await self.pilot.pause()
+97
View File
@@ -0,0 +1,97 @@
"""Controller isolation test harness for Clide."""
from typing import TYPE_CHECKING, Any, Optional
from unittest.mock import AsyncMock, MagicMock
from textual.message import Message
if TYPE_CHECKING:
from clide.controllers.base import BaseController
class MockApp:
"""Minimal mock of a Textual App for controller testing."""
def __init__(self) -> None:
self.messages: list[Message] = []
self.post_message = MagicMock(side_effect=self._capture_message)
def _capture_message(self, message: Message) -> None:
self.messages.append(message)
def get_messages(self, message_type: Optional[type] = None) -> list[Message]:
"""Get captured messages, optionally filtered by type."""
if message_type is None:
return self.messages.copy()
return [m for m in self.messages if isinstance(m, message_type)]
def clear_messages(self) -> None:
"""Clear captured messages."""
self.messages.clear()
class ControllerHarness:
"""Test harness for isolated controller testing.
Provides a mock app environment for testing controllers without
the full Textual application overhead.
Usage:
harness = ControllerHarness()
controller = GitController(harness.mock_app)
await controller.initialize()
await controller.refresh_status()
messages = harness.get_messages()
"""
def __init__(self) -> None:
self._mock_app = MockApp()
self._controllers: list["BaseController"] = []
self._mocks: dict[str, Any] = {}
@property
def mock_app(self) -> MockApp:
"""Get the mock app for controller injection."""
return self._mock_app
def register_controller(self, controller: "BaseController") -> None:
"""Register a controller for lifecycle management."""
self._controllers.append(controller)
async def initialize_all(self) -> None:
"""Initialize all registered controllers."""
for controller in self._controllers:
await controller.initialize()
async def shutdown_all(self) -> None:
"""Shutdown all registered controllers."""
for controller in self._controllers:
await controller.shutdown()
def get_messages(self, message_type: Optional[type] = None) -> list[Message]:
"""Get messages posted to the mock app."""
return self._mock_app.get_messages(message_type)
def clear_messages(self) -> None:
"""Clear all captured messages."""
self._mock_app.clear_messages()
def add_mock(self, name: str, mock: Any) -> None:
"""Add a named mock for dependency injection.
Args:
name: Identifier for the mock
mock: Mock object or AsyncMock
"""
self._mocks[name] = mock
def get_mock(self, name: str) -> Any:
"""Retrieve a named mock."""
return self._mocks.get(name)
def create_async_mock(self, return_value: Any = None) -> AsyncMock:
"""Create an AsyncMock with optional return value."""
mock = AsyncMock()
if return_value is not None:
mock.return_value = return_value
return mock