began to go through the files and organize imports and gui_2.py's new context defs
still a bunch to sift through after the last ai passes
This commit is contained in:
@@ -16,14 +16,18 @@ import glob
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from typing import Any, cast
|
||||
|
||||
from src import beads_client
|
||||
from src import project_manager
|
||||
from src import summarize
|
||||
|
||||
from src.file_cache import ASTParser
|
||||
from src.performance_monitor import get_monitor
|
||||
|
||||
|
||||
def find_next_increment(output_dir: Path, namespace: str) -> int:
|
||||
pattern = re.compile(rf"^{re.escape(namespace)}_(\d+)\.md$")
|
||||
max_num = 0
|
||||
|
||||
+14
-7
@@ -16,34 +16,41 @@ import anthropic
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from openai import OpenAI
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import difflib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path as _P
|
||||
import requests # type: ignore[import-untyped]
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import tomllib
|
||||
|
||||
# TODO(Ed): Eliminate These?
|
||||
from collections import deque
|
||||
from typing import Optional, Callable, Any, List, Union, cast, Iterable
|
||||
from pathlib import Path
|
||||
from src.events import EventEmitter
|
||||
from pathlib import Path as _P
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Any, List, Union, cast, Iterable
|
||||
|
||||
from src import project_manager
|
||||
from src import file_cache
|
||||
from src import mcp_client
|
||||
from src import mma_prompts
|
||||
from src import performance_monitor
|
||||
from src import project_manager
|
||||
from src.paths import get_credentials_path
|
||||
from src.tool_bias import ToolBiasEngine
|
||||
from src.models import ToolPreset, BiasProfile, Tool
|
||||
|
||||
# TODO(Ed): Eliminate these?
|
||||
from src.events import EventEmitter
|
||||
from src.gemini_cli_adapter import GeminiCliAdapter
|
||||
from src.models import ToolPreset, BiasProfile, Tool
|
||||
from src.paths import get_credentials_path
|
||||
from src.tool_bias import ToolBiasEngine
|
||||
from src.tool_presets import ToolPresetManager
|
||||
|
||||
|
||||
_provider: str = "gemini"
|
||||
_model: str = "gemini-2.5-flash-lite"
|
||||
_temperature: float = 0.0
|
||||
|
||||
@@ -32,11 +32,14 @@ See Also:
|
||||
- docs/guide_tools.md for Hook API documentation
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import requests # type: ignore[import-untyped]
|
||||
import sys
|
||||
import time
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class ApiHookClient:
|
||||
def __init__(self, base_url: str = "http://127.0.0.1:8999", api_key: str | None = None):
|
||||
"""
|
||||
|
||||
+13
-7
@@ -1,16 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
import sys
|
||||
import asyncio
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
import logging
|
||||
import websockets
|
||||
|
||||
# TODO(Ed): Eliminate these?
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
from websockets.asyncio.server import serve
|
||||
from src import session_logger
|
||||
|
||||
from src import cost_tracker
|
||||
from src import session_logger
|
||||
|
||||
|
||||
"""
|
||||
API Hooks - REST API for external automation and state inspection.
|
||||
|
||||
@@ -820,4 +826,4 @@ class WebSocketServer:
|
||||
return
|
||||
message = json.dumps({"channel": channel, "payload": payload})
|
||||
for ws in list(self.clients[channel]):
|
||||
asyncio.run_coroutine_threadsafe(ws.send(message), self.loop)
|
||||
asyncio.run_coroutine_threadsafe(ws.send(message), self.loop)
|
||||
|
||||
+12
-6
@@ -10,15 +10,18 @@ import time
|
||||
import tomli_w
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
# TODO(Ed): Eliminate these?
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Dict, Optional, Callable
|
||||
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Dict, Optional, Callable
|
||||
|
||||
from src import aggregate
|
||||
from src import models
|
||||
from src.models import GenerateRequest, ConfirmRequest
|
||||
from src import ai_client
|
||||
from src import conductor_tech_lead
|
||||
from src import events
|
||||
@@ -35,8 +38,11 @@ from src import shell_runner
|
||||
from src import theme_2 as theme
|
||||
from src import thinking_parser
|
||||
from src import tool_presets
|
||||
|
||||
from src.context_presets import ContextPresetManager
|
||||
from src.file_cache import ASTParser
|
||||
from src.file_cache import ASTParser
|
||||
from src.models import GenerateRequest, ConfirmRequest
|
||||
|
||||
|
||||
def parse_symbols(text: str) -> list[str]:
|
||||
"""
|
||||
|
||||
+5
-3
@@ -1,8 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class Bead:
|
||||
id: str
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Callable, List, Dict, Any
|
||||
from typing import Optional, Callable, List, Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from src.command_palette import CommandRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -34,10 +34,13 @@ See Also:
|
||||
- src/dag_engine.py for TrackDAG
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
|
||||
from typing import Any
|
||||
|
||||
from src import ai_client
|
||||
from src import mma_prompts
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict[str, Any]]:
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
from src.models import ContextPreset
|
||||
|
||||
|
||||
class ContextPresetManager:
|
||||
"""Manages context presets within the project dictionary (manual_slop.toml)."""
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ See Also:
|
||||
"""
|
||||
import re
|
||||
|
||||
|
||||
# Pricing per 1M tokens in USD
|
||||
MODEL_PRICING = [
|
||||
(r"gemini-2\.5-flash-lite", {"input_per_mtok": 0.075, "output_per_mtok": 0.30}),
|
||||
|
||||
@@ -27,9 +27,11 @@ See Also:
|
||||
- src/multi_agent_conductor.py for ConductorEngine integration
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from src.models import Ticket
|
||||
from src.performance_monitor import get_monitor
|
||||
|
||||
|
||||
class TrackDAG:
|
||||
"""
|
||||
Manages a Directed Acyclic Graph of implementation tickets.
|
||||
|
||||
+5
-3
@@ -1,8 +1,10 @@
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
import shutil
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiffHunk:
|
||||
|
||||
+3
-1
@@ -30,8 +30,10 @@ Thread Safety:
|
||||
- UserRequestEvent: Immutable, safe for concurrent access
|
||||
"""
|
||||
import queue
|
||||
from typing import Callable, Any, Dict, List, Tuple, Optional
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable, Any, Dict, List, Tuple, Optional
|
||||
|
||||
|
||||
class EventEmitter:
|
||||
"""
|
||||
|
||||
@@ -4,8 +4,10 @@ from __future__ import annotations
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# TODO(Ed): Eliminate these?
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List
|
||||
|
||||
from src.models import ExternalEditorConfig, TextEditorConfig
|
||||
|
||||
|
||||
+6
-3
@@ -34,13 +34,16 @@ See Also:
|
||||
- docs/guide_tools.md for AST tool documentation
|
||||
- src/summarize.py for heuristic summaries
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any, List, Tuple, Dict
|
||||
import re
|
||||
import tree_sitter
|
||||
import tree_sitter_python
|
||||
import tree_sitter_cpp
|
||||
import tree_sitter_c
|
||||
import re
|
||||
|
||||
# TODO(Ed): Eliminate these?
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any, List, Tuple, Dict
|
||||
|
||||
|
||||
_ast_cache: Dict[str, Tuple[float, tree_sitter.Tree]] = {}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class FuzzyAnchor:
|
||||
@staticmethod
|
||||
def get_context(lines: list[str], index: int, count: int, direction: int) -> list[str]:
|
||||
|
||||
@@ -33,14 +33,16 @@ See Also:
|
||||
- docs/guide_architecture.md for CLI adapter integration
|
||||
- src/ai_client.py for provider dispatch
|
||||
"""
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import sys
|
||||
from src import session_logger
|
||||
import time
|
||||
|
||||
from typing import Optional, Callable, Any
|
||||
|
||||
from src import session_logger
|
||||
|
||||
|
||||
class GeminiCliAdapter:
|
||||
"""
|
||||
|
||||
+133
-134
@@ -17,7 +17,6 @@ import threading
|
||||
import time
|
||||
import tomli_w
|
||||
import typing
|
||||
from contextlib import ExitStack, nullcontext
|
||||
|
||||
# Ensure thirdparty is in sys.path for defer
|
||||
_project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -25,16 +24,16 @@ _thirdparty = os.path.join(_project_root, "thirdparty")
|
||||
if _thirdparty not in sys.path:
|
||||
sys.path.insert(0, _thirdparty)
|
||||
|
||||
from defer import defer
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
from typing import Optional, Any
|
||||
from defer import defer
|
||||
from contextlib import ExitStack, nullcontext
|
||||
# from defer import defer
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
from typing import Optional, Any
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
from typing import Optional, Any
|
||||
|
||||
from src.diff_viewer import apply_patch_to_file
|
||||
from src import ai_client
|
||||
from src import aggregate
|
||||
@@ -61,6 +60,7 @@ from src import theme_nerv_fx as theme_fx
|
||||
from src import thinking_parser
|
||||
from src import workspace_manager
|
||||
from src.hot_reloader import HotReloader
|
||||
|
||||
if sys.platform == "win32":
|
||||
import win32gui
|
||||
import win32con
|
||||
@@ -2784,8 +2784,6 @@ def render_files_and_media(app: App) -> None:
|
||||
if p not in app.screenshots: app.screenshots.append(p)
|
||||
return
|
||||
|
||||
#endregion: Context Management
|
||||
|
||||
def render_context_batch_actions(app: App, total_lines: int, total_ast: int) -> None:
|
||||
imgui.text("Batch:")
|
||||
for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]:
|
||||
@@ -3361,6 +3359,130 @@ def render_snapshot_tab(app: App) -> None:
|
||||
with imscope.child("last_sys_prompt", 0, 0, True):
|
||||
markdown_helper.render(app.last_resolved_system_prompt, context_id="snapshot_sys")
|
||||
|
||||
def render_empty_context_modal(app: App) -> None:
|
||||
if app.show_empty_context_modal:
|
||||
imgui.open_popup("Empty Context Warning")
|
||||
app.show_empty_context_modal = False
|
||||
|
||||
if imgui.begin_popup_modal("Empty Context Warning", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text_colored(theme.get_color("status_warning"), "WARNING: Empty Context Composition")
|
||||
imgui.text("You are attempting to generate a response without any files selected.")
|
||||
imgui.text("This may result in poor AI performance or loss of project context.")
|
||||
imgui.separator()
|
||||
if imgui.button("Proceed Anyway", imgui.ImVec2(150, 0)):
|
||||
if app._pending_generation_action == 'generate': app.controller._handle_generate_send()
|
||||
elif app._pending_generation_action == 'md_only': app.controller._handle_md_only()
|
||||
app._pending_generation_action = None
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def render_context_modals(app: App) -> None:
|
||||
render_empty_context_modal(app)
|
||||
render_add_context_files_modal(app)
|
||||
|
||||
if app.show_missing_files_modal:
|
||||
imgui.open_popup("Missing Files Warning")
|
||||
app.show_missing_files_modal = False
|
||||
|
||||
if imgui.begin_popup_modal("Missing Files Warning", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text("The following files are missing from disk:")
|
||||
imgui.separator()
|
||||
imgui.begin_child("missing_files_list", imgui.ImVec2(0, 150), True)
|
||||
for f in app.missing_context_files:
|
||||
imgui.text_colored(theme.get_color("status_error"), f)
|
||||
imgui.end_child()
|
||||
imgui.separator()
|
||||
|
||||
if imgui.button("Save Anyway") or getattr(app, "_pending_save_anyway_click", False):
|
||||
app._pending_save_anyway_click = False
|
||||
name = app.target_context_preset_name
|
||||
preset_files = []
|
||||
for f in app.context_files:
|
||||
import copy
|
||||
from src import models
|
||||
p = f.path if hasattr(f, 'path') else str(f)
|
||||
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
|
||||
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
|
||||
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
|
||||
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
|
||||
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
|
||||
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
||||
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
|
||||
app.controller.save_context_preset(preset)
|
||||
app.ui_new_context_preset_name = ""
|
||||
imgui.close_current_popup()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel"):
|
||||
imgui.close_current_popup()
|
||||
|
||||
imgui.end_popup()
|
||||
|
||||
render_ast_inspector_modal(app)
|
||||
|
||||
def _get_context_composition_state(app: App) -> tuple:
|
||||
files_state = []
|
||||
for f in app.context_files:
|
||||
p = f.path if hasattr(f, 'path') else str(f)
|
||||
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
|
||||
agg = f.auto_aggregate if hasattr(f, 'auto_aggregate') else False
|
||||
slc = tuple((s.get('start_line'), s.get('end_line'), s.get('tag'), s.get('comment')) for s in getattr(f, 'custom_slices', []))
|
||||
mask = tuple(sorted(getattr(f, 'ast_mask', {}).items()))
|
||||
files_state.append((p, vm, agg, slc, mask))
|
||||
screenshots_state = tuple(app.screenshots)
|
||||
return (tuple(files_state), screenshots_state)
|
||||
|
||||
def _check_auto_refresh_context_preview(app: App) -> None:
|
||||
current_state = _get_context_composition_state(app)
|
||||
if not hasattr(app, "_last_context_preview_state") or app._last_context_preview_state != current_state:
|
||||
app._last_context_preview_state = current_state
|
||||
if not any(getattr(f, 'auto_aggregate', False) for f in app.context_files) and not app.screenshots:
|
||||
app.context_preview_text = "# Context Composition Empty\n\nNo files or screenshots have been selected for aggregation."
|
||||
return
|
||||
|
||||
if getattr(app, "_is_generating_preview", False):
|
||||
app._pending_preview_refresh = True
|
||||
return
|
||||
|
||||
app._is_generating_preview = True
|
||||
def worker():
|
||||
try:
|
||||
app.controller.context_files = app.context_files
|
||||
res = app.controller._do_generate()
|
||||
app.context_preview_text = res[0]
|
||||
except Exception:
|
||||
app.context_preview_text = "Error generating context preview."
|
||||
finally:
|
||||
app._is_generating_preview = False
|
||||
if getattr(app, "_pending_preview_refresh", False):
|
||||
app._pending_preview_refresh = False
|
||||
# This will trigger again on next GUI frame because _last_context_preview_state
|
||||
# will be slightly behind if another change happened during the thread.
|
||||
# Or we just clear the state so it re-triggers.
|
||||
app._last_context_preview_state = None
|
||||
|
||||
import threading
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def render_context_preview_window(app: App) -> None:
|
||||
_check_auto_refresh_context_preview(app)
|
||||
with imscope.window("Context Preview", app.show_windows["Context Preview"]) as (exp, opened):
|
||||
app.show_windows["Context Preview"] = bool(opened)
|
||||
if not opened:
|
||||
app.ui_separate_context_preview = False
|
||||
if exp:
|
||||
if imgui.button("Close"):
|
||||
app.show_windows["Context Preview"] = False
|
||||
app.ui_separate_context_preview = False
|
||||
imgui.same_line()
|
||||
if imgui.button("Copy to Clipboard"):
|
||||
imgui.set_clipboard_text(app.context_preview_text)
|
||||
with imscope.child("ctx_preview_scroll", 0, 0, True):
|
||||
markdown_helper.render(app.context_preview_text, context_id="ctx_preview")
|
||||
|
||||
#endregion: Context Management
|
||||
|
||||
#region: Discussions
|
||||
@@ -3587,6 +3709,7 @@ def render_session_insights_panel(app: App) -> None:
|
||||
imgui.text(f"Session Cost: ${insights.get('session_cost', 0):.4f}")
|
||||
completed = insights.get('completed_tickets', 0)
|
||||
efficiency = insights.get('efficiency', 0)
|
||||
|
||||
def render_prior_session_view(app: App) -> None:
|
||||
with imscope.style_color(imgui.Col_.child_bg, theme.get_color("bubble_vendor")):
|
||||
if imgui.button("Exit Prior Session"): app.controller.cb_exit_prior_session(); app._comms_log_dirty = True
|
||||
@@ -3611,6 +3734,7 @@ def render_prior_session_view(app: App) -> None:
|
||||
with theme.ai_text_style():
|
||||
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
||||
imgui.separator()
|
||||
|
||||
def render_thinking_indicator(app: App) -> None:
|
||||
is_thinking = app.ai_status in ['sending...', 'streaming...', 'running powershell...']
|
||||
if is_thinking:
|
||||
@@ -5406,128 +5530,3 @@ def render_mma_focus_selector(app: App) -> None:
|
||||
if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None
|
||||
|
||||
#endregion: MMA
|
||||
|
||||
def render_empty_context_modal(app: App) -> None:
|
||||
if app.show_empty_context_modal:
|
||||
imgui.open_popup("Empty Context Warning")
|
||||
app.show_empty_context_modal = False
|
||||
|
||||
if imgui.begin_popup_modal("Empty Context Warning", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text_colored(theme.get_color("status_warning"), "WARNING: Empty Context Composition")
|
||||
imgui.text("You are attempting to generate a response without any files selected.")
|
||||
imgui.text("This may result in poor AI performance or loss of project context.")
|
||||
imgui.separator()
|
||||
if imgui.button("Proceed Anyway", imgui.ImVec2(150, 0)):
|
||||
if app._pending_generation_action == 'generate': app.controller._handle_generate_send()
|
||||
elif app._pending_generation_action == 'md_only': app.controller._handle_md_only()
|
||||
app._pending_generation_action = None
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def render_context_modals(app: App) -> None:
|
||||
render_empty_context_modal(app)
|
||||
render_add_context_files_modal(app)
|
||||
|
||||
if app.show_missing_files_modal:
|
||||
imgui.open_popup("Missing Files Warning")
|
||||
app.show_missing_files_modal = False
|
||||
|
||||
if imgui.begin_popup_modal("Missing Files Warning", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text("The following files are missing from disk:")
|
||||
imgui.separator()
|
||||
imgui.begin_child("missing_files_list", imgui.ImVec2(0, 150), True)
|
||||
for f in app.missing_context_files:
|
||||
imgui.text_colored(theme.get_color("status_error"), f)
|
||||
imgui.end_child()
|
||||
imgui.separator()
|
||||
|
||||
if imgui.button("Save Anyway") or getattr(app, "_pending_save_anyway_click", False):
|
||||
app._pending_save_anyway_click = False
|
||||
name = app.target_context_preset_name
|
||||
preset_files = []
|
||||
for f in app.context_files:
|
||||
import copy
|
||||
from src import models
|
||||
p = f.path if hasattr(f, 'path') else str(f)
|
||||
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
|
||||
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
|
||||
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
|
||||
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
|
||||
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
|
||||
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
|
||||
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
|
||||
app.controller.save_context_preset(preset)
|
||||
app.ui_new_context_preset_name = ""
|
||||
imgui.close_current_popup()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel"):
|
||||
imgui.close_current_popup()
|
||||
|
||||
imgui.end_popup()
|
||||
|
||||
render_ast_inspector_modal(app)
|
||||
|
||||
def _get_context_composition_state(app: App) -> tuple:
|
||||
files_state = []
|
||||
for f in app.context_files:
|
||||
p = f.path if hasattr(f, 'path') else str(f)
|
||||
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
|
||||
agg = f.auto_aggregate if hasattr(f, 'auto_aggregate') else False
|
||||
slc = tuple((s.get('start_line'), s.get('end_line'), s.get('tag'), s.get('comment')) for s in getattr(f, 'custom_slices', []))
|
||||
mask = tuple(sorted(getattr(f, 'ast_mask', {}).items()))
|
||||
files_state.append((p, vm, agg, slc, mask))
|
||||
screenshots_state = tuple(app.screenshots)
|
||||
return (tuple(files_state), screenshots_state)
|
||||
|
||||
def _check_auto_refresh_context_preview(app: App) -> None:
|
||||
current_state = _get_context_composition_state(app)
|
||||
if not hasattr(app, "_last_context_preview_state") or app._last_context_preview_state != current_state:
|
||||
app._last_context_preview_state = current_state
|
||||
if not any(getattr(f, 'auto_aggregate', False) for f in app.context_files) and not app.screenshots:
|
||||
app.context_preview_text = "# Context Composition Empty\n\nNo files or screenshots have been selected for aggregation."
|
||||
return
|
||||
|
||||
if getattr(app, "_is_generating_preview", False):
|
||||
app._pending_preview_refresh = True
|
||||
return
|
||||
|
||||
app._is_generating_preview = True
|
||||
def worker():
|
||||
try:
|
||||
app.controller.context_files = app.context_files
|
||||
res = app.controller._do_generate()
|
||||
app.context_preview_text = res[0]
|
||||
except Exception:
|
||||
app.context_preview_text = "Error generating context preview."
|
||||
finally:
|
||||
app._is_generating_preview = False
|
||||
if getattr(app, "_pending_preview_refresh", False):
|
||||
app._pending_preview_refresh = False
|
||||
# This will trigger again on next GUI frame because _last_context_preview_state
|
||||
# will be slightly behind if another change happened during the thread.
|
||||
# Or we just clear the state so it re-triggers.
|
||||
app._last_context_preview_state = None
|
||||
|
||||
import threading
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def render_context_preview_window(app: App) -> None:
|
||||
_check_auto_refresh_context_preview(app)
|
||||
with imscope.window("Context Preview", app.show_windows["Context Preview"]) as (exp, opened):
|
||||
app.show_windows["Context Preview"] = bool(opened)
|
||||
if not opened:
|
||||
app.ui_separate_context_preview = False
|
||||
if exp:
|
||||
if imgui.button("Close"):
|
||||
app.show_windows["Context Preview"] = False
|
||||
app.ui_separate_context_preview = False
|
||||
imgui.same_line()
|
||||
if imgui.button("Copy to Clipboard"):
|
||||
imgui.set_clipboard_text(app.context_preview_text)
|
||||
with imscope.child("ctx_preview_scroll", 0, 0, True):
|
||||
markdown_helper.render(app.context_preview_text, context_id="ctx_preview")
|
||||
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import typing
|
||||
import time
|
||||
import typing
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class UISnapshot:
|
||||
"""Capture of restorable UI state."""
|
||||
|
||||
+5
-2
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class HotModule:
|
||||
name: str
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from imgui_bundle import imgui
|
||||
from imgui_bundle import imgui_node_editor
|
||||
|
||||
|
||||
def child(id_str: str, size_x: float = 0, size_y: float = 0, flags: int = 0): return _ScopeChild(id_str, size_x, size_y, flags)
|
||||
class _ScopeChild:
|
||||
def __init__(self, id_str: str, size_x: float | imgui.ImVec2, size_y: float, flags: int):
|
||||
|
||||
@@ -2,9 +2,12 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from src.log_registry import LogRegistry
|
||||
|
||||
|
||||
class LogPruner:
|
||||
"""
|
||||
|
||||
|
||||
+4
-2
@@ -38,11 +38,13 @@ See Also:
|
||||
- src/paths.py for registry path resolution
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tomli_w
|
||||
import tomllib
|
||||
|
||||
from datetime import datetime
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import Any
|
||||
|
||||
|
||||
class LogRegistry:
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# src/markdown_helper.py
|
||||
from __future__ import annotations
|
||||
from imgui_bundle import imgui_md, imgui, immapp, imgui_color_text_edit as ed
|
||||
import webbrowser
|
||||
|
||||
import os
|
||||
import re
|
||||
import webbrowser
|
||||
|
||||
from imgui_bundle import imgui_md, imgui, immapp, imgui_color_text_edit as ed
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Callable
|
||||
from typing import Optional, Dict, Callable
|
||||
|
||||
|
||||
def _get_language_id(name: str):
|
||||
"""Get a language identifier for ImGuiColorTextEdit.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dataclasses import dataclass
|
||||
from imgui_bundle import imgui, imgui_md
|
||||
|
||||
|
||||
_TABLE_SEPARATOR = re.compile(r"^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$")
|
||||
|
||||
def render_table(block: "TableBlock") -> None:
|
||||
|
||||
+9
-4
@@ -52,22 +52,27 @@ See Also:
|
||||
#MCP-style file context tools for manual_slop.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re as _re
|
||||
import subprocess
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Any, cast
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Any, cast
|
||||
|
||||
from scripts import py_struct_tools
|
||||
|
||||
from src import beads_client
|
||||
from src import models
|
||||
from src import outline_tool
|
||||
from src import summarize
|
||||
from scripts import py_struct_tools
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ mutating tools sentinel
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Contains templates and static strings for hierarchical orchestration.
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
# --- Tier 1 (Strategic/Orchestration: PM) ---
|
||||
|
||||
TIER1_BASE_SYSTEM: str = """
|
||||
|
||||
+9
-5
@@ -37,16 +37,20 @@ See Also:
|
||||
- src/project_manager.py for persistence layer
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
import tomllib
|
||||
from pydantic import BaseModel
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.paths import get_config_path
|
||||
|
||||
|
||||
#region: Constants
|
||||
|
||||
PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax"]
|
||||
@@ -1040,4 +1044,4 @@ def load_mcp_config(path: str) -> MCPConfiguration:
|
||||
except Exception:
|
||||
return MCPConfiguration()
|
||||
|
||||
#endregion: MCP Config
|
||||
#endregion: MCP Config
|
||||
|
||||
@@ -27,23 +27,26 @@ See Also:
|
||||
- src/dag_engine.py for TrackDAG and ExecutionEngine
|
||||
- src/models.py for Ticket, Track, WorkerContext
|
||||
"""
|
||||
from src import ai_client
|
||||
from src import summarize
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import List, Optional, Tuple, Callable
|
||||
|
||||
from dataclasses import asdict
|
||||
from src import events
|
||||
from src import models
|
||||
from src.models import Ticket, Track, WorkerContext
|
||||
from src.file_cache import ASTParser
|
||||
from pathlib import Path
|
||||
from src.personas import PersonaManager
|
||||
from typing import List, Optional, Tuple, Callable
|
||||
|
||||
from src import ai_client
|
||||
from src import events
|
||||
from src.file_cache import ASTParser
|
||||
from src import models
|
||||
from src import paths
|
||||
from src import summarize
|
||||
|
||||
from src.dag_engine import TrackDAG, ExecutionEngine
|
||||
from src.models import Ticket, Track, WorkerContext
|
||||
from src.personas import PersonaManager
|
||||
|
||||
|
||||
class WorkerPool:
|
||||
"""
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
|
||||
import json
|
||||
from src import ai_client
|
||||
from src import mma_prompts
|
||||
from src import aggregate
|
||||
from src import summarize
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from src import aggregate
|
||||
from src import ai_client
|
||||
from src import mma_prompts
|
||||
from src import paths
|
||||
from src import summarize
|
||||
|
||||
|
||||
def get_track_history_summary() -> str:
|
||||
"""
|
||||
|
||||
@@ -31,6 +31,7 @@ See Also:
|
||||
- src/summarize.py for heuristic file summaries
|
||||
"""
|
||||
import ast
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
from typing import Optional, Callable, List
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Callable, List
|
||||
|
||||
|
||||
@dataclass
|
||||
class PendingPatch:
|
||||
|
||||
+6
-3
@@ -40,10 +40,12 @@ See Also:
|
||||
- src/session_logger.py for logging paths
|
||||
- src/project_manager.py for project paths
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import tomllib
|
||||
from typing import Optional, Any
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any
|
||||
|
||||
|
||||
_RESOLVED: dict[str, Path] = {}
|
||||
|
||||
@@ -237,4 +239,5 @@ def reset_resolved() -> None:
|
||||
For testing only - clear cached resolutions.
|
||||
[C: tests/conftest.py:reset_paths, tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_gui_phase3.py:test_conductor_setup_scan, tests/test_paths.py:reset_paths, tests/test_project_paths.py:test_get_all_tracks_project_specific, tests/test_project_paths.py:test_get_conductor_dir_default, tests/test_project_paths.py:test_get_conductor_dir_project_specific_with_toml]
|
||||
"""
|
||||
_RESOLVED.clear()
|
||||
_RESOLVED.clear()
|
||||
|
||||
@@ -53,12 +53,15 @@ Integration:
|
||||
- Exposed via Hook API at /api/performance
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import psutil
|
||||
import sys
|
||||
import time
|
||||
import psutil
|
||||
import threading
|
||||
from typing import Any, Optional, Callable, Dict, List
|
||||
|
||||
from collections import deque
|
||||
from typing import Any, Optional, Callable, Dict, List
|
||||
|
||||
|
||||
_instance: Optional[PerformanceMonitor] = None
|
||||
|
||||
|
||||
+4
-2
@@ -1,9 +1,11 @@
|
||||
import tomllib
|
||||
import tomli_w
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from src.models import Persona
|
||||
from src import paths
|
||||
from src import paths
|
||||
|
||||
class PersonaManager:
|
||||
"""Manages Persona profiles across global and project-specific files."""
|
||||
|
||||
+5
-2
@@ -1,10 +1,13 @@
|
||||
import sys
|
||||
import tomllib
|
||||
import tomli_w
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from src.models import Preset
|
||||
from src.paths import get_global_presets_path, get_project_presets_path
|
||||
from src.paths import get_global_presets_path, get_project_presets_path
|
||||
|
||||
|
||||
class PresetManager:
|
||||
"""Manages system prompt presets across global and project-specific files."""
|
||||
|
||||
@@ -5,19 +5,24 @@ Handles loading/saving of project .toml configurations.
|
||||
Also handles serializing the discussion history into the TOML format using a special
|
||||
@timestamp prefix to preserve the exact sequence of events.
|
||||
"""
|
||||
import subprocess
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import tomllib
|
||||
import tomli_w
|
||||
import re
|
||||
import json
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
|
||||
from src import paths
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models import TrackState
|
||||
|
||||
TS_FMT: str = "%Y-%m-%dT%H:%M:%S"
|
||||
|
||||
|
||||
def now_ts() -> str:
|
||||
return datetime.datetime.now().strftime(TS_FMT)
|
||||
|
||||
|
||||
+6
-3
@@ -1,9 +1,11 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import json
|
||||
import copy
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from src import models
|
||||
from src import mcp_client
|
||||
|
||||
@@ -11,6 +13,7 @@ _SENTENCE_TRANSFORMERS = None
|
||||
_GOOGLE_GENAI = None
|
||||
_CHROMADB = None
|
||||
|
||||
|
||||
def _get_sentence_transformers():
|
||||
global _SENTENCE_TRANSFORMERS
|
||||
if _SENTENCE_TRANSFORMERS is None:
|
||||
|
||||
@@ -20,11 +20,13 @@ import atexit
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
from typing import Any, Optional, TextIO
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, TextIO
|
||||
|
||||
from src import paths
|
||||
|
||||
|
||||
_ts: str = "" # session timestamp string e.g. "20260301_142233"
|
||||
_session_id: str = "" # YYYYMMDD_HHMMSS[_Label]
|
||||
_session_dir: Optional[Path] = None # Path to the sub-directory for this session
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from imgui_bundle import imgui
|
||||
|
||||
|
||||
def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: imgui.ImVec2, color: imgui.ImVec4, shadow_size: float = 10.0, rounding: float = 0.0) -> None:
|
||||
"""
|
||||
|
||||
|
||||
+4
-2
@@ -7,10 +7,11 @@ configuring the environment via mcp_env.toml. It handles timeouts,
|
||||
logging, and optional QA/patch callbacks for error recovery.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
@@ -20,6 +21,7 @@ except ImportError:
|
||||
TIMEOUT_SECONDS: int = 60
|
||||
_ENV_CONFIG: dict = {}
|
||||
|
||||
|
||||
def _load_env_config() -> dict:
|
||||
"""Load mcp_env.toml from project root or environment variable."""
|
||||
env_path = os.environ.get("SLOP_MCP_ENV")
|
||||
|
||||
+5
-1
@@ -24,12 +24,16 @@ context block that replaces full file contents in the initial <context> send.
|
||||
"""
|
||||
import ast
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable, Any
|
||||
from typing import Callable, Any
|
||||
|
||||
from src.summary_cache import SummaryCache, get_file_hash
|
||||
|
||||
|
||||
_summary_cache = SummaryCache()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ per-type extractors
|
||||
|
||||
def _summarise_python(path: Path, content: str) -> str:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict
|
||||
|
||||
|
||||
def get_file_hash(content: str) -> str:
|
||||
"""
|
||||
|
||||
+11
-7
@@ -8,16 +8,20 @@ Font loading uses hello_imgui.load_font().
|
||||
Scale uses imgui.get_style().font_scale_main.
|
||||
"""
|
||||
|
||||
from imgui_bundle import imgui, hello_imgui
|
||||
from typing import Any, Optional
|
||||
import typing
|
||||
from contextlib import nullcontext
|
||||
from src import imgui_scopes as imscope
|
||||
|
||||
from contextlib import nullcontext
|
||||
from imgui_bundle import imgui, hello_imgui
|
||||
from typing import Any, Optional
|
||||
|
||||
import src.theme_nerv
|
||||
from src.theme_nerv import DATA_GREEN
|
||||
|
||||
from src import imgui_scopes as imscope
|
||||
from src.theme_nerv import DATA_GREEN
|
||||
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
|
||||
from src.paths import get_global_themes_path
|
||||
from src.theme_models import ThemeFile, load_themes_from_dir, load_themes_from_toml
|
||||
from src.paths import get_global_themes_path
|
||||
from src.theme_models import ThemeFile, load_themes_from_dir, load_themes_from_toml
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ palettes
|
||||
|
||||
|
||||
+4
-2
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
VALID_SYNTAX_PALETTES: tuple[str, ...] = ("dark", "light", "mariana", "retro_blue")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from imgui_bundle import imgui
|
||||
|
||||
|
||||
def _c(r: int, g: int, b: int, a: int = 255) -> tuple[float, float, float, float]:
|
||||
"""Convert 0-255 RGBA to 0.0-1.0 floats."""
|
||||
return (r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import time
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
|
||||
from imgui_bundle import imgui
|
||||
|
||||
|
||||
class CRTFilter:
|
||||
def __init__(self):
|
||||
self.enabled = True
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import re
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
from src.models import ThinkingSegment
|
||||
|
||||
|
||||
def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from src.models import Tool, ToolPreset, BiasProfile
|
||||
|
||||
|
||||
class ToolBiasEngine:
|
||||
def apply_semantic_nudges(self, tool_definitions: List[Dict[str, Any]], preset: ToolPreset) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
|
||||
+5
-2
@@ -1,10 +1,13 @@
|
||||
import tomllib
|
||||
import tomli_w
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Union, Any
|
||||
from src import paths
|
||||
from typing import Dict, List, Optional, Union, Any
|
||||
|
||||
from src import paths
|
||||
from src.models import ToolPreset, BiasProfile
|
||||
|
||||
|
||||
class ToolPresetManager:
|
||||
def __init__(self, project_root: Optional[Union[str, Path]] = None):
|
||||
self.project_root = Path(project_root) if project_root else None
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VendorMetric:
|
||||
"""Atomic vendor-state metric.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import tomllib
|
||||
import tomli_w
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, Union
|
||||
from typing import Dict, Any, Optional, Union
|
||||
|
||||
from src.models import WorkspaceProfile
|
||||
from src import paths
|
||||
from src import paths
|
||||
|
||||
|
||||
class WorkspaceManager:
|
||||
"""Manages Workspace profiles across global and project-specific files."""
|
||||
|
||||
Reference in New Issue
Block a user