diff --git a/README.md b/README.md index eda74ba7..74569d0c 100644 --- a/README.md +++ b/README.md @@ -4,53 +4,135 @@ A terminal-based IDE that wraps Claude Code CLI, putting AI-assisted development ## Why Clide? -Claude Code is powerful, but switching between your terminal, editor, and project tools breaks your flow. Clide brings everything into one interface: +Claude Code is powerful, but switching between terminal, editor, and project tools breaks your flow. Clide brings everything into one interface: -- **Claude stays visible** — No more switching windows. Claude Code is always front and center. -- **Context at a glance** — File tree, git status, problems, and TODOs in dedicated panels. -- **Panels appear when needed** — Editor, diff viewer, and terminal stay hidden until you need them. -- **Familiar keybindings** — VSCode-inspired shortcuts that don't interfere with your input. +- **Claude stays visible** — Claude Code runs in the center panel, always accessible +- **Context at a glance** — File tree, git status, problems, and TODOs in dedicated panels +- **Panels appear when needed** — Editor, diff viewer, and terminal stay hidden until you need them +- **Git integration** — Commit, stash, pull, and push via Claude with built-in skills +- **22 themes** — From summer-night to dracula, with custom theme support -## Layout +## Screenshot ``` ┌─────────────────┬─────────────────────────┬──────────────────┐ │ Sidebar │ Workspace │ Context │ │ │ [Editor][Diff][Terminal]│ │ -│ [Files][Git] │ (appears when needed) │ [Problems][TODOs]│ -│ [Graph] ├─────────────────────────┤ [Jira] │ +│ [Files][Git] │ (appears when needed) │ [Jira][TODOs] │ +│ [Tree] ├─────────────────────────┤ [Problems] │ │ │ │ │ │ │ Claude │ │ │ │ (always visible) │ │ │ │ │ │ ├─────────────────┤ ├──────────────────┤ -│ ⎇ main ▾ │ │ [⚠ 3][✓12][Jira]│ +│ ⎇ main ▾ │ │ │ +│ staged: 2 │ │ │ └─────────────────┴─────────────────────────┴──────────────────┘ ``` ## Features -**Left Sidebar** -- File explorer with project tree -- Git panel showing staged/unstaged changes -- Visual branch graph -- Quick branch switching +### Left Sidebar +- **Files** — Project file tree with syntax-aware icons +- **Git** — Staged/unstaged changes with action buttons +- **Tree** — Visual branch graph +- **Branch status** — Current branch with quick switcher -**Center Workspace** -- Claude Code integration (primary focus) -- Tabbed editor with syntax highlighting -- Side-by-side diff viewer for proposed changes -- Integrated terminal +### Center +- **Claude Code** — Full PTY terminal integration, always visible +- **Editor** — Syntax highlighting via tree-sitter +- **Diff** — Side-by-side diff viewer +- **Terminal** — Command execution -**Right Context Panel** -- Problems view (linter errors/warnings) -- TODOs extracted from codebase -- Jira/Confluence integration +### Right Context +- **Jira** — Issue display via CLI integration +- **TODOs** — Code comments and TODO.md items +- **Problems** — Linter errors and warnings -**Responsive Design** -- Works on 13" laptops to widescreen monitors -- Compact mode hides sidebars for focused work -- Panels preserve state when hidden +### Git Operations + +Click buttons in the Git panel to delegate operations to Claude: + +| Button | Skill | What Claude Does | +|--------|-------|------------------| +| Commit | `/commit` | Reviews changes, writes commit message | +| Stash | `/stash` | Stashes working changes | +| Pull | `/pull` | Pulls with rebase, helps resolve conflicts | +| Push | `/push` | Pushes to remote, sets upstream if needed | + +Skills are installed automatically to your project's `.claude/skills/` directory. + +## Installation + +### Requirements + +- Python 3.12+ +- Git +- Claude Code CLI (installed and authenticated) + +### Setup + +```bash +git clone +cd clide +make setup +make run +``` + +Or install directly: + +```bash +pip install -e . +clide +``` + +## Keybindings + +All shortcuts use `Alt` to avoid conflicts with Claude Code input. + +| Action | Binding | +|--------|---------| +| Toggle left sidebar | `Alt+B` | +| Toggle right sidebar | `Alt+Shift+B` | +| Toggle terminal | `` Alt+` `` | +| Focus Claude | `Alt+1` | +| Focus Editor | `Alt+2` | +| Focus Terminal | `Alt+3` | +| Toggle compact mode | `Alt+C` | +| Select theme | `Alt+T` | +| Quit | `Alt+Q` | + +## Themes + +22 built-in themes. Press `Alt+T` to switch. + +| Category | Themes | +|----------|--------| +| Core | summer-night (default), summer-day | +| Popular | one-dark, one-dark-pro, one-light, dracula, nord, gruvbox-dark, gruvbox-light | +| Seasonal | winter-is-coming, monokai-winter, fall, dark-autumn | +| Special | all-hallows-eve, halloween, christmas, santa-baby | +| Hacker | pro-hacker, hacker-style | + +Create custom themes in `~/.clide/themes/` as TOML files. + +## Configuration + +Settings stored in `~/.clide/settings.json`: + +```json +{ + "theme": "summer-night", + "compact_mode": false, + "jira_enabled": false +} +``` + +Override with environment variables: + +```bash +CLIDE_THEME=dracula clide +``` ## Tech Stack @@ -61,37 +143,25 @@ Claude Code is powerful, but switching between your terminal, editor, and projec | CLI | Typer | | Data Validation | Pydantic v2 | | Extensions | Pluggy | +| Syntax Highlighting | tree-sitter | -## Getting Started +## Development ```bash -# Clone and setup -git clone -cd clide -make setup - -# Run -make run +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 ``` -## Keybindings - -| Action | Binding | -|--------|---------| -| Toggle left sidebar | `Alt+B` | -| Toggle right sidebar | `Alt+Shift+B` | -| Toggle terminal | `` Alt+` `` | -| Focus Claude | `Alt+1` | -| Focus Editor | `Alt+2` | -| Focus Terminal | `Alt+3` | -| Command palette | `Alt+P` | -| Quick open file | `Alt+O` | -| Toggle compact mode | `Alt+C` | - ## Documentation -- [Full UI/UX Specification](docs/tui-ide-spec.md) -- [Architecture Guide](docs/ARCHITECTURE.md) +- [User Manual](docs/user-manual.md) — How to use Clide +- [UI/UX Specification](docs/tui-ide-spec.md) — Design decisions +- [Architecture](docs/ARCHITECTURE.md) — Technical overview +- [Code Organization](docs/code-organization.md) — Project structure ## License diff --git a/TODO.md b/TODO.md index ccea40be..749eaff7 100644 --- a/TODO.md +++ b/TODO.md @@ -44,23 +44,23 @@ Long-term open items for Clide development. ### Git Integration - [ ] Stage/unstage files from Git tab - [ ] Discard changes context menu -- [ ] Git graph visualization (Tree tab) -- [ ] Branch popout with checkout/new branch actions +- [x] Git graph visualization (Tree tab) +- [x] Branch popout with checkout/new branch actions ## Context Panel (Right Sidebar) ### Problems View - [ ] Linter integration (ruff, eslint, etc.) -- [ ] Click to navigate to file:line -- [ ] Reactive problem count badge +- [x] Click to navigate to file:line +- [x] Reactive problem count badge ### TODOs View -- [ ] Scan for TODO/FIXME/HACK/XXX comments -- [ ] Click to navigate to file:line -- [ ] Reactive count badge +- [x] Scan for TODO/FIXME/HACK/XXX comments +- [x] Click to navigate to file:line +- [x] Reactive count badge ### Jira View -- [ ] Render Jira CLI markdown output +- [x] Render Jira CLI markdown output - [ ] Auto-refresh on panel focus - [ ] Configurable refresh interval @@ -69,7 +69,7 @@ Long-term open items for Clide development. ### Responsiveness - [ ] CSS breakpoints for different terminal widths - [ ] Auto-hide sidebars on narrow terminals (<100 cols) -- [ ] Compact mode toggle (`Alt+C`) +- [x] Compact mode toggle (`Alt+C`) ### Fullscreen Mode - [ ] Any panel can go fullscreen (`F11`) @@ -94,15 +94,15 @@ Long-term open items for Clide development. ## Plugin System - [ ] User-defined panels via pluggy -- [ ] Custom integrations support +- [x] Custom integrations support (hookspecs defined) - [ ] Extension API documentation ## Testing -- [ ] Snapshot tests for all panels -- [ ] Integration tests for panel communication -- [ ] Unit tests for controllers -- [ ] Unit tests for services +- [x] Snapshot tests for all panels +- [x] Integration tests for panel communication +- [x] Unit tests for controllers +- [x] Unit tests for services ## Documentation diff --git a/clide/controllers/editor.py b/clide/controllers/editor.py index c3e7e0db..dc11ea90 100644 --- a/clide/controllers/editor.py +++ b/clide/controllers/editor.py @@ -130,7 +130,9 @@ class EditorController: self._state.buffers.remove(buffer) # Update active buffer - if self._state.active_buffer_index is not None and self._state.active_buffer_index >= len(self._state.buffers): + if self._state.active_buffer_index is not None and self._state.active_buffer_index >= len( + self._state.buffers + ): self._state.active_buffer_index = ( len(self._state.buffers) - 1 if self._state.buffers else None ) @@ -146,10 +148,7 @@ class EditorController: Returns: True if saved successfully """ - if path: - buffer = self._state.get_buffer_by_path(path) - else: - buffer = self.active_buffer + buffer = self._state.get_buffer_by_path(path) if path else self.active_buffer if not buffer: return False diff --git a/clide/services/claude_events.py b/clide/services/claude_events.py index 10754c2e..68049419 100644 --- a/clide/services/claude_events.py +++ b/clide/services/claude_events.py @@ -7,13 +7,12 @@ from terminal output, enabling tight IDE integration. from __future__ import annotations import re +from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from typing import Callable, Literal from textual.message import Message - # Event Types # ----------- @@ -21,54 +20,63 @@ from textual.message import Message @dataclass class ClaudeEvent: """Base class for Claude Code events.""" + pass @dataclass class FileReadEvent(ClaudeEvent): """Emitted when Claude reads a file.""" + path: Path @dataclass class FileEditEvent(ClaudeEvent): """Emitted when Claude edits a file.""" + path: Path @dataclass class FileWriteEvent(ClaudeEvent): """Emitted when Claude creates/writes a file.""" + path: Path @dataclass class GlobEvent(ClaudeEvent): """Emitted when Claude searches for files.""" + pattern: str @dataclass class GrepEvent(ClaudeEvent): """Emitted when Claude searches file contents.""" + pattern: str @dataclass class ToolStartEvent(ClaudeEvent): """Emitted when Claude starts using a tool.""" + tool_name: str @dataclass class ToolEndEvent(ClaudeEvent): """Emitted when Claude finishes using a tool.""" + tool_name: str @dataclass class DiffProposedEvent(ClaudeEvent): """Emitted when Claude proposes a diff.""" + content: str @@ -95,11 +103,9 @@ PATTERNS = { "tool_write": re.compile(r"● Write\(([^)]+)\)"), "tool_glob": re.compile(r"● Glob\(([^)]+)\)"), "tool_grep": re.compile(r"● Grep\(([^)]+)\)"), - # Generic tool pattern "tool_start": re.compile(r"● (\w+)\("), "tool_end": re.compile(r"└─"), - # Diff headers "diff_header": re.compile(r"^@@\s*-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s*@@", re.MULTILINE), "diff_file": re.compile(r"^(?:---|\+\+\+)\s+([^\s]+)", re.MULTILINE), diff --git a/clide/services/file_service.py b/clide/services/file_service.py index 3e9399ca..610ddc2c 100644 --- a/clide/services/file_service.py +++ b/clide/services/file_service.py @@ -2,7 +2,6 @@ from pathlib import Path - # Language extension mapping for syntax highlighting # Maps file extensions to tree-sitter language identifiers LANGUAGE_MAP: dict[str, str] = { diff --git a/clide/services/syntax_service.py b/clide/services/syntax_service.py index 066911f7..f5f26d7e 100644 --- a/clide/services/syntax_service.py +++ b/clide/services/syntax_service.py @@ -8,10 +8,6 @@ registering additional languages. from __future__ import annotations import logging -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - pass logger = logging.getLogger(__name__) diff --git a/clide/vendor/pyte/graphics.py b/clide/vendor/pyte/graphics.py index beb93012..61bbc732 100644 --- a/clide/vendor/pyte/graphics.py +++ b/clide/vendor/pyte/graphics.py @@ -1,17 +1,17 @@ """ - pyte.graphics - ~~~~~~~~~~~~~ +pyte.graphics +~~~~~~~~~~~~~ - This module defines graphic-related constants, mostly taken from - :manpage:`console_codes(4)` and - http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html. +This module defines graphic-related constants, mostly taken from +:manpage:`console_codes(4)` and +http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html. - :copyright: (c) 2011-2012 by Selectel. - :copyright: (c) 2012-2017 by pyte authors and contributors, - see AUTHORS for details. - :license: LGPL, see LICENSE for more details. +:copyright: (c) 2011-2012 by Selectel. +:copyright: (c) 2012-2017 by pyte authors and contributors, + see AUTHORS for details. +:license: LGPL, see LICENSE for more details. - Vendored for Clide with modifications for diagnostic logging. +Vendored for Clide with modifications for diagnostic logging. """ #: A mapping of ANSI text style codes to style names, "+" means the: @@ -51,7 +51,7 @@ FG_ANSI = { 35: "magenta", 36: "cyan", 37: "white", - 39: "default" # white. + 39: "default", # white. } #: An alias to :data:`~pyte.graphics.FG_ANSI` for compatibility. @@ -67,7 +67,7 @@ FG_AIXTERM = { 94: "brightblue", 95: "brightmagenta", 96: "brightcyan", - 97: "brightwhite" + 97: "brightwhite", } #: A mapping of ANSI background color codes to color names. @@ -85,7 +85,7 @@ BG_ANSI = { 45: "magenta", 46: "cyan", 47: "white", - 49: "default" # black. + 49: "default", # black. } #: An alias to :data:`~pyte.graphics.BG_ANSI` for compatibility. @@ -101,7 +101,7 @@ BG_AIXTERM = { 104: "brightblue", 105: "bfightmagenta", 106: "brightcyan", - 107: "brightwhite" + 107: "brightwhite", } #: SGR code for foreground in 256 or True color mode. @@ -114,25 +114,25 @@ BG_256 = 48 # The following code is part of the Pygments project (BSD licensed). _FG_BG_256 = [ (0x00, 0x00, 0x00), # 0 - (0xcd, 0x00, 0x00), # 1 - (0x00, 0xcd, 0x00), # 2 - (0xcd, 0xcd, 0x00), # 3 - (0x00, 0x00, 0xee), # 4 - (0xcd, 0x00, 0xcd), # 5 - (0x00, 0xcd, 0xcd), # 6 - (0xe5, 0xe5, 0xe5), # 7 - (0x7f, 0x7f, 0x7f), # 8 - (0xff, 0x00, 0x00), # 9 - (0x00, 0xff, 0x00), # 10 - (0xff, 0xff, 0x00), # 11 - (0x5c, 0x5c, 0xff), # 12 - (0xff, 0x00, 0xff), # 13 - (0x00, 0xff, 0xff), # 14 - (0xff, 0xff, 0xff), # 15 + (0xCD, 0x00, 0x00), # 1 + (0x00, 0xCD, 0x00), # 2 + (0xCD, 0xCD, 0x00), # 3 + (0x00, 0x00, 0xEE), # 4 + (0xCD, 0x00, 0xCD), # 5 + (0x00, 0xCD, 0xCD), # 6 + (0xE5, 0xE5, 0xE5), # 7 + (0x7F, 0x7F, 0x7F), # 8 + (0xFF, 0x00, 0x00), # 9 + (0x00, 0xFF, 0x00), # 10 + (0xFF, 0xFF, 0x00), # 11 + (0x5C, 0x5C, 0xFF), # 12 + (0xFF, 0x00, 0xFF), # 13 + (0x00, 0xFF, 0xFF), # 14 + (0xFF, 0xFF, 0xFF), # 15 ] # colors 16..231: the 6x6x6 color cube -valuerange = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff) +valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF) for i in range(216): r = valuerange[(i // 36) % 6] @@ -145,4 +145,4 @@ for i in range(24): v = 8 + i * 10 _FG_BG_256.append((v, v, v)) -FG_BG_256 = ["{0:02x}{1:02x}{2:02x}".format(r, g, b) for r, g, b in _FG_BG_256] +FG_BG_256 = [f"{r:02x}{g:02x}{b:02x}" for r, g, b in _FG_BG_256] diff --git a/clide/vendor/pyte/screens.py b/clide/vendor/pyte/screens.py index 980f5f92..2db9c1fb 100644 --- a/clide/vendor/pyte/screens.py +++ b/clide/vendor/pyte/screens.py @@ -1,16 +1,17 @@ """ - pyte.screens - ~~~~~~~~~~~~ +pyte.screens +~~~~~~~~~~~~ - This module provides classes for terminal screens. +This module provides classes for terminal screens. - :copyright: (c) 2011-2012 by Selectel. - :copyright: (c) 2012-2017 by pyte authors and contributors, - see AUTHORS for details. - :license: LGPL, see LICENSE for more details. +:copyright: (c) 2011-2012 by Selectel. +:copyright: (c) 2012-2017 by pyte authors and contributors, + see AUTHORS for details. +:license: LGPL, see LICENSE for more details. - Vendored for Clide with modifications for diagnostic logging. +Vendored for Clide with modifications for diagnostic logging. """ + from __future__ import annotations import copy @@ -20,18 +21,22 @@ import os import sys import unicodedata import warnings -from collections import deque, defaultdict +from collections import defaultdict, deque +from collections.abc import Callable, Generator, Sequence from functools import lru_cache -from typing import Any, Callable, DefaultDict, Dict, Generator, List, NamedTuple, Optional, Set, Sequence, TextIO, TypeVar +from typing import ( + Any, + NamedTuple, + TextIO, + TypeVar, +) from wcwidth import wcwidth as _wcwidth # type: ignore[import] -from . import ( - charsets as cs, - control as ctrl, - graphics as g, - modes as mo -) +from . import charsets as cs +from . import control as ctrl +from . import graphics as g +from . import modes as mo from .streams import Stream wcwidth: Callable[[str], int] = lru_cache(maxsize=4096)(_wcwidth) @@ -40,10 +45,10 @@ KT = TypeVar("KT") VT = TypeVar("VT") # Clide diagnostic logging support -_debug_logger: Optional[Callable[[str], None]] = None +_debug_logger: Callable[[str], None] | None = None -def set_debug_logger(logger: Optional[Callable[[str], None]]) -> None: +def set_debug_logger(logger: Callable[[str], None] | None) -> None: """Set a debug logger function for diagnostic output.""" global _debug_logger _debug_logger = logger @@ -57,12 +62,14 @@ def _log_debug(message: str) -> None: class Margins(NamedTuple): """A container for screen's scroll margins.""" + top: int bottom: int class Savepoint(NamedTuple): """A container for savepoint, created on :data:`~pyte.escape.DECSC`.""" + cursor: Cursor g0_charset: str g1_charset: str @@ -73,6 +80,7 @@ class Savepoint(NamedTuple): class Char(NamedTuple): """A single styled on-screen character.""" + data: str fg: str = "default" bg: str = "default" @@ -86,6 +94,7 @@ class Char(NamedTuple): class Cursor: """Screen cursor.""" + __slots__ = ("x", "y", "attrs", "hidden") def __init__(self, x: int, y: int, attrs: Char = Char(" ")) -> None: @@ -95,8 +104,9 @@ class Cursor: self.hidden = False -class StaticDefaultDict(Dict[KT, VT]): +class StaticDefaultDict(dict[KT, VT]): """A dict with a static default value.""" + def __init__(self, default: VT) -> None: self.default = default @@ -117,22 +127,24 @@ class Screen: return Char(data=" ", fg="default", bg="default", reverse=reverse) def __init__(self, columns: int, lines: int) -> None: - self.savepoints: List[Savepoint] = [] + self.savepoints: list[Savepoint] = [] self.columns = columns self.lines = lines - self.buffer: Dict[int, StaticDefaultDict[int, Char]] = defaultdict(lambda: StaticDefaultDict[int, Char](self.default_char)) - self.dirty: Set[int] = set() + self.buffer: dict[int, StaticDefaultDict[int, Char]] = defaultdict( + lambda: StaticDefaultDict[int, Char](self.default_char) + ) + self.dirty: set[int] = set() self.reset() self.mode = _DEFAULT_MODE.copy() - self.margins: Optional[Margins] = None + self.margins: Margins | None = None def __repr__(self) -> str: - return ("{0}({1}, {2})".format(self.__class__.__name__, - self.columns, self.lines)) + return f"{self.__class__.__name__}({self.columns}, {self.lines})" @property - def display(self) -> List[str]: + def display(self) -> list[str]: """A list of screen lines as unicode strings.""" + def render(line: StaticDefaultDict[int, Char]) -> Generator[str, None, None]: is_wide_char = False for x in range(self.columns): @@ -167,9 +179,9 @@ class Screen: self.cursor = Cursor(0, 0) self.cursor_position() - self.saved_columns: Optional[int] = None + self.saved_columns: int | None = None - def resize(self, lines: Optional[int] = None, columns: Optional[int] = None) -> None: + def resize(self, lines: int | None = None, columns: int | None = None) -> None: """Resize the screen to the given size.""" lines = lines or self.lines columns = columns or self.columns @@ -195,7 +207,7 @@ class Screen: self.lines, self.columns = lines, columns self.set_margins() - def set_margins(self, top: Optional[int] = None, bottom: Optional[int] = None) -> None: + def set_margins(self, top: int | None = None, bottom: int | None = None) -> None: """Select top and bottom margins for the scrolling region.""" if (top is None or top == 0) and bottom is None: self.margins = None @@ -293,8 +305,7 @@ class Screen: def draw(self, data: str) -> None: """Display decoded characters at the current cursor position.""" - data = data.translate( - self.g1_charset if self.charset else self.g0_charset) + data = data.translate(self.g1_charset if self.charset else self.g0_charset) for char in data: char_width = wcwidth(char) @@ -303,7 +314,9 @@ class Screen: if _debug_logger is not None and char_width > 0: code = ord(char) if code > 127 or code < 32: - _log_debug(f"[DRAW] char={char!r} code=U+{code:04X} width={char_width} pos=({self.cursor.x},{self.cursor.y})") + _log_debug( + f"[DRAW] char={char!r} code=U+{code:04X} width={char_width} pos=({self.cursor.x},{self.cursor.y})" + ) if self.cursor.x == self.columns: if mo.DECAWM in self.mode: @@ -331,7 +344,9 @@ class Screen: elif self.cursor.y: last = self.buffer[self.cursor.y - 1][self.columns - 1] normalized = unicodedata.normalize("NFC", last.data + char) - self.buffer[self.cursor.y - 1][self.columns - 1] = last._replace(data=normalized) + self.buffer[self.cursor.y - 1][self.columns - 1] = last._replace( + data=normalized + ) else: break @@ -396,12 +411,16 @@ class Screen: def save_cursor(self) -> None: """Push the current cursor position onto the stack.""" - self.savepoints.append(Savepoint(copy.copy(self.cursor), - self.g0_charset, - self.g1_charset, - self.charset, - mo.DECOM in self.mode, - mo.DECAWM in self.mode)) + self.savepoints.append( + Savepoint( + copy.copy(self.cursor), + self.g0_charset, + self.g1_charset, + self.charset, + mo.DECOM in self.mode, + mo.DECAWM in self.mode, + ) + ) def restore_cursor(self) -> None: """Set the current cursor position to whatever cursor is on top of the stack.""" @@ -421,7 +440,7 @@ class Screen: self.reset_mode(mo.DECOM) self.cursor_position() - def insert_lines(self, count: Optional[int] = None) -> None: + def insert_lines(self, count: int | None = None) -> None: """Insert the indicated # of lines at line with cursor.""" count = count or 1 top, bottom = self.margins or Margins(0, self.lines - 1) @@ -433,7 +452,7 @@ class Screen: self.buffer.pop(y, None) self.carriage_return() - def delete_lines(self, count: Optional[int] = None) -> None: + def delete_lines(self, count: int | None = None) -> None: """Delete the indicated # of lines.""" count = count or 1 top, bottom = self.margins or Margins(0, self.lines - 1) @@ -447,7 +466,7 @@ class Screen: self.buffer.pop(y, None) self.carriage_return() - def insert_characters(self, count: Optional[int] = None) -> None: + def insert_characters(self, count: int | None = None) -> None: """Insert the indicated # of blank characters at the cursor position.""" self.dirty.add(self.cursor.y) count = count or 1 @@ -457,7 +476,7 @@ class Screen: line[x + count] = line[x] line.pop(x, None) - def delete_characters(self, count: Optional[int] = None) -> None: + def delete_characters(self, count: int | None = None) -> None: """Delete the indicated # of characters.""" self.dirty.add(self.cursor.y) count = count or 1 @@ -468,7 +487,7 @@ class Screen: else: line.pop(x, None) - def erase_characters(self, count: Optional[int] = None) -> None: + def erase_characters(self, count: int | None = None) -> None: """Erase the indicated # of characters.""" self.dirty.add(self.cursor.y) count = count or 1 @@ -525,7 +544,7 @@ class Screen: """Ensure the cursor is within horizontal screen bounds.""" self.cursor.x = min(max(0, self.cursor.x), self.columns - 1) - def ensure_vbounds(self, use_margins: Optional[bool] = None) -> None: + def ensure_vbounds(self, use_margins: bool | None = None) -> None: """Ensure the cursor is within vertical screen bounds.""" if (use_margins or mo.DECOM in self.mode) and self.margins is not None: top, bottom = self.margins @@ -533,39 +552,39 @@ class Screen: top, bottom = 0, self.lines - 1 self.cursor.y = min(max(top, self.cursor.y), bottom) - def cursor_up(self, count: Optional[int] = None) -> None: + def cursor_up(self, count: int | None = None) -> None: """Move cursor up the indicated # of lines.""" top, _bottom = self.margins or Margins(0, self.lines - 1) self.cursor.y = max(self.cursor.y - (count or 1), top) - def cursor_up1(self, count: Optional[int] = None) -> None: + def cursor_up1(self, count: int | None = None) -> None: """Move cursor up the indicated # of lines to column 1.""" self.cursor_up(count) self.carriage_return() - def cursor_down(self, count: Optional[int] = None) -> None: + def cursor_down(self, count: int | None = None) -> None: """Move cursor down the indicated # of lines.""" _top, bottom = self.margins or Margins(0, self.lines - 1) self.cursor.y = min(self.cursor.y + (count or 1), bottom) - def cursor_down1(self, count: Optional[int] = None) -> None: + def cursor_down1(self, count: int | None = None) -> None: """Move cursor down the indicated # of lines to column 1.""" self.cursor_down(count) self.carriage_return() - def cursor_back(self, count: Optional[int] = None) -> None: + def cursor_back(self, count: int | None = None) -> None: """Move cursor left the indicated # of columns.""" if self.cursor.x == self.columns: self.cursor.x -= 1 self.cursor.x -= count or 1 self.ensure_hbounds() - def cursor_forward(self, count: Optional[int] = None) -> None: + def cursor_forward(self, count: int | None = None) -> None: """Move cursor right the indicated # of columns.""" self.cursor.x += count or 1 self.ensure_hbounds() - def cursor_position(self, line: Optional[int] = None, column: Optional[int] = None) -> None: + def cursor_position(self, line: int | None = None, column: int | None = None) -> None: """Set the cursor to a specific line and column.""" column = (column or 1) - 1 line = (line or 1) - 1 @@ -580,12 +599,12 @@ class Screen: self.ensure_hbounds() self.ensure_vbounds() - def cursor_to_column(self, column: Optional[int] = None) -> None: + def cursor_to_column(self, column: int | None = None) -> None: """Move cursor to a specific column in the current line.""" self.cursor.x = (column or 1) - 1 self.ensure_hbounds() - def cursor_to_line(self, line: Optional[int] = None) -> None: + def cursor_to_line(self, line: int | None = None) -> None: """Move cursor to a specific line in the current column.""" self.cursor.y = (line or 1) - 1 if mo.DECOM in self.mode: @@ -608,7 +627,7 @@ class Screen: """Set display attributes.""" replace = {} - if not attrs or attrs == (0, ): + if not attrs or attrs == (0,): self.cursor.attrs = self.default_char return @@ -637,8 +656,9 @@ class Screen: m = attrs_list.pop() replace[key] = g.FG_BG_256[m] elif n == 2: - replace[key] = "{0:02x}{1:02x}{2:02x}".format( - attrs_list.pop(), attrs_list.pop(), attrs_list.pop()) + replace[key] = ( + f"{attrs_list.pop():02x}{attrs_list.pop():02x}{attrs_list.pop():02x}" + ) except IndexError: pass @@ -659,7 +679,7 @@ class Screen: if mo.DECOM in self.mode: assert self.margins is not None y -= self.margins.top - self.write_process_input(ctrl.CSI + "{0};{1}R".format(y, x)) + self.write_process_input(ctrl.CSI + f"{y};{x}R") def write_process_input(self, data: str) -> None: """Write data to the process running inside the terminal.""" @@ -673,10 +693,13 @@ class Screen: class DiffScreen(Screen): """A screen subclass, which maintains a set of dirty lines. Deprecated.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: warnings.warn( "The functionality of ``DiffScreen` has been merged into " - "``Screen`` and will be removed in 0.8.0.", DeprecationWarning) + "``Screen`` and will be removed in 0.8.0.", + DeprecationWarning, + ) super(DiffScreen, self).__init__(*args, **kwargs) @@ -694,12 +717,10 @@ class HistoryScreen(Screen): _wrapped = set(Stream.events) _wrapped.update(["next_page", "prev_page"]) - def __init__(self, columns: int, lines: int, history: int = 100, ratio: float = .5) -> None: - self.history = History(deque(maxlen=history), - deque(maxlen=history), - float(ratio), - history, - history) + def __init__(self, columns: int, lines: int, history: int = 100, ratio: float = 0.5) -> None: + self.history = History( + deque(maxlen=history), deque(maxlen=history), float(ratio), history, history + ) super(HistoryScreen, self).__init__(columns, lines) def _make_wrapper(self, event: str, handler: Callable[..., Any]) -> Callable[..., Any]: @@ -708,6 +729,7 @@ class HistoryScreen(Screen): result = handler(*args, **kwargs) self.after_event(event) return result + return inner def __getattribute__(self, attr: str) -> Callable[..., Any]: @@ -732,8 +754,7 @@ class HistoryScreen(Screen): line.pop(x) self.cursor.hidden = not ( - self.history.position == self.history.size and - mo.DECTCEM in self.mode + self.history.position == self.history.size and mo.DECTCEM in self.mode ) def _reset_history(self) -> None: @@ -769,12 +790,11 @@ class HistoryScreen(Screen): def prev_page(self) -> None: """Move the screen page up through the history buffer.""" if self.history.position > self.lines and self.history.top: - mid = min(len(self.history.top), - int(math.ceil(self.lines * self.history.ratio))) + mid = min(len(self.history.top), int(math.ceil(self.lines * self.history.ratio))) self.history.bottom.extendleft( - self.buffer[y] - for y in range(self.lines - 1, self.lines - mid - 1, -1)) + self.buffer[y] for y in range(self.lines - 1, self.lines - mid - 1, -1) + ) self.history = self.history._replace(position=self.history.position - mid) for y in range(self.lines - 1, mid - 1, -1): @@ -787,8 +807,7 @@ class HistoryScreen(Screen): def next_page(self) -> None: """Move the screen page down through the history buffer.""" if self.history.position < self.history.size and self.history.bottom: - mid = min(len(self.history.bottom), - int(math.ceil(self.lines * self.history.ratio))) + mid = min(len(self.history.bottom), int(math.ceil(self.lines * self.history.ratio))) self.history.top.extend(self.buffer[y] for y in range(mid)) self.history = self.history._replace(position=self.history.position + mid) @@ -803,6 +822,7 @@ class HistoryScreen(Screen): class DebugEvent(NamedTuple): """Event dispatched to DebugScreen.""" + name: str args: Any kwargs: Any @@ -830,6 +850,7 @@ class DebugScreen: def wrapper(*args: Any, **kwargs: Any) -> None: self.to.write(str(DebugEvent(attr, args, kwargs))) self.to.write(str(os.linesep)) + return wrapper def __getattribute__(self, attr: str) -> Callable[..., None]: diff --git a/clide/vendor/pyte/streams.py b/clide/vendor/pyte/streams.py index 11d857c7..1a720691 100644 --- a/clide/vendor/pyte/streams.py +++ b/clide/vendor/pyte/streams.py @@ -1,25 +1,26 @@ """ - pyte.streams - ~~~~~~~~~~~~ +pyte.streams +~~~~~~~~~~~~ - This module provides three stream implementations with different - features; for starters, here's a quick example of how streams are - typically used: +This module provides three stream implementations with different +features; for starters, here's a quick example of how streams are +typically used: - >>> import pyte - >>> screen = pyte.Screen(80, 24) - >>> stream = pyte.Stream(screen) - >>> stream.feed("\x1b[5B") # Move the cursor down 5 rows. - >>> screen.cursor.y - 5 +>>> import pyte +>>> screen = pyte.Screen(80, 24) +>>> stream = pyte.Stream(screen) +>>> stream.feed("\x1b[5B") # Move the cursor down 5 rows. +>>> screen.cursor.y +5 - :copyright: (c) 2011-2012 by Selectel. - :copyright: (c) 2012-2017 by pyte authors and contributors, - see AUTHORS for details. - :license: LGPL, see LICENSE for more details. +:copyright: (c) 2011-2012 by Selectel. +:copyright: (c) 2012-2017 by pyte authors and contributors, + see AUTHORS for details. +:license: LGPL, see LICENSE for more details. - Vendored for Clide with modifications for diagnostic logging. +Vendored for Clide with modifications for diagnostic logging. """ + from __future__ import annotations import codecs @@ -27,21 +28,22 @@ import itertools import re import warnings from collections import defaultdict -from collections.abc import Mapping -from typing import Any, Callable, Dict, Generator, Optional, TYPE_CHECKING +from collections.abc import Callable, Generator, Mapping +from typing import TYPE_CHECKING, Any -from . import control as ctrl, escape as esc +from . import control as ctrl +from . import escape as esc if TYPE_CHECKING: from .screens import Screen # Clide diagnostic logging support -_debug_logger: Optional[Callable[[str], None]] = None -_event_callback: Optional[Callable[[str], None]] = None +_debug_logger: Callable[[str], None] | None = None +_event_callback: Callable[[str], None] | None = None -def set_debug_logger(logger: Optional[Callable[[str], None]]) -> None: +def set_debug_logger(logger: Callable[[str], None] | None) -> None: """Set a debug logger function for diagnostic output. Args: @@ -51,7 +53,7 @@ def set_debug_logger(logger: Optional[Callable[[str], None]]) -> None: _debug_logger = logger -def set_event_callback(callback: Optional[Callable[[str], None]]) -> None: +def set_event_callback(callback: Callable[[str], None] | None) -> None: """Set an event callback for raw terminal data. This callback is invoked with the raw data before parsing. @@ -70,7 +72,7 @@ def _log_debug(message: str) -> None: _debug_logger(message) -ParserGenerator = Generator[Optional[bool], str, None] +ParserGenerator = Generator[bool | None, str, None] class Stream: @@ -158,30 +160,35 @@ class Stream: esc.SGR: "select_graphic_rendition", esc.DSR: "report_device_status", esc.DECSTBM: "set_margins", - esc.HPA: "cursor_to_column" + esc.HPA: "cursor_to_column", } #: A set of all events dispatched by the stream. - events = frozenset(itertools.chain( - basic.values(), escape.values(), sharp.values(), csi.values(), - ["define_charset"], - ["set_icon_name", "set_title"], # OSC. - ["draw", "debug"])) + events = frozenset( + itertools.chain( + basic.values(), + escape.values(), + sharp.values(), + csi.values(), + ["define_charset"], + ["set_icon_name", "set_title"], # OSC. + ["draw", "debug"], + ) + ) #: A regular expression pattern matching everything what can be #: considered plain text. _special = set([ctrl.ESC, ctrl.CSI_C1, ctrl.NUL, ctrl.DEL, ctrl.OSC_C1]) _special.update(basic) - _text_pattern = re.compile( - "[^" + "".join(map(re.escape, _special)) + "]+") + _text_pattern = re.compile("[^" + "".join(map(re.escape, _special)) + "]+") del _special - def __init__(self, screen: Optional[Screen] = None, strict: bool = True) -> None: - self.listener: Optional[Screen] = None + def __init__(self, screen: Screen | None = None, strict: bool = True) -> None: + self.listener: Screen | None = None self.strict = strict self.use_utf8: bool = True - self._taking_plain_text: Optional[bool] = None + self._taking_plain_text: bool | None = None if screen is not None: self.attach(screen) @@ -192,18 +199,20 @@ class Stream: :param pyte.screens.Screen screen: a screen to attach to. """ if self.listener is not None: - warnings.warn("As of version 0.6.0 the listener queue is " - "restricted to a single element. Existing " - "listener {0} will be replaced." - .format(self.listener), DeprecationWarning) + warnings.warn( + "As of version 0.6.0 the listener queue is " + "restricted to a single element. Existing " + f"listener {self.listener} will be replaced.", + DeprecationWarning, + ) if self.strict: for event in self.events: if not hasattr(screen, event): - raise TypeError("{0} is missing {1}".format(screen, event)) + raise TypeError(f"{screen} is missing {event}") self.listener = screen - self._parser: Optional[ParserGenerator] = None + self._parser: ParserGenerator | None = None self._initialize_parser() def detach(self, screen: Screen) -> None: @@ -230,7 +239,7 @@ class Stream: # Clide: Debug log incoming data if _debug_logger is not None: # Log escape sequences in a readable format - escaped = data.encode('unicode_escape').decode('ascii') + escaped = data.encode("unicode_escape").decode("ascii") if len(escaped) > 200: escaped = escaped[:200] + "..." _log_debug(f"[STREAM] feed: {escaped}") @@ -254,12 +263,12 @@ class Stream: else: taking_plain_text = False else: - taking_plain_text = send(data[offset:offset + 1]) + taking_plain_text = send(data[offset : offset + 1]) offset += 1 self._taking_plain_text = taking_plain_text - def _send_to_parser(self, data: str) -> Optional[bool]: + def _send_to_parser(self, data: str) -> bool | None: try: assert self._parser is not None return self._parser.send(data) @@ -294,25 +303,28 @@ class Stream: SP_OR_GT = ctrl.SP + ">" NUL_OR_DEL = ctrl.NUL + ctrl.DEL CAN_OR_SUB = ctrl.CAN + ctrl.SUB - ALLOWED_IN_CSI = "".join([ctrl.BEL, ctrl.BS, ctrl.HT, ctrl.LF, - ctrl.VT, ctrl.FF, ctrl.CR]) + ALLOWED_IN_CSI = "".join([ctrl.BEL, ctrl.BS, ctrl.HT, ctrl.LF, ctrl.VT, ctrl.FF, ctrl.CR]) OSC_TERMINATORS = set([ctrl.ST_C0, ctrl.ST_C1, ctrl.BEL]) - def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., None]]: - d = defaultdict(lambda: debug, dict( - (event, getattr(listener, attr)) - for event, attr in mapping.items())) + def create_dispatcher(mapping: Mapping[str, str]) -> dict[str, Callable[..., None]]: + d = defaultdict( + lambda: debug, + dict((event, getattr(listener, attr)) for event, attr in mapping.items()), + ) # Clide: Wrap dispatchers with debug logging if _debug_logger is not None: original_d = dict(d) for event, attr in mapping.items(): original_handler = original_d.get(event, debug) + def make_wrapper(e: str, a: str, h: Callable[..., None]) -> Callable[..., None]: def wrapper(*args: Any, **kwargs: Any) -> None: _log_debug(f"[DISPATCH] {a}({args}, {kwargs})") return h(*args, **kwargs) + return wrapper + d[event] = make_wrapper(event, attr, original_handler) return d @@ -360,7 +372,7 @@ class Stream: listener.define_charset(code, mode=char) else: escape_dispatch[char]() - continue # Don't go to CSI. + continue # Don't go to CSI. if char in basic: # Ignore shifts in UTF-8 mode. See @@ -485,6 +497,7 @@ class ByteStream(Stream): Assume the input to :meth:`~pyte.streams.ByteStream.feed` is encoded using UTF-8. Defaults to ``True``. """ + def __init__(self, *args: Any, **kwargs: Any): super(ByteStream, self).__init__(*args, **kwargs) diff --git a/clide/widgets/components/action_bar.py b/clide/widgets/components/action_bar.py index 5e135aaa..597a3e92 100644 --- a/clide/widgets/components/action_bar.py +++ b/clide/widgets/components/action_bar.py @@ -2,14 +2,13 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable from textual.app import ComposeResult from textual.containers import Horizontal from textual.message import Message from textual.reactive import reactive -from textual.widget import Widget from textual.widgets import Button, Static diff --git a/clide/widgets/components/diff_pane.py b/clide/widgets/components/diff_pane.py index a7ebcb54..b9ce4af5 100644 --- a/clide/widgets/components/diff_pane.py +++ b/clide/widgets/components/diff_pane.py @@ -137,7 +137,7 @@ class DiffPane(Vertical): header = self.query_one(".diff-header", Static) header.update("No diff loaded") - def on_button_pressed(self, _event: Button.Pressed) -> None: + def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" if not self._diff: return diff --git a/clide/widgets/panels/workspace.py b/clide/widgets/panels/workspace.py index 6356e682..281d2b05 100644 --- a/clide/widgets/panels/workspace.py +++ b/clide/widgets/panels/workspace.py @@ -6,10 +6,10 @@ from textual.app import ComposeResult from textual.containers import Horizontal, Vertical from textual.message import Message from textual.reactive import reactive -from textual.widgets import ContentSwitcher, Tab, TabPane, Tabs +from textual.widgets import ContentSwitcher, Tab, Tabs from clide.models.diff import DiffContent -from clide.widgets.components.action_bar import ActionBar, ActionButton, STANDARD_BUTTONS +from clide.widgets.components.action_bar import ActionBar, ActionButton from clide.widgets.components.diff_pane import DiffPane from clide.widgets.components.editor_pane import EditorPane from clide.widgets.components.terminal_pane import TerminalPane @@ -104,6 +104,7 @@ class WorkspacePanel(Vertical): class CloseRequested(Message): """Emitted when workspace should be hidden.""" + pass # Reactive state - persisted when hidden @@ -114,10 +115,12 @@ class WorkspacePanel(Vertical): # Messages for app-level actions class MaximizeRequested(Message): """Emitted when workspace should be maximized.""" + pass class RestoreRequested(Message): """Emitted when workspace should be restored from maximized.""" + pass def __init__( @@ -163,43 +166,53 @@ class WorkspacePanel(Vertical): # Register standard buttons using simple ASCII icons # Save button (for editor) - self._action_bar.register_button(ActionButton( - id="save", - icon="[S]", - tooltip="Save", - )) + self._action_bar.register_button( + ActionButton( + id="save", + icon="[S]", + tooltip="Save", + ) + ) # Add separator self._action_bar.add_separator() # Close button - self._action_bar.register_button(ActionButton( - id="close", - icon="x", - tooltip="Close", - )) + self._action_bar.register_button( + ActionButton( + id="close", + icon="x", + tooltip="Close", + ) + ) # Minimize button - self._action_bar.register_button(ActionButton( - id="minimize", - icon="_", - tooltip="Minimize", - )) + self._action_bar.register_button( + ActionButton( + id="minimize", + icon="_", + tooltip="Minimize", + ) + ) # Maximize button - self._action_bar.register_button(ActionButton( - id="maximize", - icon="^", - tooltip="Maximize", - )) + self._action_bar.register_button( + ActionButton( + id="maximize", + icon="^", + tooltip="Maximize", + ) + ) # Restore button (hidden by default) - self._action_bar.register_button(ActionButton( - id="restore", - icon="v", - tooltip="Restore", - visible=False, - )) + self._action_bar.register_button( + ActionButton( + id="restore", + icon="v", + tooltip="Restore", + visible=False, + ) + ) # Update button visibility based on current tab self._update_action_bar_for_tab(self.active_tab) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d72e86d4..0f3d7aca 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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__) +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) diff --git a/docs/code-organization.md b/docs/code-organization.md new file mode 100644 index 00000000..0578f75c --- /dev/null +++ b/docs/code-organization.md @@ -0,0 +1,342 @@ +# Code Organization + +Clide follows a layered architecture separating UI components from business logic, enabling testability and maintainability. + +## Directory Structure + +``` +clide/ +├── app.py # Main application, layout, keybindings +├── cli.py # Typer entry point +├── models/ # Pydantic data models +├── services/ # Background services and utilities +├── controllers/ # Business logic (no UI) +├── widgets/ +│ ├── panels/ # Main layout containers +│ └── components/ # Reusable UI pieces +├── themes/ # Theme definitions and registry +├── extensions/ # Plugin system (hookspecs, manager) +├── templates/ # Bundled templates (skills, etc.) +└── vendor/ # Vendored dependencies (pyte) +``` + +## Layers + +### Models (`clide/models/`) + +Pure data structures using Pydantic with strict mode. Models are immutable and contain no business logic. + +```python +from pydantic import BaseModel, ConfigDict + +class GitChange(BaseModel): + model_config = ConfigDict(strict=True, frozen=True) + + path: str + status: Literal["added", "modified", "deleted", "untracked", "renamed"] + staged: bool +``` + +**Key models:** +- `git.py` — Git-related types (GitBranch, GitCommit, GitChange, GitStatus) +- `config.py` — Application settings (ClideSettings, PanelConfig) +- `problems.py` — Linter output (Problem, Severity) +- `todos.py` — TODO items (TodoItem, ProjectTodoItem, TodoType) + +### Services (`clide/services/`) + +Stateless utilities that perform work without UI interaction. Services may be async or use background threads. + +```python +class GitService: + """Git operations via subprocess.""" + + def __init__(self, workdir: Path): + self._workdir = workdir + + async def get_status(self) -> GitStatus: + """Get current repository status.""" + ... + + async def get_branches(self) -> list[GitBranch]: + """List all branches.""" + ... +``` + +**Key services:** +- `git_service.py` — Git CLI operations +- `file_service.py` — File read/write operations +- `todo_scanner.py` — Scans codebase for TODO/FIXME comments +- `settings_service.py` — User settings persistence +- `skill_installer.py` — Claude Code skill management +- `file_watcher.py` — File system change monitoring +- `syntax_service.py` — Tree-sitter syntax highlighting + +### Controllers (`clide/controllers/`) + +Bridge between services and UI. Controllers contain business logic, manage state, and emit Textual messages. Controllers have no direct UI rendering. + +```python +from clide.controllers.base import controller + +@controller +class GitController: + """Manages git state and operations.""" + + def __init__(self, workdir: Path): + self._service = GitService(workdir) + self._status: GitStatus | None = None + + async def refresh_status(self) -> GitStatus: + """Refresh and cache git status.""" + self._status = await self._service.get_status() + return self._status + + def stage_file(self, path: str) -> None: + """Stage a file for commit.""" + ... +``` + +**Key controllers:** +- `git.py` — Git operations, skill integration +- `editor.py` — File editing state +- `diff.py` — Diff viewing and management +- `problems.py` — Linter integration +- `todos.py` — TODO tracking +- `jira.py` — Jira CLI integration + +### Widgets (`clide/widgets/`) + +UI components split into panels (layout containers) and components (reusable pieces). + +#### Panels (`clide/widgets/panels/`) + +Top-level layout containers that compose the application UI. + +```python +class SidebarPanel(Vertical): + """Left sidebar with Files, Git, and Tree tabs.""" + + class FileSelected(Message): + """Emitted when a file is selected.""" + def __init__(self, path: Path): + self.path = path + super().__init__() + + def compose(self) -> ComposeResult: + with TabbedContent(): + with TabPane("Files"): + yield FilesView(path=self._workdir) + with TabPane("Git"): + yield GitChangesView() + with TabPane("Tree"): + yield GitGraphView() + yield BranchStatus() +``` + +**Panels:** +- `sidebar.py` — Left sidebar (files, git, graph) +- `context.py` — Right sidebar (problems, todos, jira) +- `workspace.py` — Center workspace (editor, diff, terminal) +- `claude.py` — Claude Code terminal integration + +#### Components (`clide/widgets/components/`) + +Reusable UI pieces composed into panels. + +**File browsing:** +- `files_view.py` — Project file tree +- `file_entry.py` — Single file/directory entry + +**Git:** +- `git_changes.py` — Staged/unstaged file lists +- `git_graph.py` — Visual branch graph +- `branch_status.py` — Branch indicator with popout selector + +**Context:** +- `problems_view.py` — Linter problems list +- `todos_view.py` — TODO/FIXME list with sub-tabs +- `jira_view.py` — Jira issue display + +**Editor:** +- `editor_pane.py` — Code editor with syntax highlighting +- `diff_pane.py` — Side-by-side diff viewer +- `terminal_pane.py` — Command execution terminal + +## Communication Patterns + +### Message Flow + +Components communicate via Textual's message system. Messages bubble up through the widget tree. + +``` +Component emits message + │ + ▼ +Parent panel receives and may re-emit + │ + ▼ +App handles and coordinates response + │ + ▼ +App calls controller methods + │ + ▼ +Controller updates state, may emit messages + │ + ▼ +UI updates reactively +``` + +**Example: File selection** + +```python +# In FilesView (component) +class FileSelected(Message): + def __init__(self, path: Path): + self.path = path + super().__init__() + +def on_tree_node_selected(self, event): + if event.node.data.is_file: + self.post_message(self.FileSelected(event.node.data.path)) + +# In SidebarPanel (panel) +def on_files_view_file_selected(self, event: FilesView.FileSelected): + # Re-emit for app to handle + self.post_message(self.FileSelected(event.path)) + +# In ClideApp (app) +def on_sidebar_panel_file_selected(self, event: SidebarPanel.FileSelected): + self.editor_controller.open_file(event.path) + self.show_workspace("editor") +``` + +### Reactive Properties + +State that affects UI uses Textual's reactive system: + +```python +class ClideApp(App): + # Reactive state + workspace_visible: reactive[bool] = reactive(False) + problem_count: reactive[int] = reactive(0) + current_branch: reactive[str] = reactive("main") + + def watch_workspace_visible(self, visible: bool) -> None: + """React to workspace visibility changes.""" + workspace = self.query_one("#panel-workspace") + workspace.display = visible + + claude = self.query_one("#panel-claude") + claude.styles.height = "40%" if visible else "100%" +``` + +### Background Tasks + +Long-running operations use the `@work` decorator to avoid blocking the UI: + +```python +from textual import work + +class ClideApp(App): + @work(thread=True) + def refresh_git_status(self) -> None: + """Refresh git status in background.""" + status = self.git_controller.get_status_sync() + self.call_from_thread(self._update_git_ui, status) + + def _update_git_ui(self, status: GitStatus) -> None: + """Update UI with git status (runs on main thread).""" + sidebar = self.query_one(SidebarPanel) + sidebar.update_git_status(status.staged, status.unstaged) +``` + +## Extension System + +Clide uses Pluggy for extensibility. Extensions implement hooks defined in `hookspecs.py`. + +```python +# clide/extensions/hookspecs.py +class ClideHookSpec: + @hookspec + def clide_startup(self, app: App) -> None: + """Called when app starts.""" + + @hookspec + def clide_on_file_changed(self, event: FileEvent) -> None: + """Called when a file changes.""" + +# User extension +class MyExtension: + @hookimpl + def clide_on_file_changed(self, event: FileEvent) -> None: + if event.path.suffix == ".py": + # Custom logic for Python files + ... +``` + +**Available hooks:** +- `clide_startup` — App initialization +- `clide_shutdown` — App cleanup +- `clide_on_file_changed` — File system changes +- `clide_on_file_saved` — File saved in editor + +## Skills System + +Clide integrates with Claude Code skills for git operations. Skills are installed to the project's `.claude/skills/` directory. + +```python +# clide/services/skill_installer.py +class SkillInstaller: + def install(self, skill_name: str, scope: Literal["user", "project"] = "project"): + """Install a skill from bundled templates.""" + template_dir = TEMPLATES_DIR / skill_name + target_dir = self.project_skills_dir / skill_name + shutil.copytree(template_dir, target_dir) +``` + +**Bundled skills** (`clide/templates/skills/`): +- `commit` — Git commit workflow +- `stash` — Git stash operations +- `pull` — Git pull with rebase +- `push` — Git push to remote +- `branch` — Branch management + +When a git action button is clicked, Clide ensures the skill is installed before sending the command to Claude. + +## Testing + +Tests mirror the source structure: + +``` +tests/ +├── unit/ +│ ├── test_models.py +│ ├── test_services.py +│ ├── test_controllers.py +│ ├── test_widgets.py +│ └── test_app.py +├── integration/ +│ └── test_files_view.py +└── snapshots/ + └── test_app_snapshots.py +``` + +**Unit tests** verify individual components in isolation. +**Integration tests** verify component interactions. +**Snapshot tests** catch visual regressions using pytest-textual-snapshot. + +## Configuration + +### Application Settings + +`ClideSettings` in `clide/models/config.py` defines app configuration loaded from environment or `.config/settings.toml`. + +### User Settings + +`UserSettings` persisted to `~/.clide/settings.json` stores user preferences: +- Theme selection +- Panel visibility defaults +- Compact mode preference +- Jira integration settings diff --git a/docs/tui-ide-spec.md b/docs/tui-ide-spec.md index 6985bf20..54af80d5 100644 --- a/docs/tui-ide-spec.md +++ b/docs/tui-ide-spec.md @@ -1,14 +1,14 @@ # TUI IDE Specification -A terminal-based IDE built with Textual, designed to wrap Claude Code and integrate project management tooling (Jira/Confluence via CLI). +Design specification for Clide's terminal user interface. This document describes the layout, interactions, and design decisions. ## Design Principles -- **Claude-centric**: Claude Code is the primary workspace, always visible -- **Contextual panels**: Editor/Diff/Terminal appear only when needed -- **VSCode-familiar**: Keybindings and interaction patterns follow VSCode conventions -- **Responsive**: Works on 13" laptop and widescreen monitors -- **No vim magic**: Standard keyboard navigation, no modal editing +- **Claude-centric** — Claude Code is the primary workspace, always visible +- **Contextual panels** — Editor/Diff/Terminal appear only when needed +- **Alt-key shortcuts** — Keybindings use Alt to avoid conflicts with Claude Code input +- **Responsive** — Works on 13" laptops to widescreen monitors +- **State preservation** — Hiding panels preserves all state (never destroy widgets) --- @@ -19,112 +19,105 @@ A terminal-based IDE built with Textual, designed to wrap Claude Code and integr ``` ┌─────────────────┬─────────────────────────┬──────────────────┐ │ panel-sidebar │ panel-workspace (60%) │ panel-context │ -│ │ [Editor][Diff][Terminal]│ │ -│ [Files][Git] │ (hidden when inactive) │ (content area) │ -│ [Tree] ├─────────────────────────┤ │ -│ │ │ │ -│ (content area) │ panel-claude │ │ -│ │ (40% when workspace │ │ +│ 20% │ [Editor][Diff][Terminal]│ 25% │ +│ │ (hidden when inactive) │ │ +│ [Files][Git] ├─────────────────────────┤ [Jira][TODOs] │ +│ [Tree] │ │ [Problems] │ +│ │ panel-claude │ │ +│ (content area) │ (40% when workspace │ (content area) │ │ │ visible, else 100%) │ │ ├─────────────────┤ ├──────────────────┤ -│ branch-status │ │[⚠ 3][✓12][Jira] │ -│ ⎇ main ▾ │ │ context-tabs │ +│ branch-status │ │ │ +│ ⎇ main ▾ │ │ │ +│ staged: 2 │ │ │ └─────────────────┴─────────────────────────┴──────────────────┘ ``` -### Panel Definitions +### Panel IDs ```python PANELS = { # Left sidebar "sidebar": "panel-sidebar", - "sidebar-files": "panel-sidebar-files", - "sidebar-git": "panel-sidebar-git", - "sidebar-tree": "panel-sidebar-tree", - "branch-status": "panel-branch-status", - + # Center "claude": "panel-claude", "workspace": "panel-workspace", - "editor": "panel-editor", - "diff": "panel-diff", - "terminal": "panel-terminal", - + # Right context "context": "panel-context", - "context-jira": "panel-context-jira", - "context-problems": "panel-context-problems", - "context-todos": "panel-context-todos", } ``` --- -## Left Sidebar (`panel-sidebar`) +## Left Sidebar ### Tabs -| Tab | Content | Widget | -|-----|---------|--------| -| Files | Project file tree | `DirectoryTree` | -| Git | Staged/Unstaged changes | `GitChangesView` (custom) | -| Tree | Merge/branch graph | `GitGraphView` (custom) | +| Tab | Content | Purpose | +|-----|---------|---------| +| Files | Project file tree | Navigate and open files | +| Git | Staged/Unstaged changes | Review and manage changes | +| Tree | Branch graph | Visualize git history | -### Git Tab Details +### Git Tab -Two collapsible sections: -- **Staged**: Files in index, ready to commit -- **Unstaged**: Modified/untracked files +Two collapsible sections showing staged and unstaged changes. -Each file item shows: -- Status icon: `+` added, `~` modified, `-` deleted, `?` untracked, `→` renamed -- File path (relative) +**File status indicators:** +- `+` Added +- `~` Modified +- `-` Deleted +- `?` Untracked +- `→` Renamed -**Interactions:** -- Click file → opens in Editor panel -- Double-click or keybind → stage/unstage file -- Right-click or keybind → show context menu (discard, diff, etc.) +**Action buttons:** +- **Commit** — Delegate to Claude with `/commit` skill +- **Stash** — Delegate to Claude with `/stash` skill +- **Pull** — Delegate to Claude with `/pull` skill +- **Push** — Delegate to Claude with `/push` skill -### Tree Tab Details +### Tree Tab -Renders `git log --graph --oneline --decorate --all` with visual styling. +Visual git graph using box-drawing characters: -**Polish item**: Consider custom rendering with box-drawing characters for a cleaner look: ``` -●──┬── main: Latest commit message -│ ●── feature: Feature work -●──┴── Merge branch 'feature' -◆───── Tagged release v1.0 +● main: Latest commit message +│ +├─● feature: Feature work +│ +●─┴ Merge branch 'feature' +◆ Tagged release v1.0 ``` -Use canvas or rich text with: -```python -GRAPH_CHARS = { - 'commit': '●', - 'merge': '◆', - 'line': '│', - 'branch': '├──', - 'join': '┴──', -} -``` +**Symbols:** +- `●` Regular commit +- `◆` Merge commit +- `│` Branch line +- `├` Branch point +- `┴` Merge point ### Branch Status Bar -Fixed at bottom of sidebar. Shows current branch with popout toggle. +Fixed at bottom of sidebar. Shows current branch and git stats. + +``` +┌─────────────────────────────────┐ +│ ⎇ main ▾ staged: 2 unstaged: 5│ +└─────────────────────────────────┘ +``` + +Click to expand branch selector: ``` ┌─────────────────┐ -│ ⎇ main ▾ │ ← Click or keybind to expand -└─────────────────┘ - │ - ▼ (popout overlay) -┌─────────────────┐ │ Recent branches │ -│ ○ main │ +│ ● main │ │ ○ feature/xyz │ │ ○ develop │ ├─────────────────┤ -│ [Checkout] [New]│ +│[Checkout] [New] │ └─────────────────┘ ``` @@ -132,232 +125,162 @@ Fixed at bottom of sidebar. Shows current branch with popout toggle. ## Center Column -### Claude Panel (`panel-claude`) +### Claude Panel -The primary workspace. Displays Claude Code interaction. +The primary workspace. Always visible. -**Default state**: 100% height of center column -**With workspace**: 40% height (bottom) +**Default state:** 100% height of center column +**With workspace:** 40% height (bottom) **Content:** -- Streaming markdown responses (use `Markdown` or `RichLog` widget) -- Visual distinction between: - - Claude's responses - - Tool calls / file operations - - User input -- Input area at bottom +- Full PTY terminal running Claude Code CLI +- Scrollback history (1000 lines) +- Input at bottom -### Workspace Panel (`panel-workspace`) +### Workspace Panel -Tabbed container for Editor, Diff, and Terminal. **Hidden by default.** +Tabbed container for Editor, Diff, and Terminal. Hidden by default. -**Important**: Hiding is not closing. All panels retain state when hidden: -- Editor: Open files, cursor position, scroll position, unsaved changes +**Visibility principle:** Hiding is not closing. All panels retain state: +- Editor: Open file, cursor position, scroll, unsaved changes - Diff: Current diff content, scroll position -- Terminal: Active session, command history, output buffer - -Use `display: none` for visibility, never destroy/recreate widgets. +- Terminal: Command history, output buffer **Visibility triggers:** | Trigger | Result | |---------|--------| -| Click file in sidebar | Show workspace, focus Editor tab | -| Claude proposes changes | Show workspace, focus Diff tab | -| User presses `` Ctrl+` `` | Show workspace, focus Terminal tab | -| User runs command | Show workspace, focus Terminal tab | -| Close all tabs / Escape | Hide workspace, Claude reclaims space | - -**Height**: 60% of center column when visible +| Click file in sidebar | Show workspace, focus Editor | +| Click problem/TODO | Show workspace, focus Editor at line | +| Press `` Alt+` `` | Show workspace, focus Terminal | +| Close all content | Hide workspace, Claude reclaims space | #### Editor Tab -- `TextArea` widget with syntax highlighting -- Language detection from file extension -- Theme: Follow terminal theme or user preference +Code editor with: +- Syntax highlighting (tree-sitter based) +- Line numbers +- Current line highlighting #### Diff Tab -- Side-by-side or unified diff view -- Syntax highlighting for changed content -- Accept/Reject buttons for Claude-proposed changes +Side-by-side diff viewer for: +- Git changes (staged and unstaged) +- Claude-proposed edits #### Terminal Tab -- Proper PTY integration for full terminal emulation -- Or simpler command runner with output display (decide based on complexity) +Command execution terminal: - Working directory tied to project root +- Output preserved when panel hidden --- -## Right Sidebar (`panel-context`) +## Right Context Panel -### Content Area +### Tabs -Switches based on selected bottom tab. Shows one of: -- Jira view (default) -- Problems view -- TODOs view +| Tab | Badge | Content | +|-----|-------|---------| +| Jira | — | Jira issue display | +| TODOs | Count | TODO/FIXME from code and TODO.md | +| Problems | Count | Linter errors and warnings | -### Bottom Tab Bar (`context-tabs`) +Tab badges update reactively as counts change. -``` -┌──────────────────┐ -│ [⚠ 3][✓12][Jira]│ -└──────────────────┘ -``` +### Jira Tab -Tabs show inline counts that update reactively. +Displays Jira issues via CLI integration. Manual refresh button. -| Tab | Icon | Content | -|-----|------|---------| -| Problems | ⚠ | Linter errors, warnings (count badge) | -| TODOs | ✓ | TODO/FIXME comments from codebase (count badge) | -| Jira | Jira | Output from your CLI tool (default) | +### TODOs Tab -### Jira View +Two sub-tabs: -Renders markdown output from your CLI tool. Refreshes on: -- Panel focus -- Manual refresh keybind -- Configurable interval +**Project tab:** Items from `TODO.md` (checkbox format) +**Comments tab:** TODO/FIXME/HACK/XXX comments in code -### Problems View +Click any item to jump to source location. -Aggregates from linters (eslint, ruff, etc.). Shows: +### Problems Tab + +Linter output showing: - File path - Line number -- Severity icon +- Severity (error/warning) - Message -Click → opens file in Editor at that line. - -### TODOs View - -Grep results for `TODO`, `FIXME`, `HACK`, `XXX`. Shows: -- File path -- Line number -- Comment text - -Click → opens file in Editor at that line. +Click to navigate to source. --- ## Responsiveness -### CSS Breakpoints +### CSS Strategy ```css -/* Widescreen (default) */ +/* Default layout */ #panel-sidebar { width: 20%; min-width: 25; } #panel-context { width: 25%; min-width: 30; } #panel-claude { width: 1fr; } - -/* Medium terminals */ -@media (width < 120) { - #panel-sidebar { width: 18%; } - #panel-context { width: 22%; } -} - -/* Narrow terminals (laptop, split screen) */ -@media (width < 100) { - #panel-sidebar { display: none; } - #panel-context { width: 25%; } -} - -@media (width < 80) { - #panel-context { display: none; } - #panel-claude { width: 100%; } -} ``` ### Compact Mode -Toggle with `Ctrl+Shift+C`. Hides both sidebars, maximizes Claude + workspace. +Toggle with `Alt+C`. Hides both sidebars: ```css .compact #panel-sidebar { display: none; } .compact #panel-context { display: none; } ``` -### Fullscreen Mode - -Any panel can go fullscreen with `F11` (when focused). Press `Escape` to exit. - -```css -.fullscreen { - dock: top; - width: 100%; - height: 100%; - layer: fullscreen; -} -``` +All panel state preserved when hidden. --- ## Keybindings -Following VSCode conventions where possible. +All shortcuts use `Alt` modifier to avoid conflicts with Claude Code input. -### Global +### Panel Navigation | Action | Binding | |--------|---------| -| Command palette | `Ctrl+Shift+P` | -| Quick open file | `Ctrl+P` | -| Toggle left sidebar | `Ctrl+B` | -| Toggle right sidebar | `Ctrl+Shift+B` | -| Toggle terminal | `` Ctrl+` `` | -| Toggle compact mode | `Ctrl+Shift+C` | -| Fullscreen focused panel | `F11` | -| Exit fullscreen | `Escape` | +| Toggle left sidebar | `Alt+B` | +| Toggle right sidebar | `Alt+Shift+B` | +| Toggle terminal | `` Alt+` `` | +| Focus Claude | `Alt+1` | +| Focus Editor | `Alt+2` | +| Focus Terminal | `Alt+3` | +| Toggle compact mode | `Alt+C` | -### Navigation +### Application | Action | Binding | |--------|---------| -| Focus Claude panel | `Ctrl+1` | -| Focus Editor | `Ctrl+2` | -| Focus Terminal | `Ctrl+3` | -| Focus sidebar | `Ctrl+0` | -| Next tab (in tabbed panels) | `Ctrl+Tab` | -| Previous tab | `Ctrl+Shift+Tab` | -| Close current tab/editor | `Ctrl+W` | +| Command palette | `Alt+P` | +| Quick open file | `Alt+O` | +| Select theme | `Alt+T` | +| Quit | `Alt+Q` | ### Git | Action | Binding | |--------|---------| -| Open Git panel | `Ctrl+Shift+G` | -| Stage file | `Ctrl+Enter` (in git view) | -| Unstage file | `Ctrl+Backspace` (in git view) | - -### Search & Problems - -| Action | Binding | -|--------|---------| -| Find in file | `Ctrl+F` | -| Find in project | `Ctrl+Shift+F` | -| Go to problems | `Ctrl+Shift+M` | -| Next problem | `F8` | -| Previous problem | `Shift+F8` | +| Open Git panel | `Alt+G` | ### Editor | Action | Binding | |--------|---------| -| Save | `Ctrl+S` | -| Undo | `Ctrl+Z` | -| Redo | `Ctrl+Shift+Z` | -| Go to line | `Ctrl+G` | +| Save | `Alt+S` | +| Go to line | `Alt+L` | +| Go to problems | `Alt+M` | --- ## Panel Communication -Panels should feel connected, like a normal IDE. - -### File Navigation +### File Navigation Flow ``` Sidebar file click @@ -372,7 +295,7 @@ Editor tab focused File loaded in Editor ``` -### Problems/TODOs Navigation +### Problem/TODO Navigation Flow ``` Click problem/todo item @@ -387,117 +310,57 @@ Editor tab focused File opened at specific line │ ▼ -Line highlighted/scrolled into view +Line scrolled into view ``` -### Claude Diff Flow +### Git Action Flow ``` -Claude proposes file changes +Click git action button (Commit, Stash, etc.) │ ▼ -Workspace appears +Ensure skill installed (async, with notification) │ ▼ -Diff tab focused +Send /command to Claude │ ▼ -Changes displayed with Accept/Reject - │ - ├─► Accept: Apply changes, optionally close diff - │ - └─► Reject: Discard, close diff -``` - -### Git File Actions - -``` -Click file in Git tab - │ - ▼ -Workspace appears - │ - ▼ -Diff tab shows unstaged changes - │ - ▼ -Stage/unstage from diff view +Claude executes git workflow ``` --- -## Implementation Notes +## State Management -### Recommended Textual Widgets - -| Component | Widget | -|-----------|--------| -| File browser | `DirectoryTree` | -| Claude output | `Markdown` or `RichLog` (for streaming) | -| Editor | `TextArea` (syntax highlighting built-in) | -| Tabbed panels | `TabbedContent`, `TabPane` | -| Panel switching | `ContentSwitcher` | -| Problems/TODOs list | `ListView` with `ListItem` | -| Git graph | `RichLog` or custom canvas widget | -| Command palette | `CommandPalette` (built-in) | - -### Background Tasks - -Use Textual's `@work` decorator for: -- Git status refresh -- Linter execution -- TODO scanning -- Jira CLI calls +### Reactive Properties ```python -@work(thread=True) -def refresh_git_status(self) -> None: - result = subprocess.run(["git", "status", "--porcelain"], ...) - self.call_from_thread(self.update_git_view, result.stdout) -``` - -### State Management - -**Core principle**: Hiding is not closing. All panels persist state when hidden. - -```python -class IDEApp(App): - current_file: reactive[str | None] = reactive(None) +class ClideApp(App): workspace_visible: reactive[bool] = reactive(False) problem_count: reactive[int] = reactive(0) todo_count: reactive[int] = reactive(0) + current_branch: reactive[str] = reactive("main") compact_mode: reactive[bool] = reactive(False) ``` -**Panel visibility pattern** — toggle `display`, don't destroy: - -```python -def toggle_workspace(self, visible: bool) -> None: - workspace = self.query_one("#panel-workspace") - workspace.display = visible # Retains all child state - - # Adjust Claude panel height - claude = self.query_one("#panel-claude") - claude.styles.height = "40%" if visible else "100%" -``` - -**State to preserve per panel:** +### State Preservation | Panel | Preserved State | |-------|-----------------| -| Editor | Open files, cursor positions, scroll, unsaved changes, undo history | -| Diff | Current diff content, scroll position, accept/reject state | -| Terminal | PTY session, command history, output buffer, working directory | -| Sidebar tabs | Scroll position, expanded/collapsed sections, selection | -| Context tabs | Scroll position, selected item | -| Git views | Expanded sections, selected files | +| Editor | Open file, cursor, scroll, unsaved changes | +| Diff | Current diff, scroll position | +| Terminal | Session, history, output buffer | +| Sidebar tabs | Scroll, expanded sections, selection | +| Context tabs | Scroll, selected item | --- -## Future Considerations +## Themes -- **Session persistence**: Remember open files, panel sizes, last git state -- **Multiple projects**: Workspace switcher -- **Claude history**: Browse past conversations -- **Custom themes**: User-selectable color schemes -- **Plugin system**: User-defined panels/integrations \ No newline at end of file +22 built-in themes with custom theme support. + +**Default:** summer-night (dark theme) + +Theme selection persists in user settings (`~/.clide/settings.json`). + +Custom themes can be added to `~/.clide/themes/` as TOML files. diff --git a/docs/user-manual.md b/docs/user-manual.md new file mode 100644 index 00000000..d3fb35fd --- /dev/null +++ b/docs/user-manual.md @@ -0,0 +1,398 @@ +# Clide User Manual + +Clide is a terminal-based IDE that puts Claude Code at the center of your development workflow. This manual covers installation, daily usage, and customization. + +## Installation + +### Requirements + +- Python 3.12 or later +- Git +- Claude Code CLI installed and authenticated + +### Setup + +```bash +# Clone the repository +git clone +cd clide + +# Install dependencies and create virtual environment +make setup + +# Run Clide +make run +``` + +Or install directly: + +```bash +pip install -e . +clide +``` + +### First Run + +On first launch, Clide creates a configuration directory at `~/.clide/` for user settings. Project-specific settings are stored in `.clide/` within your project. + +## Interface Overview + +``` +┌─────────────────┬─────────────────────────┬──────────────────┐ +│ Sidebar │ Workspace │ Context │ +│ │ [Editor][Diff][Terminal]│ │ +│ [Files][Git] │ (appears when needed) │ [Jira][TODOs] │ +│ [Tree] ├─────────────────────────┤ [Problems] │ +│ │ │ │ +│ │ Claude │ │ +│ │ (always visible) │ │ +│ │ │ │ +├─────────────────┤ ├──────────────────┤ +│ ⎇ main ▾ │ │ │ +│ staged: 2 │ │ │ +└─────────────────┴─────────────────────────┴──────────────────┘ +``` + +### Panels + +**Left Sidebar** — File browser, git changes, and branch graph + +**Center** — Claude Code (always visible) and workspace panels (editor, diff, terminal) that appear when needed + +**Right Context** — Jira integration, TODO list, and problems from linters + +## Keyboard Shortcuts + +All shortcuts use `Alt` as the modifier to avoid conflicts with Claude Code input. + +### Panel Navigation + +| Action | Shortcut | +|--------|----------| +| Toggle left sidebar | `Alt+B` | +| Toggle right sidebar | `Alt+Shift+B` | +| Toggle terminal | `` Alt+` `` | +| Focus Claude | `Alt+1` | +| Focus Editor | `Alt+2` | +| Focus Terminal | `Alt+3` | +| Toggle compact mode | `Alt+C` | + +### File Operations + +| Action | Shortcut | +|--------|----------| +| Quick open file | `Alt+O` | +| Save file | `Alt+S` | +| Go to line | `Alt+L` | + +### Git Operations + +| Action | Shortcut | +|--------|----------| +| Open Git panel | `Alt+G` | +| Open Problems panel | `Alt+M` | + +### Application + +| Action | Shortcut | +|--------|----------| +| Command palette | `Alt+P` | +| Select theme | `Alt+T` | +| Quit | `Alt+Q` | + +## Working with Claude + +Claude Code runs in the center panel and is always visible. Type your prompts directly and Claude will respond with code suggestions, explanations, and file operations. + +### Git Integration + +The sidebar includes buttons for common git operations that delegate to Claude: + +- **Commit** — Claude reviews staged changes and creates a well-formatted commit +- **Stash** — Claude stashes your working changes +- **Pull** — Claude pulls with rebase and helps resolve conflicts +- **Push** — Claude pushes to remote, setting upstream if needed + +On first use, Clide installs the corresponding skill to your project's `.claude/skills/` directory. These skills guide Claude through each operation following best practices. + +### Branch Status + +The branch status bar at the bottom of the sidebar shows: + +- Current branch name +- Staged and unstaged file counts + +Click the branch name to open the branch selector: + +``` +┌─────────────────┐ +│ Recent branches │ +│ ● main │ +│ ○ feature/xyz │ +│ ○ develop │ +├─────────────────┤ +│[Checkout] [New] │ +└─────────────────┘ +``` + +## Left Sidebar + +### Files Tab + +Browse your project structure. Click a file to open it in the editor. + +- Directories expand/collapse on click +- Hidden files (starting with `.`) are shown but dimmed +- Noisy directories (`.git`, `__pycache__`, `node_modules`) are filtered + +### Git Tab + +View staged and unstaged changes: + +``` +Staged (2) + + src/new_file.py + ~ src/modified.py + +Unstaged (3) + ~ README.md + ? untracked.txt + - deleted.py +``` + +**Status indicators:** +- `+` Added +- `~` Modified +- `-` Deleted +- `?` Untracked +- `→` Renamed + +Click a file to view its diff. Use the action buttons to commit, stash, pull, or push via Claude. + +### Tree Tab + +Visual git graph showing branch history: + +``` +● main: Latest commit message +│ +├─● feature: Feature work +│ +●─┴ Merge branch 'feature' +``` + +**Commit types:** +- `●` Regular commit +- `◆` Merge commit + +## Right Context Panel + +### Jira Tab + +Displays Jira issues when configured. Click the refresh button to update. + +Configure Jira integration in settings: + +```json +{ + "jira_enabled": true, + "jira_cli_path": "jira" +} +``` + +### TODOs Tab + +Scans your codebase for TODO comments and project tasks. + +**Sub-tabs:** + +- **Project** — Items from `TODO.md` (checkbox format) +- **Comments** — TODO/FIXME/HACK/XXX comments in code + +Click an item to jump to that location in the editor. + +**Supported comment markers:** +- `TODO` — Tasks to complete +- `FIXME` — Bugs to fix +- `HACK` — Temporary solutions +- `XXX` — Dangerous or problematic code +- `NOTE` — Important notes +- `BUG` — Known bugs + +### Problems Tab + +Displays linter errors and warnings. Click a problem to jump to the source location. + +## Workspace Panels + +The workspace appears when you need to view or edit files. It contains three tabs: + +### Editor + +Full-featured code editor with: + +- Syntax highlighting (Python, JavaScript, TypeScript, HTML, CSS, JSON, YAML, Markdown, and more) +- Line numbers +- Current line highlighting + +### Diff + +Side-by-side diff viewer for reviewing changes. Used when: + +- Viewing git changes +- Reviewing Claude's proposed edits + +### Terminal + +Command-line terminal for running commands. Output is preserved when the panel is hidden. + +## Themes + +Clide includes 22 built-in themes. Press `Alt+T` to open the theme selector. + +**Theme categories:** + +| Category | Themes | +|----------|--------| +| Core | summer-night (default), summer-day | +| Popular | one-dark, one-dark-pro, one-light, dracula, nord, gruvbox-dark, gruvbox-light | +| Seasonal | winter-is-coming, monokai-winter, fall, dark-autumn | +| Halloween | all-hallows-eve, halloween | +| Christmas | christmas, santa-baby | +| Hacker | pro-hacker, hacker-style | +| Other | gamma, one-dark-teal, houston | + +Your theme choice is saved and persists across sessions. + +### Custom Themes + +Create custom themes in `~/.clide/themes/`: + +```toml +# ~/.clide/themes/my-theme.toml +name = "my-theme" +display_name = "My Custom Theme" +dark = true + +[colors] +primary = "#007acc" +secondary = "#3c3c3c" +accent = "#0e639c" +background = "#1e1e1e" +surface = "#252526" +panel = "#2d2d30" +foreground = "#d4d4d4" +success = "#4ec9b0" +warning = "#dcdcaa" +error = "#f44747" +``` + +## Configuration + +### User Settings + +Settings are stored in `~/.clide/settings.json`: + +```json +{ + "theme": "summer-night", + "compact_mode": false, + "jira_enabled": false, + "jira_cli_path": "jira" +} +``` + +### Project Settings + +Project-specific settings in `.clide/`: + +``` +.clide/ +├── settings.json # Project overrides +└── skills/ # Installed Claude skills + ├── commit/ + ├── stash/ + └── ... +``` + +### Environment Variables + +Override settings with environment variables prefixed with `CLIDE_`: + +```bash +CLIDE_THEME=dracula clide +``` + +## Compact Mode + +Press `Alt+C` to toggle compact mode, which hides both sidebars for focused work. All panel state is preserved—nothing is lost when hiding panels. + +## Project TODOs + +Clide integrates with a `TODO.md` file in your project root. Format: + +```markdown +# TODO + +## Features + +- [ ] Implement user authentication +- [ ] Add search functionality +- [x] Set up database connection + +## Bugs + +- [ ] Fix login redirect +``` + +Items appear in the TODOs panel, grouped by section. Click to jump to that line. Check off items directly in the file. + +If no `TODO.md` exists, click "Create TODO.md" in the TODOs panel to generate a template. + +## Tips + +### Efficient Navigation + +1. Use `Alt+1/2/3` to quickly switch between Claude, Editor, and Terminal +2. Click items in Problems or TODOs to jump directly to source +3. Use compact mode (`Alt+C`) when you need more space for Claude + +### Git Workflow + +1. Make changes to your code +2. Review changes in the Git tab +3. Click "Commit" to have Claude create a well-formatted commit +4. Use "Push" when ready to share + +### Working with Claude + +- Claude sees your project context automatically +- Use the git action buttons for consistent commit messages +- Click files in the sidebar to show Claude what you're working on + +## Troubleshooting + +### Claude Code not starting + +Ensure Claude Code CLI is installed and authenticated: + +```bash +claude --version +claude auth status +``` + +### Theme not applying + +Check that the theme name in settings matches exactly. Theme names are case-sensitive. + +### Skills not working + +Skills are installed to `.claude/skills/` in your project. If a skill fails: + +1. Check that the skill folder exists +2. Verify `SKILL.md` is present +3. Try removing and re-triggering the action + +### Panels not updating + +Try refreshing with the relevant shortcut or clicking the refresh button. File changes should update automatically via file watching. diff --git a/pyproject.toml b/pyproject.toml index 7f63c392..13539e98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ asyncio_default_fixture_loop_scope = "function" addopts = [ "-v", "--tb=short", + "--snapshot-report=logs/snapshot_report.html", ] [tool.mypy] @@ -93,6 +94,7 @@ exclude = ["tests", "dist", "build"] target-version = "py312" line-length = 100 src = ["clide", "tests"] +exclude = ["clide/vendor"] [tool.ruff.lint] select = [ @@ -116,6 +118,8 @@ ignore = [ "SIM105", # contextlib.suppress (try-except-pass is clearer in context) "SIM115", # context handler for files (sometimes not applicable) "PTH123", # Path.open() vs open() (not always cleaner) + "TCH002", # move third-party imports to TYPE_CHECKING (clutters code) + "TCH003", # move stdlib imports to TYPE_CHECKING (clutters code) ] [tool.ruff.lint.isort] diff --git a/tests/snapshots/__snapshots__/test_app_snapshots/test_initial_layout.svg b/tests/snapshots/__snapshots__/test_app_snapshots/test_initial_layout.svg index fb3f0e79..6af352f6 100644 --- a/tests/snapshots/__snapshots__/test_app_snapshots/test_initial_layout.svg +++ b/tests/snapshots/__snapshots__/test_app_snapshots/test_initial_layout.svg @@ -180,10 +180,10 @@ - + Clide — Claude Code IDE -FilesGitTreeJiraTODOs (96)Problems (1 +FilesGitTreeJiraTODOs (87)Problems (3 ━━━━━╺━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━ ▾ clide ├── ▸ .claude @@ -195,14 +195,14 @@ ├── ◦ .coverageCLIDE_JIRA_ENABLED=true ├── ◦ .gitignore ├── ◦ .pre-commit-config. -├── ◦ claude-authentik-cr -├── ◦ CLAUDE.md -├── ◦ firebase-debug.log +├── ◦ CHANGELOG.md +├── ◦ claude-authentik-cr +├── ◦ CLAUDE.md ├── ◦ Makefile ├── ◦ pyproject.toml ├── ◦ README.md -├── ◦ snapshot_report.htm -└── ◦ TODO.md +└── ◦ TODO.md + diff --git a/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_left_panel.svg b/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_left_panel.svg index fb3f0e79..6af352f6 100644 --- a/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_left_panel.svg +++ b/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_left_panel.svg @@ -180,10 +180,10 @@ - + Clide — Claude Code IDE -FilesGitTreeJiraTODOs (96)Problems (1 +FilesGitTreeJiraTODOs (87)Problems (3 ━━━━━╺━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━ ▾ clide ├── ▸ .claude @@ -195,14 +195,14 @@ ├── ◦ .coverageCLIDE_JIRA_ENABLED=true ├── ◦ .gitignore ├── ◦ .pre-commit-config. -├── ◦ claude-authentik-cr -├── ◦ CLAUDE.md -├── ◦ firebase-debug.log +├── ◦ CHANGELOG.md +├── ◦ claude-authentik-cr +├── ◦ CLAUDE.md ├── ◦ Makefile ├── ◦ pyproject.toml ├── ◦ README.md -├── ◦ snapshot_report.htm -└── ◦ TODO.md +└── ◦ TODO.md + diff --git a/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_right_panel.svg b/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_right_panel.svg index fb3f0e79..6af352f6 100644 --- a/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_right_panel.svg +++ b/tests/snapshots/__snapshots__/test_app_snapshots/test_layout_without_right_panel.svg @@ -180,10 +180,10 @@ - + Clide — Claude Code IDE -FilesGitTreeJiraTODOs (96)Problems (1 +FilesGitTreeJiraTODOs (87)Problems (3 ━━━━━╺━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━ ▾ clide ├── ▸ .claude @@ -195,14 +195,14 @@ ├── ◦ .coverageCLIDE_JIRA_ENABLED=true ├── ◦ .gitignore ├── ◦ .pre-commit-config. -├── ◦ claude-authentik-cr -├── ◦ CLAUDE.md -├── ◦ firebase-debug.log +├── ◦ CHANGELOG.md +├── ◦ claude-authentik-cr +├── ◦ CLAUDE.md ├── ◦ Makefile ├── ◦ pyproject.toml ├── ◦ README.md -├── ◦ snapshot_report.htm -└── ◦ TODO.md +└── ◦ TODO.md +