socratic-debug 0.0.1__py3-none-any.whl

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,38 @@
1
+ class SolutionPrompt:
2
+ @staticmethod
3
+ def solution_prompt(
4
+ error_type: str,
5
+ line_number: int,
6
+ code_snippet: str,
7
+ root_cause: str,
8
+ eval_status: str = "solved",
9
+ language: str = "English",
10
+ style: str = "friendly",
11
+ ) -> str:
12
+ return f"""You are a Socratic Coding Mentor wrapping up a debugging session.
13
+
14
+ Session Context:
15
+ - Language: Respond strictly in {language}.
16
+ - Style/Tone: {style}.
17
+ - Final Status: {eval_status.upper()} (The developer {'successfully understood and solved the bug' if eval_status == 'solved' else 'struggled or surrendered'}).
18
+ - Error Type: {error_type}
19
+ - Line #{line_number}
20
+ - Known Root Cause:
21
+ {root_cause}
22
+
23
+ Original Code Context:
24
+ ```python
25
+ {code_snippet}
26
+ ```
27
+
28
+ Instructions:
29
+ 1. If status is SOLVED:
30
+ - Enthusiastically praise the developer for discovering and understanding the bug themselves.
31
+ - Summarize the core takeaway lesson in one memorable sentence.
32
+ 2. If status is SURRENDERED:
33
+ - Be empathetic, encouraging, and clear.
34
+ - Concisely explain the concept so they understand why it happened.
35
+ 3. IN BOTH CASES:
36
+ - Provide a clean, idiomatic Python code snippet demonstrating the "Best Practice" or defensive programming way to prevent this bug (e.g., using default values, proper guards, or cleaner logic).
37
+ - Keep the entire response concise and under 250 words.
38
+ """
src/agent/runner.py ADDED
@@ -0,0 +1,21 @@
1
+ from src.extractor import extract_crash_context
2
+ from src.agent.graph.graph import SocraticGraph
3
+
4
+ def run_socratic_agent(exception_type, exception_value, exception_traceback):
5
+ context = extract_crash_context(exception_traceback)
6
+
7
+ initial_state = {
8
+ "file_path": context["file_path"],
9
+ "function_name": context["function_name"],
10
+ "line_number": context["line_number"],
11
+ "line_content": context["line_content"],
12
+ "code_snippet": context["code_snippet"],
13
+ "error_type": exception_type.__name__,
14
+ "error_name": str(exception_value),
15
+ "root_cause": "",
16
+ "dialogue_history": [],
17
+ "is_solved": False,
18
+ }
19
+
20
+ app = SocraticGraph.build_graph()
21
+ app.invoke(initial_state)
@@ -0,0 +1,21 @@
1
+ from typing import TypedDict, List, Dict, Any, Optional
2
+
3
+
4
+ class SocraticAgentState(TypedDict):
5
+ # data about the problem
6
+ file_path: str
7
+ function_name: Optional[str]
8
+ line_number: int
9
+ line_content: str
10
+ code_snippet: str
11
+ error_type: str
12
+ error_name: str
13
+
14
+ # root cause of the problem
15
+ root_cause: str
16
+
17
+ # chat with user
18
+ dialogue_history: List[Dict[str, str]]
19
+
20
+ # dose problem is solved or not !
21
+ is_solved: bool
src/cli.py ADDED
@@ -0,0 +1,98 @@
1
+ import sys
2
+ import os
3
+ from pathlib import Path
4
+
5
+ # Add project root to sys.path so modules and UI resolve everywhere
6
+ project_root = str(Path(__file__).resolve().parent.parent)
7
+ if project_root not in sys.path:
8
+ sys.path.insert(0, project_root)
9
+
10
+ from src.hook import install
11
+ from ui.console import console
12
+
13
+
14
+ def print_usage():
15
+ console.print("""
16
+ [bold cyan]🧠 Socratic Debug — Autonomous Pedagogical Debugger[/bold cyan]
17
+
18
+ [bold yellow]Usage:[/bold yellow]
19
+ [green]socratic run <script.py>[/green] Run a Python file under Socratic supervision
20
+ [green]socratic init[/green] Create a default .socratic.yaml config file
21
+ [green]socratic --help[/green] Show this help message
22
+ """)
23
+
24
+
25
+ def run_target_script(script_path: str, script_args: list):
26
+ resolved_path = Path(script_path).resolve()
27
+ if not resolved_path.exists():
28
+ console.print(f"[bold red]Error:[/bold red] File not found: [yellow]{resolved_path}[/yellow]")
29
+ sys.exit(1)
30
+
31
+ # Automatically install Socratic hook before user code executes
32
+ install()
33
+
34
+ # Prepare execution environment simulating direct `python <script.py>`
35
+ sys.argv = [str(resolved_path)] + script_args
36
+ sys.path.insert(0, str(resolved_path.parent))
37
+
38
+ # Read and execute target code in its own module namespace
39
+ with open(resolved_path, "r", encoding="utf-8") as f:
40
+ code_str = f.read()
41
+
42
+ namespace = {
43
+ "__name__": "__main__",
44
+ "__file__": str(resolved_path),
45
+ "__doc__": None,
46
+ }
47
+
48
+ try:
49
+ exec(compile(code_str, str(resolved_path), "exec"), namespace)
50
+ except SystemExit:
51
+ raise
52
+ except Exception:
53
+ # sys.excepthook will catch this cleanly and launch Socratic GUI!
54
+ sys.excepthook(*sys.exc_info())
55
+
56
+
57
+ def main():
58
+ if len(sys.argv) < 2:
59
+ print_usage()
60
+ sys.exit(0)
61
+
62
+ command = sys.argv[1].lower()
63
+
64
+ if command in ("-h", "--help", "help"):
65
+ print_usage()
66
+ sys.exit(0)
67
+
68
+ elif command == "init":
69
+ from src.config_creator import create_config
70
+ create_config()
71
+ console.print("[bold green]✔[/bold green] Created [cyan].socratic.yaml[/cyan] with default settings.")
72
+ sys.exit(0)
73
+
74
+ elif command == "session":
75
+ # Internal command executed inside the dedicated popup window
76
+ target_file = sys.argv[2]
77
+ extra_args = sys.argv[3:]
78
+ run_target_script(target_file, extra_args, force_in_process=True)
79
+
80
+ elif command == "run":
81
+ if len(sys.argv) < 3:
82
+ console.print("[bold red]Error:[/bold red] Missing script file to run.")
83
+ console.print("Example: [green]socratic run app.py[/green]")
84
+ sys.exit(1)
85
+
86
+ target_file = sys.argv[2]
87
+ extra_args = sys.argv[3:]
88
+ run_target_script(target_file, extra_args)
89
+
90
+ else:
91
+ # Default fallback: if user runs `socratic script.py` directly
92
+ target_file = sys.argv[1]
93
+ extra_args = sys.argv[2:]
94
+ run_target_script(target_file, extra_args)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
src/config_creator.py ADDED
@@ -0,0 +1,32 @@
1
+ try:
2
+ from src.config_template import ConfigTemplate
3
+ except ImportError:
4
+ from config_template import ConfigTemplate
5
+ from pathlib import Path
6
+ import yaml
7
+
8
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
9
+
10
+ # make the socratic.config file
11
+ def create_config():
12
+ config_path = PROJECT_ROOT / ".socratic.yaml"
13
+
14
+ if config_path.exists():
15
+ return
16
+
17
+ with open(config_path, "w") as f:
18
+ f.write(ConfigTemplate.DEFAULT_CONFIG_TEMPLATE)
19
+
20
+ # read the socratic.config file and return the values
21
+ def read_config() -> dict:
22
+ config_path = PROJECT_ROOT / ".socratic.yaml"
23
+
24
+ if not config_path.exists():
25
+ return {}
26
+
27
+ with open(config_path, "r") as f:
28
+ return yaml.safe_load(f) or {}
29
+
30
+ if __name__ == "__main__":
31
+ create_config()
32
+ print("Config created!")
src/config_template.py ADDED
@@ -0,0 +1,21 @@
1
+ class ConfigTemplate:
2
+ DEFAULT_CONFIG_TEMPLATE = """# Socratic Configuration
3
+ # in this file you can configure the socratic debugging tool
4
+
5
+ # LLM Configurations
6
+ provider: "ollama"
7
+ provider_name: "gemma4:31b-cloud"
8
+ provider_link: "http://localhost:11434"
9
+
10
+ # API Key (the variable name must named `api_key`)
11
+ api_key: "your_api_key"
12
+
13
+ # Socratic Mentor configuration
14
+ socratic_language: "English"
15
+ socratic_style: "friendly"
16
+
17
+ # Observability
18
+ LANGSMITH_TRACING: "true"
19
+ LANGSMITH_API_KEY: "your_langsmith_api_key"
20
+ LANGSMITH_PROJECT: "socratic-debug"
21
+ """
src/extractor.py ADDED
@@ -0,0 +1,34 @@
1
+ import traceback
2
+
3
+ def extract_crash_context(exception_traceback) -> dict:
4
+ frames = traceback.extract_tb(exception_traceback)
5
+ last_frame = frames[-1]
6
+
7
+ snippet = get_code_snippet(last_frame.filename, last_frame.lineno)
8
+
9
+ return {
10
+ "file_path": last_frame.filename,
11
+ "line_number": last_frame.lineno,
12
+ "function_name": last_frame.name,
13
+ "line_content": last_frame.line,
14
+ "code_snippet": snippet
15
+ }
16
+
17
+ def get_code_snippet(file_path: str, line_number: int, context_lines: int = 5) -> str:
18
+ try:
19
+ with open(file_path, "r", encoding="utf-8") as f:
20
+ lines = f.readlines()
21
+
22
+ start_lines = max(1, line_number - context_lines)
23
+ end_lines = min(len(lines), line_number + context_lines)
24
+
25
+ snippet_lines = []
26
+ for i in range(start_lines, end_lines + 1):
27
+ prefix = ">" if i == line_number else " "
28
+ line_text = lines[i - 1]
29
+ snippet_lines.append(f"{prefix} {i:3d} | {line_text}")
30
+
31
+ return "".join(snippet_lines)
32
+
33
+ except Exception as e:
34
+ return f"Error getting code snippet: {e}"
src/hook.py ADDED
@@ -0,0 +1,67 @@
1
+ import sys
2
+ import os
3
+ import threading
4
+ from src.extractor import extract_crash_context
5
+ from src.config_creator import create_config
6
+ from src.agent.runner import run_socratic_agent
7
+ from ui.views import SocraticUI
8
+
9
+
10
+ def socratic_excepthook(exception_type, exception_value, exception_traceback):
11
+ """Intercepts uncaught exceptions and launches the Socratic GUI Terminal window."""
12
+ context = extract_crash_context(exception_traceback)
13
+
14
+ # Check if graphical desktop display is available
15
+ has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY") or sys.platform == "win32")
16
+
17
+ if has_display:
18
+ try:
19
+ from PyQt6.QtWidgets import QApplication
20
+ from ui.app import SocraticWindow
21
+
22
+ os.environ["SOCRATIC_GUI_ACTIVE"] = "1"
23
+
24
+ app = QApplication.instance()
25
+ if app is None:
26
+ app = QApplication(sys.argv)
27
+
28
+ # Create and display the centered Socratic Terminal window
29
+ window = SocraticWindow(
30
+ error_type=exception_type.__name__,
31
+ error_message=str(exception_value),
32
+ file_path=context["file_path"],
33
+ line_number=context["line_number"],
34
+ line_content=context["line_content"],
35
+ )
36
+ window.show()
37
+
38
+ # Run LangGraph State Machine in background worker thread
39
+ agent_thread = threading.Thread(
40
+ target=run_socratic_agent,
41
+ args=(exception_type, exception_value, exception_traceback),
42
+ daemon=True,
43
+ )
44
+ agent_thread.start()
45
+
46
+ # Run Qt GUI event loop
47
+ app.exec()
48
+ return
49
+ except Exception as e:
50
+ # Fallback to terminal mode if GUI setup fails
51
+ pass
52
+
53
+ # Headless / Terminal Fallback Mode
54
+ os.environ["SOCRATIC_GUI_ACTIVE"] = "0"
55
+ SocraticUI.display_crash_banner(
56
+ error_type=exception_type.__name__,
57
+ error_message=str(exception_value),
58
+ file_path=context["file_path"],
59
+ line_number=context["line_number"],
60
+ line_content=context["line_content"],
61
+ )
62
+ run_socratic_agent(exception_type, exception_value, exception_traceback)
63
+
64
+ # use the hook
65
+ def install():
66
+ create_config()
67
+ sys.excepthook = socratic_excepthook
src/utils/read_yaml.py ADDED
@@ -0,0 +1,36 @@
1
+ from src.config_creator import create_config
2
+ from pathlib import Path
3
+ import yaml
4
+ import os
5
+
6
+
7
+ class ReadYaml:
8
+ @staticmethod
9
+ def get_config_path() -> Path:
10
+ # 1. First check in current working directory
11
+ cwd_file = Path.cwd() / ".socratic.yaml"
12
+ if cwd_file.exists():
13
+ return cwd_file
14
+
15
+ # 2. Check in project root
16
+ project_root = Path(__file__).resolve().parent.parent.parent
17
+ root_file = project_root / ".socratic.yaml"
18
+ if root_file.exists():
19
+ return root_file
20
+
21
+ # 3. Fallback: auto-create in project root and return
22
+ create_config()
23
+ return root_file if root_file.exists() else cwd_file
24
+
25
+ @staticmethod
26
+ def read_yaml() -> dict:
27
+ config_path = ReadYaml.get_config_path()
28
+ try:
29
+ with open(config_path, "r", encoding="utf-8") as file:
30
+ data = yaml.safe_load(file) or {}
31
+ for key in ["LANGSMITH_TRACING", "LANGSMITH_API_KEY", "LANGSMITH_PROJECT"]:
32
+ if data.get(key):
33
+ os.environ[key] = str(data[key])
34
+ return data
35
+ except Exception:
36
+ return {}
ui/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from ui.app import SocraticWindow, bridge, wait_for_gui_user_input
2
+ from ui.views import SocraticUI
3
+ from ui.translations import TRANSLATIONS
4
+ from ui.styles import SOCRATIC_DARK_THEME
5
+ from ui.templates import ChatTemplates
6
+
7
+ __all__ = [
8
+ "SocraticWindow",
9
+ "SocraticUI",
10
+ "bridge",
11
+ "wait_for_gui_user_input",
12
+ "TRANSLATIONS",
13
+ "SOCRATIC_DARK_THEME",
14
+ "ChatTemplates",
15
+ ]
ui/app.py ADDED
@@ -0,0 +1,203 @@
1
+ import sys
2
+ import threading
3
+ from PyQt6.QtWidgets import (
4
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
5
+ QTextEdit, QLineEdit, QPushButton, QLabel, QFrame
6
+ )
7
+ from PyQt6.QtCore import Qt, pyqtSignal, QObject
8
+ from PyQt6.QtGui import QTextCursor
9
+
10
+ from src.utils.read_yaml import ReadYaml
11
+ from ui.translations import TRANSLATIONS
12
+ from ui.styles import SOCRATIC_DARK_THEME
13
+ from ui.templates import ChatTemplates
14
+
15
+
16
+ class UIBridge(QObject):
17
+ """Bridge to pass messages from background agent threads to the Qt GUI thread."""
18
+ mentor_message_received = pyqtSignal(str, int)
19
+ user_input_requested = pyqtSignal()
20
+ solution_received = pyqtSignal(str)
21
+ evaluator_verdict_received = pyqtSignal(str)
22
+
23
+
24
+ bridge = UIBridge()
25
+ _user_reply_event = threading.Event()
26
+ _user_reply_text = ""
27
+
28
+
29
+ def wait_for_gui_user_input() -> str:
30
+ _user_reply_event.clear()
31
+ bridge.user_input_requested.emit()
32
+ _user_reply_event.wait()
33
+ return _user_reply_text
34
+
35
+
36
+ def submit_gui_user_input(text: str):
37
+ global _user_reply_text
38
+ _user_reply_text = text
39
+ _user_reply_event.set()
40
+
41
+
42
+ class SocraticWindow(QMainWindow):
43
+ """Pure, clean, fully-localized Socratic Terminal window."""
44
+
45
+ def __init__(self, error_type: str, error_message: str, file_path: str, line_number: int, line_content: str):
46
+ super().__init__()
47
+
48
+ # Load language setting from .socratic.yaml
49
+ config = ReadYaml.read_yaml()
50
+ lang_setting = config.get("socratic_language", "English").strip().lower()
51
+ self.is_arabic = "arab" in lang_setting
52
+ self.translations = TRANSLATIONS["arabic"] if self.is_arabic else TRANSLATIONS["english"]
53
+
54
+ self.setWindowTitle(self.translations["app_title"])
55
+ self.resize(880, 640)
56
+
57
+ # Dynamic RTL / LTR layout
58
+ self.setLayoutDirection(Qt.LayoutDirection.RightToLeft if self.is_arabic else Qt.LayoutDirection.LeftToRight)
59
+
60
+ self._center_on_screen()
61
+ self.setStyleSheet(SOCRATIC_DARK_THEME)
62
+ self._build_ui(error_type, error_message, file_path, line_number, line_content)
63
+ self._connect_signals()
64
+
65
+ def _center_on_screen(self):
66
+ screen = QApplication.primaryScreen()
67
+ if screen:
68
+ screen_geom = screen.availableGeometry()
69
+ x = (screen_geom.width() - self.width()) // 2
70
+ y = (screen_geom.height() - self.height()) // 2
71
+ self.move(x, y)
72
+
73
+ def _build_ui(self, error_type: str, error_message: str, file_path: str, line_number: int, line_content: str):
74
+ central_widget = QWidget(self)
75
+ self.setCentralWidget(central_widget)
76
+ main_layout = QVBoxLayout(central_widget)
77
+ main_layout.setContentsMargins(18, 16, 18, 16)
78
+ main_layout.setSpacing(12)
79
+
80
+ # Header Bar
81
+ top_bar = QHBoxLayout()
82
+ header_label = QLabel(self.translations["header_title"], self)
83
+ header_label.setObjectName("headerTitle")
84
+
85
+ self.status_badge = QLabel(self.translations["status_active"], self)
86
+ self.status_badge.setObjectName("statusBadge")
87
+ self.status_badge.setStyleSheet("color: #3fb950; background-color: #162a1c;")
88
+
89
+ top_bar.addWidget(header_label)
90
+ top_bar.addStretch()
91
+ top_bar.addWidget(self.status_badge)
92
+ main_layout.addLayout(top_bar)
93
+
94
+ # Crash Summary Box
95
+ crash_frame = QFrame(self)
96
+ crash_frame.setObjectName("crashBanner")
97
+ crash_layout = QVBoxLayout(crash_frame)
98
+ crash_layout.setContentsMargins(12, 10, 12, 10)
99
+ crash_layout.setSpacing(4)
100
+
101
+ crash_title = QLabel(f"💥 <b>{error_type}</b>: {error_message}", crash_frame)
102
+ crash_title.setStyleSheet("color: #ff7b72; font-size: 15px;")
103
+ crash_title.setTextFormat(Qt.TextFormat.RichText)
104
+
105
+ loc_text = self.translations["crash_location"]
106
+ line_text = self.translations["crashing_line"]
107
+ crash_info = QLabel(
108
+ f"📍 {loc_text}: <i>{file_path}:{line_number}</i> &nbsp;&nbsp;|&nbsp;&nbsp; ⚡ {line_text}: <code style='color: #ffffff; background: #2b1d20; padding: 2px 6px;'>{line_content.strip()}</code>",
109
+ crash_frame
110
+ )
111
+ crash_info.setStyleSheet("color: #8b949e; font-size: 13px;")
112
+ crash_info.setTextFormat(Qt.TextFormat.RichText)
113
+
114
+ crash_layout.addWidget(crash_title)
115
+ crash_layout.addWidget(crash_info)
116
+ main_layout.addWidget(crash_frame)
117
+
118
+ # Chat Feed Area
119
+ self.chat_area = QTextEdit(self)
120
+ self.chat_area.setObjectName("chatArea")
121
+ self.chat_area.setReadOnly(True)
122
+ main_layout.addWidget(self.chat_area, stretch=1)
123
+
124
+ # Bottom Input Area
125
+ input_layout = QHBoxLayout()
126
+ input_layout.setSpacing(10)
127
+
128
+ self.input_field = QLineEdit(self)
129
+ self.input_field.setObjectName("inputField")
130
+ self.input_field.setPlaceholderText(self.translations["placeholder"])
131
+ self.input_field.returnPressed.connect(self._on_send_clicked)
132
+ self.input_field.setEnabled(False)
133
+
134
+ self.send_button = QPushButton(self.translations["send_btn"], self)
135
+ self.send_button.setObjectName("sendButton")
136
+ self.send_button.clicked.connect(self._on_send_clicked)
137
+ self.send_button.setEnabled(False)
138
+
139
+ input_layout.addWidget(self.input_field, stretch=1)
140
+ input_layout.addWidget(self.send_button)
141
+ main_layout.addLayout(input_layout)
142
+
143
+ def _connect_signals(self):
144
+ bridge.mentor_message_received.connect(self._append_mentor_message)
145
+ bridge.user_input_requested.connect(self._enable_user_input)
146
+ bridge.solution_received.connect(self._append_solution)
147
+ bridge.evaluator_verdict_received.connect(self._append_verdict)
148
+
149
+ def _append_mentor_message(self, content: str, hint_level: int):
150
+ html_msg = ChatTemplates.mentor_bubble(
151
+ content=content,
152
+ hint_level=hint_level,
153
+ mentor_name=self.translations["mentor_tag"],
154
+ hint_label=self.translations["hint_level"],
155
+ is_arabic=self.is_arabic,
156
+ )
157
+ self.chat_area.append(html_msg)
158
+ self._scroll_to_bottom()
159
+
160
+ def _enable_user_input(self):
161
+ self.input_field.setEnabled(True)
162
+ self.send_button.setEnabled(True)
163
+ self.input_field.setFocus()
164
+
165
+ def _on_send_clicked(self):
166
+ text = self.input_field.text().strip()
167
+ if not text:
168
+ return
169
+
170
+ self.input_field.clear()
171
+ self.input_field.setEnabled(False)
172
+ self.send_button.setEnabled(False)
173
+
174
+ user_html = ChatTemplates.user_bubble(
175
+ text=text,
176
+ user_tag=self.translations["user_tag"],
177
+ is_arabic=self.is_arabic,
178
+ )
179
+ self.chat_area.append(user_html)
180
+ self._scroll_to_bottom()
181
+
182
+ submit_gui_user_input(text)
183
+
184
+ def _append_verdict(self, verdict: str):
185
+ badge_html = ChatTemplates.verdict_badge(verdict)
186
+ self.chat_area.append(badge_html)
187
+ self._scroll_to_bottom()
188
+
189
+ def _append_solution(self, content: str):
190
+ self.status_badge.setText(self.translations["status_solved"])
191
+ self.status_badge.setStyleSheet("color: #ffffff; background-color: #238636;")
192
+
193
+ sol_html = ChatTemplates.solution_bubble(
194
+ content=content,
195
+ wrapup_title=self.translations["wrapup_title"],
196
+ )
197
+ self.chat_area.append(sol_html)
198
+ self._scroll_to_bottom()
199
+
200
+ def _scroll_to_bottom(self):
201
+ cursor = self.chat_area.textCursor()
202
+ cursor.movePosition(QTextCursor.MoveOperation.End)
203
+ self.chat_area.setTextCursor(cursor)
ui/bidi.py ADDED
@@ -0,0 +1,23 @@
1
+ class BidiHelper:
2
+ """Helper to detect text direction for Socratic views."""
3
+
4
+ @staticmethod
5
+ def is_rtl_text(text: str) -> bool:
6
+ """Determines whether text is predominantly Arabic / RTL."""
7
+ if not text:
8
+ return False
9
+ for char in text:
10
+ code = ord(char)
11
+ # Arabic unicode ranges
12
+ if 0x0600 <= code <= 0x06FF or 0x0750 <= code <= 0x077F or 0x08A0 <= code <= 0x08FF:
13
+ return True
14
+ return False
15
+
16
+ @staticmethod
17
+ def format_text(text: str) -> str:
18
+ """
19
+ Modern Linux terminal emulators (GNOME Terminal, VTE, Kitty) have native
20
+ HarfBuzz shaping and BiDi engines. Passing raw Unicode preserves perfect
21
+ ligatures and prevents double-reversing artifacts.
22
+ """
23
+ return text
ui/console.py ADDED
@@ -0,0 +1,23 @@
1
+ import shutil
2
+ from rich.console import Console
3
+ from rich.theme import Theme
4
+
5
+ # Modern Dev Theme with sleek accents
6
+ custom_theme = Theme({
7
+ "mentor": "bold cyan",
8
+ "mentor.box": "cyan",
9
+ "crash": "bold red",
10
+ "crash.box": "red",
11
+ "code": "bright_white on #1e1e2e",
12
+ "user": "bold yellow",
13
+ "success": "bold green",
14
+ "success.box": "green",
15
+ "subtle": "dim white",
16
+ "badge": "bold black on bright_cyan",
17
+ })
18
+
19
+ # Global rich console instance for socratic (adapts dynamically to window dimensions)
20
+ console = Console(
21
+ theme=custom_theme,
22
+ highlight=False,
23
+ )