more organization

This commit is contained in:
ed
2026-06-06 10:24:22 -04:00
parent 1c627bcc30
commit 7d555361f9
20 changed files with 630 additions and 725 deletions
+3 -1
View File
@@ -1,10 +1,12 @@
# src/bg_shader.py # src/bg_shader.py
import time import time
import math import math
from typing import Optional
import numpy as np import numpy as np
from typing import Optional
from imgui_bundle import imgui, nanovg as nvg, hello_imgui from imgui_bundle import imgui, nanovg as nvg, hello_imgui
class BackgroundShader: class BackgroundShader:
def __init__(self): def __init__(self):
""" """
+12 -21
View File
@@ -1,9 +1,12 @@
from __future__ import annotations from __future__ import annotations
from imgui_bundle import imgui
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Callable, List, Dict, Any from typing import Optional, Callable, List, Dict, Any
@dataclass @dataclass
class Command: class Command:
id: str id: str
@@ -14,7 +17,6 @@ class Command:
enabled_when: Optional[str] = None enabled_when: Optional[str] = None
action: Optional[Callable] = None action: Optional[Callable] = None
@dataclass @dataclass
class ScoredCommand: class ScoredCommand:
command: Command command: Command
@@ -70,12 +72,9 @@ def _is_subsequence(query: str, target: str) -> bool:
def _compute_score(query: str, target: str) -> float: def _compute_score(query: str, target: str) -> float:
score = 0.0 score = 0.0
if target.startswith(query): if target.startswith(query): score += 1.0
score += 1.0 elif _starts_at_word_boundary(query, target): score += 0.5
elif _starts_at_word_boundary(query, target): if _is_contiguous(query, target): score += 0.3
score += 0.5
if _is_contiguous(query, target):
score += 0.3
gaps = _count_gaps(query, target) gaps = _count_gaps(query, target)
score -= 0.1 * gaps score -= 0.1 * gaps
return score return score
@@ -97,8 +96,7 @@ def _count_gaps(query: str, target: str) -> int:
last_match = -1 last_match = -1
for ti, ch in enumerate(target): for ti, ch in enumerate(target):
if qi < len(query) and ch == query[qi]: if qi < len(query) and ch == query[qi]:
if last_match >= 0 and ti - last_match > 1: if last_match >= 0 and ti - last_match > 1: gaps += ti - last_match - 1
gaps += ti - last_match - 1
last_match = ti last_match = ti
qi += 1 qi += 1
return gaps return gaps
@@ -128,19 +126,14 @@ def render_palette_modal(app: Any, commands: List[Command]) -> None:
if not getattr(app, "show_command_palette", False): if not getattr(app, "show_command_palette", False):
return return
from imgui_bundle import imgui
viewport = imgui.get_main_viewport() viewport = imgui.get_main_viewport()
center = viewport.get_center() center = viewport.get_center()
imgui.set_next_window_pos((center.x - 300, center.y - 200), imgui.Cond_.always) imgui.set_next_window_pos((center.x - 300, center.y - 200), imgui.Cond_.always)
imgui.set_next_window_size((600, 400), imgui.Cond_.always) imgui.set_next_window_size((600, 400), imgui.Cond_.always)
if not hasattr(app, "_command_palette_query"): if not hasattr(app, "_command_palette_query"): app._command_palette_query = ""
app._command_palette_query = "" if not hasattr(app, "_command_palette_selected"): app._command_palette_selected = 0
if not hasattr(app, "_command_palette_selected"): if not hasattr(app, "_command_palette_focused"): app._command_palette_focused = False
app._command_palette_selected = 0
if not hasattr(app, "_command_palette_focused"):
app._command_palette_focused = False
# Set focus on the window + input field ONCE per open. # Set focus on the window + input field ONCE per open.
if not app._command_palette_focused: if not app._command_palette_focused:
@@ -167,10 +160,8 @@ def render_palette_modal(app: Any, commands: List[Command]) -> None:
# Process Up/Down/Enter BEFORE input_text so we see the keys before the # Process Up/Down/Enter BEFORE input_text so we see the keys before the
# input field consumes them for cursor movement / text editing. # input field consumes them for cursor movement / text editing.
results = fuzzy_match(app._command_palette_query, commands, top_n=20) results = fuzzy_match(app._command_palette_query, commands, top_n=20)
if results: if results: app._command_palette_selected = max(0, min(app._command_palette_selected, len(results) - 1))
app._command_palette_selected = max(0, min(app._command_palette_selected, len(results) - 1)) else: app._command_palette_selected = 0
else:
app._command_palette_selected = 0
if imgui.is_key_pressed(imgui.Key.down_arrow): if imgui.is_key_pressed(imgui.Key.down_arrow):
if results: if results:
+13 -21
View File
@@ -1,13 +1,19 @@
from __future__ import annotations from __future__ import annotations
import webbrowser
from pathlib import Path
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable
from src import models
from src import theme_2
from src.command_palette import CommandRegistry from src.command_palette import CommandRegistry
from src.hot_reloader import HotReloader
if TYPE_CHECKING: if TYPE_CHECKING:
from src.gui_2 import App from src.gui_2 import App
registry = CommandRegistry() registry = CommandRegistry()
@@ -38,14 +44,10 @@ def reset_session(app: "App") -> None:
"""Reset Session — Reset the AI session, clear comms and tool logs.""" """Reset Session — Reset the AI session, clear comms and tool logs."""
from src import ai_client from src import ai_client
ai_client.reset_session() ai_client.reset_session()
if hasattr(app, "_handle_reset_session"): if hasattr(app, "_handle_reset_session"): app._handle_reset_session()
app._handle_reset_session() if hasattr(app, "_comms_log"): app._comms_log.clear()
if hasattr(app, "_comms_log"): if hasattr(app, "_tool_log"): app._tool_log.clear()
app._comms_log.clear() if hasattr(app, "ai_response"): app.ai_response = ""
if hasattr(app, "_tool_log"):
app._tool_log.clear()
if hasattr(app, "ai_response"):
app.ai_response = ""
@registry.register @registry.register
@@ -98,11 +100,8 @@ def save_project(app: "App") -> None:
@registry.register @registry.register
def save_all(app: "App") -> None: def save_all(app: "App") -> None:
"""Save All — Flush to project, flush to config, save global config.""" """Save All — Flush to project, flush to config, save global config."""
from src import models if hasattr(app, "_flush_to_project"): app._flush_to_project()
if hasattr(app, "_flush_to_project"): if hasattr(app, "_flush_to_config"): app._flush_to_config()
app._flush_to_project()
if hasattr(app, "_flush_to_config"):
app._flush_to_config()
if hasattr(app, "config"): if hasattr(app, "config"):
try: try:
models.save_config(app.config) models.save_config(app.config)
@@ -229,7 +228,6 @@ def show_workspace_manager(app: "App") -> None:
@registry.register @registry.register
def trigger_hot_reload(app: "App") -> None: def trigger_hot_reload(app: "App") -> None:
"""Hot Reload — Reload the GUI module to pick up code changes.""" """Hot Reload — Reload the GUI module to pick up code changes."""
from src.hot_reloader import HotReloader
HotReloader.reload("src.gui_2", app) HotReloader.reload("src.gui_2", app)
@@ -254,28 +252,24 @@ def redo(app: "App") -> None:
@registry.register @registry.register
def switch_to_dark_theme(app: "App") -> None: def switch_to_dark_theme(app: "App") -> None:
"""Switch to Dark Theme (10x Dark palette).""" """Switch to Dark Theme (10x Dark palette)."""
from src import theme_2
theme_2.apply("10x Dark") theme_2.apply("10x Dark")
@registry.register @registry.register
def switch_to_light_theme(app: "App") -> None: def switch_to_light_theme(app: "App") -> None:
"""Switch to Light Theme (ImGui Light palette).""" """Switch to Light Theme (ImGui Light palette)."""
from src import theme_2
theme_2.apply("ImGui Light") theme_2.apply("ImGui Light")
@registry.register @registry.register
def switch_to_nerv_theme(app: "App") -> None: def switch_to_nerv_theme(app: "App") -> None:
"""Switch to NERV Theme (Tactical Console aesthetic).""" """Switch to NERV Theme (Tactical Console aesthetic)."""
from src import theme_2
theme_2.apply("NERV") theme_2.apply("NERV")
@registry.register @registry.register
def cycle_theme(app: "App") -> None: def cycle_theme(app: "App") -> None:
"""Cycle Theme — Switch to the next theme in the cycle (Dark → Light → NERV → Dark).""" """Cycle Theme — Switch to the next theme in the cycle (Dark → Light → NERV → Dark)."""
from src import theme_2
order = ["10x Dark", "ImGui Light", "NERV"] order = ["10x Dark", "ImGui Light", "NERV"]
current = theme_2.get_current_palette() current = theme_2.get_current_palette()
if current in order: if current in order:
@@ -292,14 +286,12 @@ def cycle_theme(app: "App") -> None:
@registry.register @registry.register
def show_documentation(app: "App") -> None: def show_documentation(app: "App") -> None:
"""Show Documentation — Open the project URL in the browser.""" """Show Documentation — Open the project URL in the browser."""
import webbrowser
webbrowser.open("https://git.cozyair.dev/ed/manual_slop/") webbrowser.open("https://git.cozyair.dev/ed/manual_slop/")
@registry.register @registry.register
def show_command_palette_help(app: "App") -> None: def show_command_palette_help(app: "App") -> None:
"""Show Command Palette Help — Open the docs/Readme.md in the Text Viewer.""" """Show Command Palette Help — Open the docs/Readme.md in the Text Viewer."""
from pathlib import Path
if hasattr(app, "readme_text"): if hasattr(app, "readme_text"):
docs_readme = Path("docs/Readme.md") docs_readme = Path("docs/Readme.md")
if docs_readme.exists(): if docs_readme.exists():
-4
View File
@@ -44,8 +44,6 @@ from src import mma_prompts
def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict[str, Any]]: def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict[str, Any]]:
""" """
Tier 2 (Tech Lead) call. Tier 2 (Tech Lead) call.
Breaks down a Track Brief and module skeletons into discrete Tier 3 Tickets. Breaks down a Track Brief and module skeletons into discrete Tier 3 Tickets.
[C: tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_failure, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_success, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_success, tests/test_orchestration_logic.py:test_generate_tickets] [C: tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_failure, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_success, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_success, tests/test_orchestration_logic.py:test_generate_tickets]
@@ -101,8 +99,6 @@ from src.models import Ticket
def topological_sort(tickets: list[dict[str, Any]]) -> list[dict[str, Any]]: def topological_sort(tickets: list[dict[str, Any]]) -> list[dict[str, Any]]:
""" """
Sorts a list of tickets based on their 'depends_on' field. Sorts a list of tickets based on their 'depends_on' field.
Raises ValueError if a circular dependency or missing internal dependency is detected. Raises ValueError if a circular dependency or missing internal dependency is detected.
[C: tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_complex, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_cycle, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_empty, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_linear, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_missing_dependency, tests/test_conductor_tech_lead.py:test_topological_sort_vlog, tests/test_dag_engine.py:test_topological_sort, tests/test_dag_engine.py:test_topological_sort_cycle, tests/test_orchestration_logic.py:test_topological_sort, tests/test_orchestration_logic.py:test_topological_sort_circular, tests/test_perf_dag.py:test_dag_edge_cases, tests/test_perf_dag.py:test_dag_performance] [C: tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_complex, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_cycle, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_empty, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_linear, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_missing_dependency, tests/test_conductor_tech_lead.py:test_topological_sort_vlog, tests/test_dag_engine.py:test_topological_sort, tests/test_dag_engine.py:test_topological_sort_cycle, tests/test_orchestration_logic.py:test_topological_sort, tests/test_orchestration_logic.py:test_topological_sort_circular, tests/test_perf_dag.py:test_dag_edge_cases, tests/test_perf_dag.py:test_dag_performance]
+3 -4
View File
@@ -116,10 +116,8 @@ class TrackDAG:
if is_backtracking: if is_backtracking:
path.remove(node_id) path.remove(node_id)
continue continue
if node_id in path: if node_id in path: return True
return True if node_id in visited: continue
if node_id in visited:
continue
visited.add(node_id) visited.add(node_id)
path.add(node_id) path.add(node_id)
stack.append((node_id, True)) stack.append((node_id, True))
@@ -216,3 +214,4 @@ class ExecutionEngine:
ticket = self.dag.ticket_map.get(task_id) ticket = self.dag.ticket_map.get(task_id)
if ticket: if ticket:
ticket.status = status ticket.status = status
+2 -6
View File
@@ -27,18 +27,14 @@ class ExternalEditorLauncher:
return self.config.editors.get(editor_name) return self.config.editors.get(editor_name)
return self.config.get_default() return self.config.get_default()
def build_diff_command( def build_diff_command(self, editor: TextEditorConfig, original_path: str, modified_path: str) -> List[str]:
self, editor: TextEditorConfig, original_path: str, modified_path: str
) -> List[str]:
""" """
[C: tests/test_external_editor.py:TestExternalEditorLauncher.test_build_diff_command, tests/test_external_editor_gui.py:test_verify_command_format, tests/test_external_editor_gui.py:test_verify_vscode_command_format] [C: tests/test_external_editor.py:TestExternalEditorLauncher.test_build_diff_command, tests/test_external_editor_gui.py:test_verify_command_format, tests/test_external_editor_gui.py:test_verify_vscode_command_format]
""" """
cmd = [editor.path] + editor.diff_args + [original_path, modified_path] cmd = [editor.path] + editor.diff_args + [original_path, modified_path]
return cmd return cmd
def launch_diff( def launch_diff(self, editor_name: Optional[str], original_path: str, modified_path: str) -> Optional[subprocess.Popen]:
self, editor_name: Optional[str], original_path: str, modified_path: str
) -> Optional[subprocess.Popen]:
""" """
[C: src/gui_2.py:App._open_patch_in_external_editor, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_file_not_found, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_missing_editor, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_success] [C: src/gui_2.py:App._open_patch_in_external_editor, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_file_not_found, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_missing_editor, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_success]
""" """
+17 -24
View File
@@ -49,12 +49,12 @@ _ast_cache: Dict[str, Tuple[float, tree_sitter.Tree]] = {}
class ASTParser: class ASTParser:
""" """
Parser for extracting AST-based views of source code. Parser for extracting AST-based views of source code.
Currently supports Python. Currently supports Python.
""" """
#region: Core Operations #region: Core Operations
def __init__(self, language: str) -> None: def __init__(self, language: str) -> None:
""" """
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__] [C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
@@ -63,17 +63,13 @@ class ASTParser:
raise ValueError(f"Language '{language}' not supported yet.") raise ValueError(f"Language '{language}' not supported yet.")
self.language_name = language self.language_name = language
# Load the tree-sitter language grammar # Load the tree-sitter language grammar
if language == "python": if language == "python": self.language = tree_sitter.Language(tree_sitter_python.language())
self.language = tree_sitter.Language(tree_sitter_python.language()) elif language == "cpp": self.language = tree_sitter.Language(tree_sitter_cpp.language())
elif language == "cpp": elif language == "c": self.language = tree_sitter.Language(tree_sitter_c.language())
self.language = tree_sitter.Language(tree_sitter_cpp.language())
elif language == "c":
self.language = tree_sitter.Language(tree_sitter_c.language())
self.parser = tree_sitter.Parser(self.language) self.parser = tree_sitter.Parser(self.language)
def parse(self, code: str) -> tree_sitter.Tree: def parse(self, code: str) -> tree_sitter.Tree:
""" """
Parse the given code and return the tree-sitter Tree. Parse the given code and return the tree-sitter Tree.
[C: src/mcp_client.py:_search_file, src/mcp_client.py:derive_code_path, src/mcp_client.py:py_check_syntax, src/mcp_client.py:py_get_class_summary, src/mcp_client.py:py_get_definition, src/mcp_client.py:py_get_docstring, src/mcp_client.py:py_get_imports, src/mcp_client.py:py_get_signature, src/mcp_client.py:py_get_symbol_info, src/mcp_client.py:py_get_var_declaration, src/mcp_client.py:py_set_signature, src/mcp_client.py:py_set_var_declaration, src/mcp_client.py:py_update_definition, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/rag_engine.py:RAGEngine._chunk_code, src/summarize.py:_summarise_python, tests/test_ast_parser.py:test_ast_parser_parse, tests/test_tree_sitter_setup.py:test_tree_sitter_python_setup] [C: src/mcp_client.py:_search_file, src/mcp_client.py:derive_code_path, src/mcp_client.py:py_check_syntax, src/mcp_client.py:py_get_class_summary, src/mcp_client.py:py_get_definition, src/mcp_client.py:py_get_docstring, src/mcp_client.py:py_get_imports, src/mcp_client.py:py_get_signature, src/mcp_client.py:py_get_symbol_info, src/mcp_client.py:py_get_var_declaration, src/mcp_client.py:py_set_signature, src/mcp_client.py:py_set_var_declaration, src/mcp_client.py:py_update_definition, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/rag_engine.py:RAGEngine._chunk_code, src/summarize.py:_summarise_python, tests/test_ast_parser.py:test_ast_parser_parse, tests/test_tree_sitter_setup.py:test_tree_sitter_python_setup]
""" """
@@ -185,12 +181,13 @@ class ASTParser:
if child.type in ("type_identifier", "identifier", "namespace_identifier", "qualified_identifier"): if child.type in ("type_identifier", "identifier", "namespace_identifier", "qualified_identifier"):
return code_bytes[child.start_byte:child.end_byte].decode("utf8", errors="replace") return code_bytes[child.start_byte:child.end_byte].decode("utf8", errors="replace")
return "" return ""
#endregion: Core Operations #endregion: Core Operations
#region: Skeleton & Curated Views #region: Skeleton & Curated Views
def get_skeleton(self, code: str, path: Optional[str] = None) -> str: def get_skeleton(self, code: str, path: Optional[str] = None) -> str:
""" """
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies). Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
[C: src/mcp_client.py:py_get_skeleton, src/mcp_client.py:ts_c_get_skeleton, src/mcp_client.py:ts_cpp_get_skeleton, src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ast_parser.py:test_ast_parser_get_skeleton_c, tests/test_ast_parser.py:test_ast_parser_get_skeleton_cpp, tests/test_ast_parser.py:test_ast_parser_get_skeleton_python, tests/test_context_pruner.py:test_ast_caching, tests/test_context_pruner.py:test_performance_large_file] [C: src/mcp_client.py:py_get_skeleton, src/mcp_client.py:ts_c_get_skeleton, src/mcp_client.py:ts_cpp_get_skeleton, src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ast_parser.py:test_ast_parser_get_skeleton_c, tests/test_ast_parser.py:test_ast_parser_get_skeleton_cpp, tests/test_ast_parser.py:test_ast_parser_get_skeleton_python, tests/test_context_pruner.py:test_ast_caching, tests/test_context_pruner.py:test_performance_large_file]
""" """
@@ -275,8 +272,6 @@ class ASTParser:
return code_bytearray.decode("utf8") return code_bytearray.decode("utf8")
def get_curated_view(self, code: str, path: Optional[str] = None) -> str: def get_curated_view(self, code: str, path: Optional[str] = None) -> str:
""" """
Returns a curated skeleton of a Python file. Returns a curated skeleton of a Python file.
Preserves function bodies if they have @core_logic decorator or # [HOT] comment. Preserves function bodies if they have @core_logic decorator or # [HOT] comment.
Otherwise strips bodies but preserves docstrings. Otherwise strips bodies but preserves docstrings.
@@ -350,13 +345,13 @@ class ASTParser:
for start, end, replacement in edits: for start, end, replacement in edits:
code_bytearray[start:end] = bytes(replacement, "utf8") code_bytearray[start:end] = bytes(replacement, "utf8")
return code_bytearray.decode("utf8") return code_bytearray.decode("utf8")
#endregion: Skeleton & Curated Views #endregion: Skeleton & Curated Views
#region: Targeted Views #region: Targeted Views
def get_targeted_view(self, code: str, function_names: List[str], path: Optional[str] = None) -> str: def get_targeted_view(self, code: str, function_names: List[str], path: Optional[str] = None) -> str:
""" """
Returns a targeted view of the code including only the specified functions Returns a targeted view of the code including only the specified functions
and their dependencies up to depth 2. and their dependencies up to depth 2.
[C: src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ast_parser.py:test_ast_parser_get_targeted_view, tests/test_context_pruner.py:test_class_targeted_extraction, tests/test_context_pruner.py:test_targeted_extraction] [C: src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ast_parser.py:test_ast_parser_get_targeted_view, tests/test_context_pruner.py:test_class_targeted_extraction, tests/test_context_pruner.py:test_targeted_extraction]
@@ -517,12 +512,13 @@ class ASTParser:
result = code_bytearray.decode("utf8") result = code_bytearray.decode("utf8")
result = re.sub(r'\n\s*\n\s*\n+', '\n\n', result) result = re.sub(r'\n\s*\n\s*\n+', '\n\n', result)
return result.strip() + "\n" return result.strip() + "\n"
#endregion: Targeted Views #endregion: Targeted Views
#region: Symbol Extraction #region: Symbol Extraction
def get_definition(self, code: str, name: str, path: Optional[str] = None) -> str: def get_definition(self, code: str, name: str, path: Optional[str] = None) -> str:
""" """
Returns the full source code for a specific definition by name. Returns the full source code for a specific definition by name.
Supports 'ClassName::method' or 'method' for C++. Supports 'ClassName::method' or 'method' for C++.
[C: src/mcp_client.py:trace, src/mcp_client.py:ts_c_get_definition, src/mcp_client.py:ts_cpp_get_definition, tests/test_ast_parser.py:test_ast_parser_get_definition_c, tests/test_ast_parser.py:test_ast_parser_get_definition_cpp, tests/test_ast_parser.py:test_ast_parser_get_definition_cpp_template] [C: src/mcp_client.py:trace, src/mcp_client.py:ts_c_get_definition, src/mcp_client.py:ts_cpp_get_definition, tests/test_ast_parser.py:test_ast_parser_get_definition_c, tests/test_ast_parser.py:test_ast_parser_get_definition_cpp, tests/test_ast_parser.py:test_ast_parser_get_definition_cpp_template]
@@ -621,15 +617,12 @@ class ASTParser:
def get_signature(self, code: str, name: str, path: Optional[str] = None) -> str: def get_signature(self, code: str, name: str, path: Optional[str] = None) -> str:
""" """
Returns only the signature part of a function or method. Returns only the signature part of a function or method.
For C/C++, this is the code from the start of the definition until the block start '{'. For C/C++, this is the code from the start of the definition until the block start '{'.
[C: src/mcp_client.py:ts_c_get_signature, src/mcp_client.py:ts_cpp_get_signature, tests/test_ast_parser.py:test_ast_parser_get_signature_c, tests/test_ast_parser.py:test_ast_parser_get_signature_cpp] [C: src/mcp_client.py:ts_c_get_signature, src/mcp_client.py:ts_cpp_get_signature, tests/test_ast_parser.py:test_ast_parser_get_signature_c, tests/test_ast_parser.py:test_ast_parser_get_signature_cpp]
""" """
code_bytes = code.encode("utf8") code_bytes = code.encode("utf8")
tree = self.get_cached_tree(path, code) tree = self.get_cached_tree(path, code)
parts = re.split(r'::|\.', name) parts = re.split(r'::|\.', name)
def walk(node: tree_sitter.Node, target_parts: List[str]) -> Optional[tree_sitter.Node]: def walk(node: tree_sitter.Node, target_parts: List[str]) -> Optional[tree_sitter.Node]:
@@ -729,13 +722,13 @@ class ASTParser:
return code_bytes[found_node.start_byte:found_node.end_byte].decode("utf8", errors="replace").strip() return code_bytes[found_node.start_byte:found_node.end_byte].decode("utf8", errors="replace").strip()
return f"ERROR: signature for '{name}' not found" return f"ERROR: signature for '{name}' not found"
#endregion: Symbol Extraction #endregion: Symbol Extraction
#region: Analysis & Updates #region: Analysis & Updates
def get_code_outline(self, code: str, path: Optional[str] = None) -> str: def get_code_outline(self, code: str, path: Optional[str] = None) -> str:
""" """
Returns a hierarchical outline of the code (classes, structs, functions, methods). Returns a hierarchical outline of the code (classes, structs, functions, methods).
[C: src/mcp_client.py:ts_c_get_code_outline, src/mcp_client.py:ts_cpp_get_code_outline, tests/test_ast_parser.py:test_ast_parser_get_code_outline_c, tests/test_ast_parser.py:test_ast_parser_get_code_outline_cpp] [C: src/mcp_client.py:ts_c_get_code_outline, src/mcp_client.py:ts_cpp_get_code_outline, tests/test_ast_parser.py:test_ast_parser_get_code_outline_c, tests/test_ast_parser.py:test_ast_parser_get_code_outline_cpp]
""" """
@@ -778,14 +771,11 @@ class ASTParser:
def update_definition(self, code: str, name: str, new_content: str, path: Optional[str] = None) -> str: def update_definition(self, code: str, name: str, new_content: str, path: Optional[str] = None) -> str:
""" """
Surgically replace the definition of a class or function by name. Surgically replace the definition of a class or function by name.
[C: src/mcp_client.py:ts_c_update_definition, src/mcp_client.py:ts_cpp_update_definition, tests/test_ast_parser.py:test_ast_parser_update_definition_cpp] [C: src/mcp_client.py:ts_c_update_definition, src/mcp_client.py:ts_cpp_update_definition, tests/test_ast_parser.py:test_ast_parser_update_definition_cpp]
""" """
code_bytes = code.encode("utf8") code_bytes = code.encode("utf8")
tree = self.get_cached_tree(path, code) tree = self.get_cached_tree(path, code)
parts = re.split(r'::|\.', name) parts = re.split(r'::|\.', name)
def walk(node: tree_sitter.Node, target_parts: List[str]) -> Optional[tree_sitter.Node]: def walk(node: tree_sitter.Node, target_parts: List[str]) -> Optional[tree_sitter.Node]:
@@ -876,12 +866,15 @@ class ASTParser:
code_bytearray[found_node.start_byte:found_node.end_byte] = bytes(new_content, "utf8") code_bytearray[found_node.start_byte:found_node.end_byte] = bytes(new_content, "utf8")
return code_bytearray.decode("utf8") return code_bytearray.decode("utf8")
return f"ERROR: definition '{name}' not found" return f"ERROR: definition '{name}' not found"
#endregion: Analysis & Updates #endregion: Analysis & Updates
#region: Module Level Utilities #region: Module Level Utilities
def reset_client() -> None: def reset_client() -> None:
pass pass
def get_file_id(path: Path) -> Optional[str]: def get_file_id(path: Path) -> Optional[str]:
return None return None
#endregion: Module Level Utilities #endregion: Module Level Utilities
+1 -7
View File
@@ -46,13 +46,10 @@ from src import session_logger
class GeminiCliAdapter: class GeminiCliAdapter:
""" """
Adapter for the Gemini CLI that parses streaming JSON output. Adapter for the Gemini CLI that parses streaming JSON output.
""" """
def __init__(self, binary_path: str = "gemini"): def __init__(self, binary_path: str = "gemini"):
""" """
Initializes the adapter with the path to the gemini CLI executable. Initializes the adapter with the path to the gemini CLI executable.
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__] [C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
""" """
@@ -61,11 +58,8 @@ class GeminiCliAdapter:
self.last_usage: Optional[dict[str, Any]] = None self.last_usage: Optional[dict[str, Any]] = None
self.last_latency: float = 0.0 self.last_latency: float = 0.0
def send(self, message: str, safety_settings: list[Any] | None = None, system_instruction: str | None = None, def send(self, message: str, safety_settings: list[Any] | None = None, system_instruction: str | None = None, model: str | None = None, stream_callback: Optional[Callable[[str], None]] = None) -> dict[str, Any]:
model: str | None = None, stream_callback: Optional[Callable[[str], None]] = None) -> dict[str, Any]:
""" """
Sends a message to the Gemini CLI and processes the streaming JSON output. Sends a message to the Gemini CLI and processes the streaming JSON output.
Uses non-blocking line-by-line reading to allow stream_callback. Uses non-blocking line-by-line reading to allow stream_callback.
[C: simulation/user_agent.py:UserSimAgent.generate_response, src/multi_agent_conductor.py:run_worker_lifecycle, src/orchestrator_pm.py:generate_tracks, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_full_flow_integration, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_captures_usage_metadata, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_handles_tool_use_events, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_parses_jsonl_output, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_starts_subprocess_with_correct_args, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_parses_tool_calls_from_streaming_json, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_starts_subprocess_with_model, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_context_bleed_prevention, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_token_usage.py:test_token_usage_tracking, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast] [C: simulation/user_agent.py:UserSimAgent.generate_response, src/multi_agent_conductor.py:run_worker_lifecycle, src/orchestrator_pm.py:generate_tracks, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_full_flow_integration, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_captures_usage_metadata, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_handles_tool_use_events, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_parses_jsonl_output, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_starts_subprocess_with_correct_args, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_parses_tool_calls_from_streaming_json, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_starts_subprocess_with_model, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_context_bleed_prevention, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_token_usage.py:test_token_usage_tracking, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
+5 -25
View File
@@ -79,8 +79,6 @@ class HistoryManager:
def push(self, state: typing.Any, description: str) -> None: def push(self, state: typing.Any, description: str) -> None:
""" """
Pushes a new state to the undo stack and clears the redo stack. Pushes a new state to the undo stack and clears the redo stack.
If the undo stack exceeds max_capacity, the oldest state is removed. If the undo stack exceeds max_capacity, the oldest state is removed.
[C: tests/test_history.py:test_jump_to_undo, tests/test_history.py:test_max_capacity, tests/test_history.py:test_push_state, tests/test_history.py:test_redo_cleared_on_push, tests/test_history.py:test_undo_redo, tests/test_history_manager.py:TestHistoryManager.test_get_history_returns_descriptions, tests/test_history_manager.py:TestHistoryManager.test_jump_to_undo, tests/test_history_manager.py:TestHistoryManager.test_push_and_undo, tests/test_history_manager.py:TestHistoryManager.test_push_clears_redo_stack, tests/test_history_manager.py:TestHistoryManager.test_undo_and_redo] [C: tests/test_history.py:test_jump_to_undo, tests/test_history.py:test_max_capacity, tests/test_history.py:test_push_state, tests/test_history.py:test_redo_cleared_on_push, tests/test_history.py:test_undo_redo, tests/test_history_manager.py:TestHistoryManager.test_get_history_returns_descriptions, tests/test_history_manager.py:TestHistoryManager.test_jump_to_undo, tests/test_history_manager.py:TestHistoryManager.test_push_and_undo, tests/test_history_manager.py:TestHistoryManager.test_push_clears_redo_stack, tests/test_history_manager.py:TestHistoryManager.test_undo_and_redo]
@@ -93,45 +91,33 @@ class HistoryManager:
def undo(self, current_state: typing.Any, current_description: str = "Current State") -> typing.Optional[HistoryEntry]: def undo(self, current_state: typing.Any, current_description: str = "Current State") -> typing.Optional[HistoryEntry]:
""" """
Undoes the last action by moving the current_state to the redo stack Undoes the last action by moving the current_state to the redo stack
and returning the top of the undo stack. and returning the top of the undo stack.
[C: tests/test_history.py:test_redo_cleared_on_push, tests/test_history.py:test_undo_redo, tests/test_history_manager.py:TestHistoryManager.test_push_and_undo, tests/test_history_manager.py:TestHistoryManager.test_push_clears_redo_stack, tests/test_history_manager.py:TestHistoryManager.test_undo_and_redo, tests/test_history_manager.py:TestHistoryManager.test_undo_no_history_returns_none] [C: tests/test_history.py:test_redo_cleared_on_push, tests/test_history.py:test_undo_redo, tests/test_history_manager.py:TestHistoryManager.test_push_and_undo, tests/test_history_manager.py:TestHistoryManager.test_push_clears_redo_stack, tests/test_history_manager.py:TestHistoryManager.test_undo_and_redo, tests/test_history_manager.py:TestHistoryManager.test_undo_no_history_returns_none]
""" """
if not self._undo_stack: if not self._undo_stack: return None
return None
redo_entry = HistoryEntry(state=current_state, description=current_description) redo_entry = HistoryEntry(state=current_state, description=current_description)
self._redo_stack.append(redo_entry) self._redo_stack.append(redo_entry)
return self._undo_stack.pop() return self._undo_stack.pop()
def redo(self, current_state: typing.Any, current_description: str = "Current State") -> typing.Optional[HistoryEntry]: def redo(self, current_state: typing.Any, current_description: str = "Current State") -> typing.Optional[HistoryEntry]:
""" """
Redoes the last undone action by moving the current_state to the undo stack Redoes the last undone action by moving the current_state to the undo stack
and returning the top of the redo stack. and returning the top of the redo stack.
[C: tests/test_history.py:test_undo_redo, tests/test_history_manager.py:TestHistoryManager.test_redo_no_history_returns_none, tests/test_history_manager.py:TestHistoryManager.test_undo_and_redo] [C: tests/test_history.py:test_undo_redo, tests/test_history_manager.py:TestHistoryManager.test_redo_no_history_returns_none, tests/test_history_manager.py:TestHistoryManager.test_undo_and_redo]
""" """
if not self._redo_stack: if not self._redo_stack: return None
return None
undo_entry = HistoryEntry(state=current_state, description=current_description) undo_entry = HistoryEntry(state=current_state, description=current_description)
self._undo_stack.append(undo_entry) self._undo_stack.append(undo_entry)
return self._redo_stack.pop() return self._redo_stack.pop()
@property @property
def can_undo(self) -> bool: def can_undo(self) -> bool: return len(self._undo_stack) > 0
return len(self._undo_stack) > 0
@property @property
def can_redo(self) -> bool: def can_redo(self) -> bool: return len(self._redo_stack) > 0
return len(self._redo_stack) > 0
def get_history(self) -> typing.List[typing.Dict[str, typing.Any]]: def get_history(self) -> typing.List[typing.Dict[str, typing.Any]]:
""" """
Returns a list of descriptions and timestamps for the undo stack. Returns a list of descriptions and timestamps for the undo stack.
[C: tests/test_history.py:test_initial_state, tests/test_history.py:test_push_state, tests/test_history_manager.py:TestHistoryManager.test_get_history_returns_descriptions] [C: tests/test_history.py:test_initial_state, tests/test_history.py:test_push_state, tests/test_history_manager.py:TestHistoryManager.test_get_history_returns_descriptions]
""" """
@@ -142,20 +128,14 @@ class HistoryManager:
def jump_to_undo(self, index: int, current_state: typing.Any, current_description: str = "Before Jump") -> typing.Optional[HistoryEntry]: def jump_to_undo(self, index: int, current_state: typing.Any, current_description: str = "Before Jump") -> typing.Optional[HistoryEntry]:
""" """
Jumps to a specific state in the undo stack by moving subsequent states Jumps to a specific state in the undo stack by moving subsequent states
and the current_state to the redo stack. and the current_state to the redo stack.
[C: tests/test_history.py:test_jump_to_undo, tests/test_history_manager.py:TestHistoryManager.test_jump_to_undo] [C: tests/test_history.py:test_jump_to_undo, tests/test_history_manager.py:TestHistoryManager.test_jump_to_undo]
""" """
if index < 0 or index >= len(self._undo_stack): if index < 0 or index >= len(self._undo_stack): return None
return None
# Move current state to redo # Move current state to redo
self._redo_stack.append(HistoryEntry(state=current_state, description=current_description)) self._redo_stack.append(HistoryEntry(state=current_state, description=current_description))
# Move states between index and top of undo to redo # Move states between index and top of undo to redo
while len(self._undo_stack) > index + 1: while len(self._undo_stack) > index + 1:
self._redo_stack.append(self._undo_stack.pop()) self._redo_stack.append(self._undo_stack.pop())
return self._undo_stack.pop() return self._undo_stack.pop()
+3 -4
View File
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import importlib
import sys
import traceback import traceback
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -45,8 +47,6 @@ class HotReloader:
state = cls.capture_state(app, hm.state_keys) state = cls.capture_state(app, hm.state_keys)
try: try:
import importlib
import sys
if module_name in sys.modules: if module_name in sys.modules:
old_module = sys.modules[module_name] old_module = sys.modules[module_name]
importlib.reload(old_module) importlib.reload(old_module)
@@ -65,6 +65,5 @@ class HotReloader:
def reload_all(cls, app: Any) -> bool: def reload_all(cls, app: Any) -> bool:
success = True success = True
for name in cls.HOT_MODULES: for name in cls.HOT_MODULES:
if not cls.reload(name, app): if not cls.reload(name, app): success = False
success = False
return success return success
-6
View File
@@ -10,8 +10,6 @@ from src.log_registry import LogRegistry
class LogPruner: class LogPruner:
""" """
Handles the automated deletion of old and insignificant session logs. Handles the automated deletion of old and insignificant session logs.
Ensures that only whitelisted or significant sessions (based on size/content) Ensures that only whitelisted or significant sessions (based on size/content)
are preserved long-term. are preserved long-term.
@@ -19,8 +17,6 @@ class LogPruner:
def __init__(self, log_registry: LogRegistry, logs_dir: str) -> None: def __init__(self, log_registry: LogRegistry, logs_dir: str) -> None:
""" """
Initializes the LogPruner. Initializes the LogPruner.
Args: Args:
@@ -33,8 +29,6 @@ class LogPruner:
def prune(self, max_age_days: int = 1, min_size_kb: int = 2) -> None: def prune(self, max_age_days: int = 1, min_size_kb: int = 2) -> None:
""" """
Prunes old and small session directories from the logs directory. Prunes old and small session directories from the logs directory.
Deletes session directories that meet the following criteria: Deletes session directories that meet the following criteria:
-18
View File
@@ -49,16 +49,12 @@ from typing import Any
class LogRegistry: class LogRegistry:
""" """
Manages a persistent registry of session logs using a TOML file. Manages a persistent registry of session logs using a TOML file.
Tracks session paths, start times, whitelisting status, and metadata. Tracks session paths, start times, whitelisting status, and metadata.
""" """
def __init__(self, registry_path: str) -> None: def __init__(self, registry_path: str) -> None:
""" """
Initializes the LogRegistry with a path to the registry file. Initializes the LogRegistry with a path to the registry file.
Args: Args:
@@ -76,8 +72,6 @@ class LogRegistry:
def load_registry(self) -> None: def load_registry(self) -> None:
""" """
Loads the registry data from the TOML file into memory. Loads the registry data from the TOML file into memory.
Handles date/time conversions from TOML-native formats to strings for consistency. Handles date/time conversions from TOML-native formats to strings for consistency.
""" """
@@ -106,8 +100,6 @@ class LogRegistry:
def save_registry(self) -> None: def save_registry(self) -> None:
""" """
Serializes and saves the current registry data to the TOML file. Serializes and saves the current registry data to the TOML file.
Converts internal datetime objects to ISO format strings for compatibility. Converts internal datetime objects to ISO format strings for compatibility.
[C: tests/test_logging_e2e.py:test_logging_e2e] [C: tests/test_logging_e2e.py:test_logging_e2e]
@@ -142,8 +134,6 @@ class LogRegistry:
def register_session(self, session_id: str, path: str, start_time: datetime | str) -> None: def register_session(self, session_id: str, path: str, start_time: datetime | str) -> None:
""" """
Registers a new session in the registry. Registers a new session in the registry.
Args: Args:
@@ -169,8 +159,6 @@ class LogRegistry:
def update_session_metadata(self, session_id: str, message_count: int, errors: int, size_kb: int, whitelisted: bool, reason: str) -> None: def update_session_metadata(self, session_id: str, message_count: int, errors: int, size_kb: int, whitelisted: bool, reason: str) -> None:
""" """
Updates metadata fields for an existing session. Updates metadata fields for an existing session.
Args: Args:
@@ -204,8 +192,6 @@ class LogRegistry:
def is_session_whitelisted(self, session_id: str) -> bool: def is_session_whitelisted(self, session_id: str) -> bool:
""" """
Checks if a specific session is marked as whitelisted. Checks if a specific session is marked as whitelisted.
Args: Args:
@@ -223,8 +209,6 @@ class LogRegistry:
def update_auto_whitelist_status(self, session_id: str) -> None: def update_auto_whitelist_status(self, session_id: str) -> None:
""" """
Analyzes session logs and updates whitelisting status based on heuristics. Analyzes session logs and updates whitelisting status based on heuristics.
Sessions are automatically whitelisted if they contain error keywords, Sessions are automatically whitelisted if they contain error keywords,
have a high message count, or exceed a size threshold. have a high message count, or exceed a size threshold.
@@ -284,8 +268,6 @@ class LogRegistry:
def get_old_non_whitelisted_sessions(self, cutoff_datetime: datetime) -> list[dict[str, Any]]: def get_old_non_whitelisted_sessions(self, cutoff_datetime: datetime) -> list[dict[str, Any]]:
""" """
Retrieves a list of sessions that are older than a specific cutoff time Retrieves a list of sessions that are older than a specific cutoff time
and are not marked as whitelisted. and are not marked as whitelisted.
Also includes non-whitelisted sessions that are empty (message_count=0 or size_kb=0). Also includes non-whitelisted sessions that are empty (message_count=0 or size_kb=0).
+12 -22
View File
@@ -10,6 +10,10 @@ from imgui_bundle import imgui_md, imgui, immapp, imgui_color_text_edit as ed
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Callable from typing import Optional, Dict, Callable
from src import theme_2
from src.markdown_table import parse_tables, render_table
def _get_language_id(name: str): def _get_language_id(name: str):
"""Get a language identifier for ImGuiColorTextEdit. """Get a language identifier for ImGuiColorTextEdit.
@@ -41,17 +45,13 @@ def _set_editor_language(editor, lang_obj) -> None:
1.92.801+: editor.set_language(obj). 1.92.5: editor.set_language_definition(obj). 1.92.801+: editor.set_language(obj). 1.92.5: editor.set_language_definition(obj).
No-op when lang_obj is None (used to skip the call for unknown languages). No-op when lang_obj is None (used to skip the call for unknown languages).
""" """
if lang_obj is None: if lang_obj is None: return
return
if hasattr(editor, "set_language"): if hasattr(editor, "set_language"): editor.set_language(lang_obj)
editor.set_language(lang_obj) elif hasattr(editor, "set_language_definition"): editor.set_language_definition(lang_obj)
elif hasattr(editor, "set_language_definition"):
editor.set_language_definition(lang_obj)
class MarkdownRenderer: class MarkdownRenderer:
""" """
Hybrid Markdown renderer that uses imgui_md for text/headers Hybrid Markdown renderer that uses imgui_md for text/headers
and ImGuiColorTextEdit for syntax-highlighted code blocks. and ImGuiColorTextEdit for syntax-highlighted code blocks.
""" """
@@ -80,7 +80,6 @@ class MarkdownRenderer:
# Apply the current theme's syntax palette on construction so new # Apply the current theme's syntax palette on construction so new
# editors we create pick up the right colors. The renderer is re-created # editors we create pick up the right colors. The renderer is re-created
# when the theme changes (see theme_2 module-load behavior). # when the theme changes (see theme_2 module-load behavior).
from src import theme_2
palette_id = theme_2.get_syntax_palette_for_theme(theme_2.get_current_palette()) palette_id = theme_2.get_syntax_palette_for_theme(theme_2.get_current_palette())
theme_2.apply_syntax_palette(palette_id) theme_2.apply_syntax_palette(palette_id)
@@ -119,20 +118,16 @@ class MarkdownRenderer:
def render(self, text: str, context_id: str = "default") -> None: def render(self, text: str, context_id: str = "default") -> None:
""" """
Render Markdown text with code block interception and GFM table substitution. Render Markdown text with code block interception and GFM table substitution.
[C: src/theme_2.py:render_post_fx, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render] [C: src/theme_2.py:render_post_fx, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render]
""" """
if not text: if not text: return
return
from src.markdown_table import parse_tables, render_table
text = self._normalize_bullet_delimiters(text) text = self._normalize_bullet_delimiters(text)
text = self._normalize_nested_list_endings(text) text = self._normalize_nested_list_endings(text)
text = self._normalize_list_continuations(text) text = self._normalize_list_continuations(text)
blocks = parse_tables(text) blocks = parse_tables(text)
lines = text.splitlines(keepends=True) lines = text.splitlines(keepends=True)
if not lines: if not lines: return
return
table_at_line: dict[int, int] = {b.span[0]: i for i, b in enumerate(blocks)} table_at_line: dict[int, int] = {b.span[0]: i for i, b in enumerate(blocks)}
table_end: dict[int, int] = {b.span[0]: b.span[1] for i, b in enumerate(blocks)} table_end: dict[int, int] = {b.span[0]: b.span[1] for i, b in enumerate(blocks)}
@@ -213,7 +208,6 @@ class MarkdownRenderer:
(we cannot subclass the C++ imgui_md class). (we cannot subclass the C++ imgui_md class).
[C: src/markdown_helper.py:MarkdownRenderer.render] [C: src/markdown_helper.py:MarkdownRenderer.render]
""" """
import re
return re.sub(r"(?m)^([ \t]*)\*[ \t]+", r"\1- ", text) return re.sub(r"(?m)^([ \t]*)\*[ \t]+", r"\1- ", text)
def _normalize_nested_list_endings(self, text: str) -> str: def _normalize_nested_list_endings(self, text: str) -> str:
@@ -226,7 +220,6 @@ class MarkdownRenderer:
paragraph break. Cannot fix the upstream C++ from Python. paragraph break. Cannot fix the upstream C++ from Python.
[C: src/markdown_helper.py:MarkdownRenderer.render] [C: src/markdown_helper.py:MarkdownRenderer.render]
""" """
import re
lines = text.split("\n") lines = text.split("\n")
out: list[str] = [] out: list[str] = []
for i, line in enumerate(lines): for i, line in enumerate(lines):
@@ -261,7 +254,6 @@ class MarkdownRenderer:
a single list item. Acceptable for our use case. a single list item. Acceptable for our use case.
[C: src.markdown_helper:MarkdownRenderer.render] [C: src.markdown_helper:MarkdownRenderer.render]
""" """
import re
lines = text.split("\n") lines = text.split("\n")
out: list[str] = [] out: list[str] = []
prev_was_list = False prev_was_list = False
@@ -288,8 +280,7 @@ class MarkdownRenderer:
next_line = lines[j] next_line = lines[j]
curr_indent = len(next_line) - len(next_line.lstrip()) curr_indent = len(next_line) - len(next_line.lstrip())
is_next_list = bool(re.match(r"^\s*[-*+\d]", next_line)) is_next_list = bool(re.match(r"^\s*[-*+\d]", next_line))
if curr_indent > prev_indent and not is_next_list: if curr_indent > prev_indent and not is_next_list: continue
continue
out.append(line) out.append(line)
prev_was_list = False prev_was_list = False
return "\n".join(out) return "\n".join(out)
@@ -386,8 +377,7 @@ _renderer: Optional[MarkdownRenderer] = None
def get_renderer() -> MarkdownRenderer: def get_renderer() -> MarkdownRenderer:
global _renderer global _renderer
if _renderer is None: if _renderer is None: _renderer = MarkdownRenderer()
_renderer = MarkdownRenderer()
return _renderer return _renderer
def render(text: str, context_id: str = "default") -> None: def render(text: str, context_id: str = "default") -> None:
-2
View File
@@ -106,8 +106,6 @@ perf_monitor_callback: Optional[Callable[[], dict[str, Any]]] = None
def configure(file_items: list[dict[str, Any]], extra_base_dirs: list[str] | None = None) -> None: def configure(file_items: list[dict[str, Any]], extra_base_dirs: list[str] | None = None) -> None:
""" """
Build the allowlist from aggregate file_items. Build the allowlist from aggregate file_items.
Called by ai_client before each send so the list reflects the current project. Called by ai_client before each send so the list reflects the current project.
+2 -3
View File
@@ -41,6 +41,7 @@ from __future__ import annotations
import datetime import datetime
import json import json
import os import os
import sys
import tomllib import tomllib
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -48,6 +49,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
from src.dag_engine import TrackDAG
from src.paths import get_config_path from src.paths import get_config_path
@@ -164,8 +166,6 @@ def load_config() -> dict[str, Any]:
return tomllib.load(f) return tomllib.load(f)
def save_config(config: dict[str, Any]) -> None: def save_config(config: dict[str, Any]) -> None:
import tomli_w
import sys
config = _clean_nones(config) config = _clean_nones(config)
sys.stderr.write(f"[DEBUG] Saving config. Theme: {config.get('theme')}\n") sys.stderr.write(f"[DEBUG] Saving config. Theme: {config.get('theme')}\n")
sys.stderr.flush() sys.stderr.flush()
@@ -345,7 +345,6 @@ class Track:
""" """
[C: tests/test_mma_models.py:test_track_get_executable_tickets, tests/test_mma_models.py:test_track_get_executable_tickets_complex] [C: tests/test_mma_models.py:test_track_get_executable_tickets, tests/test_mma_models.py:test_track_get_executable_tickets_complex]
""" """
from src.dag_engine import TrackDAG
dag = TrackDAG(self.tickets) dag = TrackDAG(self.tickets)
return dag.get_ready_tasks() return dag.get_ready_tasks()