move python clide to legacy/

Clide is being rebuilt as a Flutter desktop app. The Python Textual
implementation moves wholesale into legacy/ rather than being deleted:
its pane model, panel set, git skills, and panel communication design
are real thought that should remain readable next to the new code
while the rebuild finds its shape. Git's rename tracking preserves
history, so `git log -- legacy/` still works.

The Flutter rebuild lives at the repo root alongside a Go sidecar
(the architecture claudian was heading toward, which folds into
clide as a core component rather than a separate plugin project).
Bootstrap of the new shape lands in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 20:30:51 +02:00
co-authored by Claude Opus 4.7
parent 30b8cf7db9
commit a355751437
163 changed files with 0 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
"""Extension system for Clide using pluggy."""
from clide.extensions.hookspecs import ClideHookSpec, hookimpl, hookspec
from clide.extensions.manager import ExtensionManager
__all__ = ["ClideHookSpec", "ExtensionManager", "hookimpl", "hookspec"]
@@ -0,0 +1 @@
"""Built-in extensions for Clide."""
+116
View File
@@ -0,0 +1,116 @@
"""Pluggy hook specifications for Clide extensions."""
from pathlib import Path
from typing import TYPE_CHECKING, Any
import pluggy
if TYPE_CHECKING:
from textual.app import App
from textual.widget import Widget
from clide.services.file_watcher import FileEvent
hookspec = pluggy.HookspecMarker("clide")
hookimpl = pluggy.HookimplMarker("clide")
class ClideHookSpec:
"""Hook specifications for Clide extensions.
Extensions implement these hooks to extend functionality.
"""
@hookspec
def clide_register_panel(self) -> dict[str, Any] | None:
"""Register a custom panel for the UI.
Returns:
Dictionary with panel configuration:
- name: Panel identifier
- widget: Widget class to instantiate
- position: "left", "right", or "bottom"
- keybinding: Optional keyboard shortcut
"""
@hookspec
def clide_register_commands(self) -> list[dict[str, Any]] | None:
"""Register custom commands for the command palette.
Returns:
List of command dictionaries:
- name: Command display name
- callback: Async callable to execute
- description: Help text
"""
@hookspec
def clide_on_app_startup(self, app: "App[object]") -> None:
"""Called when the application starts.
Args:
app: The Clide application instance
"""
@hookspec
def clide_on_app_shutdown(self, app: "App[object]") -> None:
"""Called when the application is shutting down.
Args:
app: The Clide application instance
"""
@hookspec
def clide_on_file_open(self, path: str) -> None:
"""Called when a file is opened in the file browser.
Args:
path: Absolute path to the opened file
"""
@hookspec
def clide_modify_widget(self, widget: "Widget") -> "Widget":
"""Modify a widget before it's mounted.
Args:
widget: The widget about to be mounted
Returns:
The modified (or original) widget
"""
@hookspec
def clide_on_file_changed(self, event: "FileEvent") -> None:
"""Called when a file is created, modified, deleted, or moved.
Extensions can use this to:
- Refresh TODO scanning
- Re-run linters
- Update Jira issue links
- Trigger custom actions
Args:
event: The file event with path, type, and timestamp
"""
@hookspec
def clide_on_file_saved(self, path: Path) -> None:
"""Called after a file is saved by the editor.
More specific than file_changed - only for user saves.
Args:
path: Absolute path to the saved file
"""
@hookspec
def clide_on_claude_event(self, event_type: str, data: dict[str, Any]) -> None:
"""Called when Claude Code performs an action.
Extensions can use this to react to Claude's actions,
such as opening files, making edits, or running commands.
Args:
event_type: Type of event (e.g., "file_read", "file_edit", "tool_use")
data: Event-specific data (e.g., {"path": "/path/to/file"})
"""
+108
View File
@@ -0,0 +1,108 @@
"""Extension manager for loading and managing Clide extensions."""
from importlib.metadata import entry_points
from pathlib import Path
from typing import TYPE_CHECKING, Any
import pluggy
from clide.extensions.hookspecs import ClideHookSpec
if TYPE_CHECKING:
from textual.app import App
from clide.services.file_watcher import FileEvent
EXTENSION_NAMESPACE = "clide.extensions"
class ExtensionManager:
"""Manages loading and lifecycle of Clide extensions."""
def __init__(self) -> None:
self._pm = pluggy.PluginManager("clide")
self._pm.add_hookspecs(ClideHookSpec)
self._loaded: list[str] = []
@property
def hook(self) -> pluggy.HookRelay:
"""Access the hook relay for calling hooks."""
return self._pm.hook
def load_extensions(self) -> None:
"""Load all extensions from entry points."""
eps = entry_points(group=EXTENSION_NAMESPACE)
for ep in eps:
try:
plugin = ep.load()
self._pm.register(plugin, name=ep.name)
self._loaded.append(ep.name)
except Exception as e:
# Log but don't crash on extension load failure
print(f"Failed to load extension {ep.name}: {e}")
def register_plugin(self, plugin: object, name: str) -> None:
"""Manually register a plugin instance.
Args:
plugin: Plugin object with hookimpl methods
name: Unique name for the plugin
"""
self._pm.register(plugin, name=name)
self._loaded.append(name)
def unregister_plugin(self, name: str) -> None:
"""Unregister a plugin by name.
Args:
name: Name of the plugin to unregister
"""
plugin = self._pm.get_plugin(name)
if plugin:
self._pm.unregister(plugin)
self._loaded.remove(name)
def list_extensions(self) -> list[str]:
"""Get list of loaded extension names."""
return self._loaded.copy()
async def trigger_app_startup(self, app: "App[object]") -> None:
"""Trigger startup hooks for all extensions.
Args:
app: The Clide application instance
"""
self.hook.clide_on_app_startup(app=app)
async def trigger_app_shutdown(self, app: "App[object]") -> None:
"""Trigger shutdown hooks for all extensions.
Args:
app: The Clide application instance
"""
self.hook.clide_on_app_shutdown(app=app)
def trigger_file_changed(self, event: "FileEvent") -> None:
"""Trigger file change hooks for all extensions.
Args:
event: The file event with path, type, and timestamp
"""
self.hook.clide_on_file_changed(event=event)
def trigger_file_saved(self, path: Path) -> None:
"""Trigger file saved hooks for all extensions.
Args:
path: Path to the saved file
"""
self.hook.clide_on_file_saved(path=path)
def trigger_claude_event(self, event_type: str, data: dict[str, Any]) -> None:
"""Trigger Claude event hooks for all extensions.
Args:
event_type: Type of event (e.g., "file_read", "file_edit")
data: Event-specific data
"""
self.hook.clide_on_claude_event(event_type=event_type, data=data)