aider-webui 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LX_Aider Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: aider-webui
3
+ Version: 0.1.0
4
+ Summary: A modern web interface for Aider CLI — split-view Chat UI + Terminal.
5
+ Author: LX_Aider Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/LX-Aider/aider-webui
8
+ Project-URL: Repository, https://github.com/LX-Aider/aider-webui
9
+ Keywords: aider,webui,ai,coding,assistant,terminal
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: User Interfaces
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: nicegui>=3.8.0
26
+ Requires-Dist: python-dotenv>=1.0.0
27
+ Requires-Dist: pywinpty>=2.0.0; sys_platform == "win32"
28
+ Dynamic: license-file
29
+
30
+ # Aider WebUI
31
+
32
+ A modern web interface for [Aider CLI](https://aider.chat) — split-view **Chat UI** + **Terminal**.
33
+
34
+ ## Features
35
+
36
+ - 💬 **Chat History** — Clean Markdown rendering of AI conversations
37
+ - ⬛ **Full Terminal** — Real xterm.js terminal with ANSI color support
38
+ - 🔀 **Split View** — Resizable panels for chat and terminal side-by-side
39
+ - 🔒 **State Sync** — Input locks while Aider is processing
40
+ - 📁 **File Picker** — Tree-based file browser for `/add`, `/read-only`, `/run` commands
41
+ - 🧩 **Session Isolation** — Each browser tab gets its own PTY process
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install aider-webui
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ 1. Make sure [Aider](https://aider.chat) is installed and accessible in your PATH.
52
+
53
+ 2. Create a `.env` file in your working directory (optional):
54
+
55
+ ```env
56
+ AIDER_COMMAND=aider
57
+ WORKING_DIR=.
58
+ ANTHROPIC_API_KEY=sk-...
59
+ ```
60
+
61
+ 3. Run:
62
+
63
+ ```bash
64
+ aider-webui
65
+ ```
66
+
67
+ 4. Open `http://localhost:8080` in your browser.
68
+
69
+ ## Usage
70
+
71
+ ```bash
72
+ # Run with default settings
73
+ aider-webui
74
+
75
+ # Or run as a Python module
76
+ python -m aider_webui
77
+ ```
78
+
79
+ ## Requirements
80
+
81
+ - Python 3.10+
82
+ - [Aider CLI](https://aider.chat) installed and accessible
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,57 @@
1
+ # Aider WebUI
2
+
3
+ A modern web interface for [Aider CLI](https://aider.chat) — split-view **Chat UI** + **Terminal**.
4
+
5
+ ## Features
6
+
7
+ - 💬 **Chat History** — Clean Markdown rendering of AI conversations
8
+ - ⬛ **Full Terminal** — Real xterm.js terminal with ANSI color support
9
+ - 🔀 **Split View** — Resizable panels for chat and terminal side-by-side
10
+ - 🔒 **State Sync** — Input locks while Aider is processing
11
+ - 📁 **File Picker** — Tree-based file browser for `/add`, `/read-only`, `/run` commands
12
+ - 🧩 **Session Isolation** — Each browser tab gets its own PTY process
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install aider-webui
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ 1. Make sure [Aider](https://aider.chat) is installed and accessible in your PATH.
23
+
24
+ 2. Create a `.env` file in your working directory (optional):
25
+
26
+ ```env
27
+ AIDER_COMMAND=aider
28
+ WORKING_DIR=.
29
+ ANTHROPIC_API_KEY=sk-...
30
+ ```
31
+
32
+ 3. Run:
33
+
34
+ ```bash
35
+ aider-webui
36
+ ```
37
+
38
+ 4. Open `http://localhost:8080` in your browser.
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ # Run with default settings
44
+ aider-webui
45
+
46
+ # Or run as a Python module
47
+ python -m aider_webui
48
+ ```
49
+
50
+ ## Requirements
51
+
52
+ - Python 3.10+
53
+ - [Aider CLI](https://aider.chat) installed and accessible
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "setuptools-scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aider-webui"
7
+ dynamic = ["version"]
8
+ description = "A modern web interface for Aider CLI — split-view Chat UI + Terminal."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "LX_Aider Contributors"},
14
+ ]
15
+ keywords = ["aider", "webui", "ai", "coding", "assistant", "terminal"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Web Environment",
19
+ "Framework :: FastAPI",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Software Development :: User Interfaces",
29
+ ]
30
+ dependencies = [
31
+ "nicegui>=3.8.0",
32
+ "python-dotenv>=1.0.0",
33
+ "pywinpty>=2.0.0; sys_platform == 'win32'",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/LX-Aider/aider-webui"
38
+ Repository = "https://github.com/LX-Aider/aider-webui"
39
+
40
+ [project.scripts]
41
+ aider-webui = "aider_webui.main:run"
42
+
43
+ [tool.setuptools.dynamic]
44
+ version = {attr = "aider_webui.__version__"}
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ aider_webui = ["static/**"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Aider WebUI — Web interface for Aider CLI."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m aider_webui`."""
2
+
3
+ from aider_webui.main import run
4
+
5
+ run()
@@ -0,0 +1,104 @@
1
+ """
2
+ Chat history parser.
3
+ Parses Aider's .aider.chat.history.md file into structured messages.
4
+
5
+ Aider's actual format:
6
+ #### <user message text>
7
+
8
+ <assistant response text>
9
+
10
+ > Tokens: ... Cost: ...
11
+
12
+ Lines starting with '>' are aider system/meta lines (costs, prompts, etc.)
13
+ Lines starting with '# aider chat started' are session markers.
14
+ Lines starting with '####' are user messages.
15
+ Everything else (non-blank, non-meta) after a user message is the assistant response.
16
+ """
17
+
18
+ import re
19
+ from dataclasses import dataclass
20
+
21
+
22
+ @dataclass
23
+ class ChatMessage:
24
+ """Represents a single chat message."""
25
+ role: str # "user" or "assistant"
26
+ content: str # Markdown content
27
+
28
+ def __eq__(self, other):
29
+ if not isinstance(other, ChatMessage):
30
+ return NotImplemented
31
+ return self.role == other.role and self.content == other.content
32
+
33
+ def __hash__(self):
34
+ return hash((self.role, self.content))
35
+
36
+ def __repr__(self):
37
+ preview = self.content[:50].replace("\n", "\\n")
38
+ return f"ChatMessage(role={self.role!r}, content={preview!r}...)"
39
+
40
+
41
+ # Matches user prompt lines: #### <message>
42
+ _USER_PROMPT_RE = re.compile(r"^####\s+(.+)$", re.MULTILINE)
43
+
44
+ # Matches aider meta/system lines (starting with >)
45
+ _META_LINE_RE = re.compile(r"^>\s+", re.MULTILINE)
46
+
47
+ # Matches session start markers
48
+ _SESSION_RE = re.compile(r"^#\s+aider chat started", re.MULTILINE)
49
+
50
+
51
+ def parse_history(text: str) -> list[ChatMessage]:
52
+ """
53
+ Parse the full chat history text into a list of ChatMessages.
54
+
55
+ Aider writes chat history in this format:
56
+ #### <user message>
57
+
58
+ <assistant response lines>
59
+
60
+ > Tokens: ... Cost: ...
61
+ """
62
+ messages: list[ChatMessage] = []
63
+ user_matches = list(_USER_PROMPT_RE.finditer(text))
64
+
65
+ for i, match in enumerate(user_matches):
66
+ # User message is the text after ####
67
+ user_content = match.group(1).strip()
68
+ # Skip raw slash-command entries (e.g. "/ask how are you").
69
+ # Aider writes both the raw command and the processed text;
70
+ # we only want the processed version.
71
+ if user_content and not user_content.startswith("/"):
72
+ messages.append(ChatMessage(role="user", content=user_content))
73
+
74
+ # Assistant response is everything between this user prompt
75
+ # and the next user prompt (or end of text),
76
+ # excluding meta lines (>) and session markers (#)
77
+ resp_start = match.end()
78
+ resp_end = user_matches[i + 1].start() if i + 1 < len(user_matches) else len(text)
79
+ resp_block = text[resp_start:resp_end]
80
+
81
+ # Extract assistant lines: skip blank, meta (>), and session (#) lines
82
+ assistant_lines = []
83
+ for line in resp_block.split("\n"):
84
+ stripped = line.strip()
85
+ if not stripped:
86
+ # Keep blank lines within a response block (for formatting)
87
+ if assistant_lines:
88
+ assistant_lines.append("")
89
+ continue
90
+ if stripped.startswith(">"):
91
+ continue
92
+ if stripped.startswith("# aider chat"):
93
+ continue
94
+ assistant_lines.append(line.rstrip())
95
+
96
+ # Strip trailing blank lines
97
+ while assistant_lines and not assistant_lines[-1].strip():
98
+ assistant_lines.pop()
99
+
100
+ assistant_content = "\n".join(assistant_lines).strip()
101
+ if assistant_content:
102
+ messages.append(ChatMessage(role="assistant", content=assistant_content))
103
+
104
+ return messages
@@ -0,0 +1,131 @@
1
+ """
2
+ Chat history file poller.
3
+ Polls .aider.chat.history.md for changes and dispatches new messages.
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Callable, Optional
11
+
12
+ from .chat_parser import ChatMessage, parse_history
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ChatPoller:
18
+ """
19
+ Async task that polls .aider.chat.history.md every 500ms.
20
+ When new content is detected, parses delta and calls the callback.
21
+
22
+ Handles the edge case where Aider truncates and rewrites the file
23
+ by tracking _last_msg_count instead of relying on content prefix matching.
24
+ """
25
+
26
+ POLL_INTERVAL = 0.5 # seconds
27
+
28
+ def __init__(
29
+ self,
30
+ history_file: str,
31
+ on_new_messages: Callable[[list[ChatMessage]], None],
32
+ ):
33
+ self._file_path = Path(history_file)
34
+ self._on_new_messages = on_new_messages
35
+ self._task: Optional[asyncio.Task] = None
36
+ self._last_mtime: float = 0
37
+ self._last_size: int = 0
38
+ self._last_msg_count: int = 0
39
+ self._last_messages: list[ChatMessage] = []
40
+
41
+ def start(self):
42
+ """Start the polling asyncio task."""
43
+ if self._task and not self._task.done():
44
+ logger.warning("Poller already running")
45
+ return
46
+
47
+ self._task = asyncio.create_task(self._poll_loop())
48
+ logger.info(f"Chat poller started: {self._file_path}")
49
+
50
+ def stop(self):
51
+ """Stop the polling task."""
52
+ if self._task and not self._task.done():
53
+ self._task.cancel()
54
+ logger.info("Chat poller stopped")
55
+
56
+ async def _poll_loop(self):
57
+ """Main polling loop."""
58
+ try:
59
+ while True:
60
+ await self._check_file()
61
+ await asyncio.sleep(self.POLL_INTERVAL)
62
+ except asyncio.CancelledError:
63
+ pass
64
+ except Exception as e:
65
+ logger.error(f"Chat poller error: {e}", exc_info=True)
66
+
67
+ async def _check_file(self):
68
+ """Check if the history file has changed.
69
+
70
+ Always re-parses the full file and uses index-based tracking
71
+ to determine truly new messages. This avoids ordering bugs
72
+ caused by delta-only parsing of partial/growing responses.
73
+ """
74
+ if not self._file_path.exists():
75
+ return
76
+
77
+ try:
78
+ stat = self._file_path.stat()
79
+ mtime = stat.st_mtime
80
+ size = stat.st_size
81
+
82
+ # Skip if no change
83
+ if mtime == self._last_mtime and size == self._last_size:
84
+ return
85
+
86
+ self._last_mtime = mtime
87
+ self._last_size = size
88
+
89
+ # Read and parse the FULL file every time
90
+ content = await asyncio.to_thread(
91
+ self._file_path.read_text, "utf-8"
92
+ )
93
+ all_messages = parse_history(content)
94
+
95
+ new_messages: list[ChatMessage] = []
96
+
97
+ if len(all_messages) > self._last_msg_count:
98
+ # New messages were added
99
+ new_messages = all_messages[self._last_msg_count:]
100
+ elif len(all_messages) < self._last_msg_count:
101
+ # File was truncated — reset tracking
102
+ logger.warning(
103
+ f"History file shrank: {self._last_msg_count} → "
104
+ f"{len(all_messages)} messages, resetting count"
105
+ )
106
+ self._last_msg_count = len(all_messages)
107
+ self._last_messages = list(all_messages)
108
+ return
109
+ elif (
110
+ all_messages
111
+ and self._last_messages
112
+ and all_messages[-1] != self._last_messages[-1]
113
+ ):
114
+ # Same count but last message content changed (still growing)
115
+ # Send as an update so the UI can refresh it in-place
116
+ new_messages = [all_messages[-1]]
117
+ # Don't increment _last_msg_count — count hasn't changed
118
+ self._last_messages = list(all_messages)
119
+ if new_messages:
120
+ self._on_new_messages(new_messages)
121
+ return
122
+
123
+ self._last_msg_count = len(all_messages)
124
+ self._last_messages = list(all_messages)
125
+
126
+ if new_messages:
127
+ self._on_new_messages(new_messages)
128
+
129
+ except Exception as e:
130
+ logger.error(f"Error reading history file: {e}")
131
+
@@ -0,0 +1,61 @@
1
+ """
2
+ Environment configuration manager.
3
+ Loads settings from .env file and provides them to PTY processes.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from dotenv import load_dotenv
9
+
10
+
11
+ class ConfigManager:
12
+ """Manages environment configuration for Aider CLI sessions."""
13
+
14
+ # Keys that will be passed to PTY environment
15
+ PASSTHROUGH_KEYS = [
16
+ "ANTHROPIC_API_KEY",
17
+ "ANTHROPIC_BASE_URL",
18
+ "OPENAI_API_KEY",
19
+ "OPENAI_BASE_URL",
20
+ ]
21
+
22
+ def __init__(self, env_file: str = ".env"):
23
+ self._env_file = Path(env_file)
24
+ self._config: dict[str, str] = {}
25
+ self._load()
26
+
27
+ def _load(self):
28
+ """Load configuration from .env file."""
29
+ if self._env_file.exists():
30
+ load_dotenv(self._env_file, override=True)
31
+
32
+ self._config = {
33
+ "AIDER_COMMAND": os.getenv("AIDER_COMMAND", "aider"),
34
+ "WORKING_DIR": os.getenv("WORKING_DIR", "."),
35
+ }
36
+
37
+ # Load API keys / base URLs if set
38
+ for key in self.PASSTHROUGH_KEYS:
39
+ value = os.getenv(key)
40
+ if value:
41
+ self._config[key] = value
42
+
43
+ @property
44
+ def aider_command(self) -> str:
45
+ return self._config["AIDER_COMMAND"]
46
+
47
+ @property
48
+ def working_dir(self) -> str:
49
+ return str(Path(self._config["WORKING_DIR"]).resolve())
50
+
51
+ def get_env_dict(self) -> dict[str, str]:
52
+ """Return environment variables dict to pass to PTY process."""
53
+ env = os.environ.copy()
54
+ for key in self.PASSTHROUGH_KEYS:
55
+ if key in self._config:
56
+ env[key] = self._config[key]
57
+ return env
58
+
59
+ def reload(self):
60
+ """Reload configuration from .env file."""
61
+ self._load()