some organization pass, still need to review a bunch
This commit is contained in:
+1
-1
@@ -4170,7 +4170,7 @@ def render_operations_hub(app: App) -> None:
|
|||||||
with imscope.tab_item("Vendor State") as (exp, _):
|
with imscope.tab_item("Vendor State") as (exp, _):
|
||||||
if exp: render_vendor_state(app)
|
if exp: render_vendor_state(app)
|
||||||
|
|
||||||
def render_vendor_state(app: App) -> None:
|
def render_vendor_state(app: App) -> None: # TODO(Ed): Shouldn't this just be a part of usage analytics? We can show all used vendors at once...
|
||||||
"""Render the Operations Hub > Vendor State panel.
|
"""Render the Operations Hub > Vendor State panel.
|
||||||
[C: src/vendor_state.py:get_vendor_state]
|
[C: src/vendor_state.py:get_vendor_state]
|
||||||
"""
|
"""
|
||||||
|
|||||||
+1
-1
@@ -121,4 +121,4 @@ class PresetManager:
|
|||||||
raise ValueError(f"Cannot save to {path}: Parent directory {path.parent} is a file.")
|
raise ValueError(f"Cannot save to {path}: Parent directory {path.parent} is a file.")
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(tomli_w.dumps(data).encode("utf-8"))
|
f.write(tomli_w.dumps(data).encode("utf-8"))
|
||||||
|
|||||||
@@ -35,9 +35,8 @@ def parse_ts(s: str) -> Optional[datetime.datetime]:
|
|||||||
|
|
||||||
def entry_to_str(entry: dict[str, Any]) -> str:
|
def entry_to_str(entry: dict[str, Any]) -> str:
|
||||||
"""
|
"""
|
||||||
|
Serialise a disc entry dict -> stored string.
|
||||||
Serialise a disc entry dict -> stored string.
|
[C: tests/test_thinking_persistence.py:test_entry_to_str_with_thinking]
|
||||||
[C: tests/test_thinking_persistence.py:test_entry_to_str_with_thinking]
|
|
||||||
"""
|
"""
|
||||||
ts = entry.get("ts", "")
|
ts = entry.get("ts", "")
|
||||||
role = entry.get("role", "User")
|
role = entry.get("role", "User")
|
||||||
@@ -56,15 +55,14 @@ def entry_to_str(entry: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
def format_discussion(entries: list[dict[str, Any]]) -> str:
|
def format_discussion(entries: list[dict[str, Any]]) -> str:
|
||||||
"""
|
"""
|
||||||
Convert a list of discussion entry dicts into a single formatted string.
|
Convert a list of discussion entry dicts into a single formatted string.
|
||||||
"""
|
"""
|
||||||
return "\n\n".join([entry_to_str(e) for e in entries])
|
return "\n\n".join([entry_to_str(e) for e in entries])
|
||||||
|
|
||||||
def str_to_entry(raw: str, roles: list[str]) -> dict[str, Any]:
|
def str_to_entry(raw: str, roles: list[str]) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
Parse a stored string back to a disc entry dict.
|
||||||
Parse a stored string back to a disc entry dict.
|
[C: tests/test_thinking_persistence.py:test_str_to_entry_with_thinking]
|
||||||
[C: tests/test_thinking_persistence.py:test_str_to_entry_with_thinking]
|
|
||||||
"""
|
"""
|
||||||
ts = ""
|
ts = ""
|
||||||
rest = raw
|
rest = raw
|
||||||
@@ -492,4 +490,4 @@ def promote_take(project_dict: dict, take_id: str, new_id: str) -> None:
|
|||||||
|
|
||||||
# If the take was active, update the active pointer
|
# If the take was active, update the active pointer
|
||||||
if project_dict["discussion"].get("active") == take_id:
|
if project_dict["discussion"].get("active") == take_id:
|
||||||
project_dict["discussion"]["active"] = new_id
|
project_dict["discussion"]["active"] = new_id
|
||||||
|
|||||||
+235
-237
@@ -6,288 +6,286 @@ import sys
|
|||||||
|
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
from src import ai_client
|
||||||
from src import models
|
from src import models
|
||||||
from src import mcp_client
|
from src import mcp_client
|
||||||
|
|
||||||
|
from src.file_cache import ASTParser
|
||||||
|
|
||||||
|
|
||||||
_SENTENCE_TRANSFORMERS = None
|
_SENTENCE_TRANSFORMERS = None
|
||||||
_GOOGLE_GENAI = None
|
_GOOGLE_GENAI = None
|
||||||
_CHROMADB = None
|
_CHROMADB = None
|
||||||
|
|
||||||
|
|
||||||
def _get_sentence_transformers():
|
def _get_sentence_transformers():
|
||||||
global _SENTENCE_TRANSFORMERS
|
global _SENTENCE_TRANSFORMERS
|
||||||
if _SENTENCE_TRANSFORMERS is None:
|
if _SENTENCE_TRANSFORMERS is None:
|
||||||
try:
|
try:
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
_SENTENCE_TRANSFORMERS = SentenceTransformer
|
_SENTENCE_TRANSFORMERS = SentenceTransformer
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write(f"FAILED to import sentence_transformers: {e}\n")
|
sys.stderr.write(f"FAILED to import sentence_transformers: {e}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
raise e
|
raise e
|
||||||
return _SENTENCE_TRANSFORMERS
|
return _SENTENCE_TRANSFORMERS
|
||||||
|
|
||||||
def _get_google_genai():
|
def _get_google_genai():
|
||||||
global _GOOGLE_GENAI
|
global _GOOGLE_GENAI
|
||||||
if _GOOGLE_GENAI is None:
|
if _GOOGLE_GENAI is None:
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
_GOOGLE_GENAI = (genai, types)
|
_GOOGLE_GENAI = (genai, types)
|
||||||
return _GOOGLE_GENAI
|
return _GOOGLE_GENAI
|
||||||
|
|
||||||
def _get_chromadb():
|
def _get_chromadb():
|
||||||
global _CHROMADB
|
global _CHROMADB
|
||||||
if _CHROMADB is None:
|
if _CHROMADB is None:
|
||||||
import chromadb
|
import chromadb
|
||||||
from chromadb.config import Settings
|
from chromadb.config import Settings
|
||||||
_CHROMADB = (chromadb, Settings)
|
_CHROMADB = (chromadb, Settings)
|
||||||
return _CHROMADB
|
return _CHROMADB
|
||||||
|
|
||||||
class BaseEmbeddingProvider:
|
class BaseEmbeddingProvider:
|
||||||
def embed(self, texts: List[str]) -> List[List[float]]:
|
def embed(self, texts: List[str]) -> List[List[float]]:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
class LocalEmbeddingProvider(BaseEmbeddingProvider):
|
class LocalEmbeddingProvider(BaseEmbeddingProvider):
|
||||||
def __init__(self, model_name: str = 'all-MiniLM-L6-v2'):
|
def __init__(self, model_name: str = 'all-MiniLM-L6-v2'):
|
||||||
self.model = None
|
self.model = None
|
||||||
try:
|
try:
|
||||||
ST = _get_sentence_transformers()
|
ST = _get_sentence_transformers()
|
||||||
if ST:
|
if ST:
|
||||||
self.model = ST(model_name)
|
self.model = ST(model_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write(f"LocalEmbeddingProvider failed to load model {model_name}: {e}. Using dummy embeddings.\n")
|
sys.stderr.write(f"LocalEmbeddingProvider failed to load model {model_name}: {e}. Using dummy embeddings.\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
def embed(self, texts: List[str]) -> List[List[float]]:
|
def embed(self, texts: List[str]) -> List[List[float]]:
|
||||||
if self.model:
|
if self.model:
|
||||||
embeddings = self.model.encode(texts)
|
embeddings = self.model.encode(texts)
|
||||||
return embeddings.tolist()
|
return embeddings.tolist()
|
||||||
else:
|
else:
|
||||||
# Dummy embeddings (384 dims for all-MiniLM-L6-v2)
|
# Dummy embeddings (384 dims for all-MiniLM-L6-v2)
|
||||||
return [[0.0] * 384 for _ in texts]
|
return [[0.0] * 384 for _ in texts]
|
||||||
|
|
||||||
class GeminiEmbeddingProvider(BaseEmbeddingProvider):
|
class GeminiEmbeddingProvider(BaseEmbeddingProvider):
|
||||||
def __init__(self, model_name: str = 'gemini-embedding-001'):
|
def __init__(self, model_name: str = 'gemini-embedding-001'):
|
||||||
self.model_name = model_name
|
self.model_name = model_name
|
||||||
|
|
||||||
def embed(self, texts: List[str]) -> List[List[float]]:
|
def embed(self, texts: List[str]) -> List[List[float]]:
|
||||||
google_module = _get_google_genai()
|
google_module = _get_google_genai()
|
||||||
if google_module is None:
|
if google_module is None:
|
||||||
raise ImportError("google-genai is not installed")
|
raise ImportError("google-genai is not installed")
|
||||||
genai_pkg, types = google_module
|
genai_pkg, types = google_module
|
||||||
from src import ai_client
|
ai_client._ensure_gemini_client()
|
||||||
ai_client._ensure_gemini_client()
|
client = ai_client._gemini_client
|
||||||
client = ai_client._gemini_client
|
if not client:
|
||||||
if not client:
|
raise ValueError("Gemini client not initialized")
|
||||||
raise ValueError("Gemini client not initialized")
|
res = client.models.embed_content(
|
||||||
res = client.models.embed_content(
|
model = self.model_name,
|
||||||
model=self.model_name,
|
contents = texts,
|
||||||
contents=texts,
|
config = types.EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT")
|
||||||
config=types.EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT")
|
)
|
||||||
)
|
return [e.values for e in res.embeddings]
|
||||||
return [e.values for e in res.embeddings]
|
|
||||||
|
|
||||||
class RAGEngine:
|
class RAGEngine:
|
||||||
def __init__(self, config: models.RAGConfig, base_dir: str = "."):
|
def __init__(self, config: models.RAGConfig, base_dir: str = "."):
|
||||||
self.config = copy.deepcopy(config)
|
self.config = copy.deepcopy(config)
|
||||||
self.base_dir = base_dir
|
self.base_dir = base_dir
|
||||||
self.client = None
|
self.client = None
|
||||||
self.collection = None
|
self.collection = None
|
||||||
self.embedding_provider = None
|
self.embedding_provider = None
|
||||||
|
|
||||||
if not self.config.enabled:
|
if not self.config.enabled: return
|
||||||
return
|
|
||||||
|
|
||||||
self._init_embedding_provider()
|
self._init_embedding_provider()
|
||||||
self._init_vector_store()
|
self._init_vector_store()
|
||||||
|
|
||||||
def _init_embedding_provider(self):
|
def _init_embedding_provider(self):
|
||||||
if self.config.embedding_provider == 'gemini':
|
if self.config.embedding_provider == 'gemini':
|
||||||
self.embedding_provider = GeminiEmbeddingProvider()
|
self.embedding_provider = GeminiEmbeddingProvider()
|
||||||
elif self.config.embedding_provider == 'local':
|
elif self.config.embedding_provider == 'local':
|
||||||
self.embedding_provider = LocalEmbeddingProvider()
|
self.embedding_provider = LocalEmbeddingProvider()
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown embedding provider: {self.config.embedding_provider}")
|
raise ValueError(f"Unknown embedding provider: {self.config.embedding_provider}")
|
||||||
|
|
||||||
def _init_vector_store(self):
|
def _init_vector_store(self):
|
||||||
vs_config = self.config.vector_store
|
vs_config = self.config.vector_store
|
||||||
if vs_config.provider == 'chroma':
|
if vs_config.provider == 'chroma':
|
||||||
# Use a collection-specific path to avoid dimension conflicts and locks between tests
|
# Use a collection-specific path to avoid dimension conflicts and locks between tests
|
||||||
db_path = os.path.abspath(os.path.join(self.base_dir, ".slop_cache", f"chroma_{vs_config.collection_name}"))
|
db_path = os.path.abspath(os.path.join(self.base_dir, ".slop_cache", f"chroma_{vs_config.collection_name}"))
|
||||||
os.makedirs(db_path, exist_ok=True)
|
os.makedirs(db_path, exist_ok=True)
|
||||||
chroma_module = _get_chromadb()
|
chroma_module = _get_chromadb()
|
||||||
if chroma_module is None:
|
if chroma_module is None:
|
||||||
raise ImportError("chromadb is not installed")
|
raise ImportError("chromadb is not installed")
|
||||||
chromadb, Settings = chroma_module
|
chromadb, Settings = chroma_module
|
||||||
self.client = chromadb.PersistentClient(path=db_path)
|
self.client = chromadb.PersistentClient(path=db_path)
|
||||||
self.collection = self.client.get_or_create_collection(name=vs_config.collection_name)
|
self.collection = self.client.get_or_create_collection(name=vs_config.collection_name)
|
||||||
elif vs_config.provider == 'mock':
|
elif vs_config.provider == 'mock':
|
||||||
self.client = "mock"
|
self.client = "mock"
|
||||||
self.collection = "mock"
|
self.collection = "mock"
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown vector store provider: {vs_config.provider}")
|
raise ValueError(f"Unknown vector store provider: {vs_config.provider}")
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
if not self.config.enabled:
|
if not self.config.enabled:
|
||||||
return True
|
return True
|
||||||
if self.config.vector_store.provider == 'mock' or self.collection == "mock":
|
if self.config.vector_store.provider == 'mock' or self.collection == "mock":
|
||||||
return True
|
return True
|
||||||
if self.collection is None:
|
if self.collection is None:
|
||||||
return True
|
return True
|
||||||
return self.collection.count() == 0
|
return self.collection.count() == 0
|
||||||
|
|
||||||
def add_documents(self, ids: List[str], texts: List[str], metadatas: Optional[List[Dict[str, Any]]] = None):
|
def add_documents(self, ids: List[str], texts: List[str], metadatas: Optional[List[Dict[str, Any]]] = None):
|
||||||
"""
|
"""
|
||||||
[C: tests/test_rag_engine.py:test_rag_engine_chroma]
|
[C: tests/test_rag_engine.py:test_rag_engine_chroma]
|
||||||
"""
|
"""
|
||||||
if not self.config.enabled or self.collection == "mock":
|
if not self.config.enabled or self.collection == "mock":
|
||||||
return
|
return
|
||||||
embeddings = self.embedding_provider.embed(texts)
|
embeddings = self.embedding_provider.embed(texts)
|
||||||
self.collection.upsert(
|
self.collection.upsert(
|
||||||
ids=ids,
|
ids = ids,
|
||||||
embeddings=embeddings,
|
embeddings = embeddings,
|
||||||
documents=texts,
|
documents = texts,
|
||||||
metadatas=metadatas
|
metadatas = metadatas
|
||||||
)
|
)
|
||||||
|
|
||||||
def _chunk_text(self, content: str) -> List[str]:
|
def _chunk_text(self, content: str) -> List[str]:
|
||||||
"""Character-based chunking with overlap."""
|
"""Character-based chunking with overlap."""
|
||||||
chunks = []
|
chunks = []
|
||||||
if not content:
|
if not content:
|
||||||
return chunks
|
return chunks
|
||||||
chunk_size = self.config.chunk_size
|
chunk_size = self.config.chunk_size
|
||||||
overlap = self.config.chunk_overlap
|
overlap = self.config.chunk_overlap
|
||||||
start = 0
|
start = 0
|
||||||
while start < len(content):
|
while start < len(content):
|
||||||
end = start + chunk_size
|
end = start + chunk_size
|
||||||
chunks.append(content[start:end])
|
chunks.append(content[start:end])
|
||||||
if end >= len(content):
|
if end >= len(content):
|
||||||
break
|
break
|
||||||
start += (chunk_size - overlap)
|
start += (chunk_size - overlap)
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
def _chunk_code(self, content: str, file_path: str) -> List[str]:
|
def _chunk_code(self, content: str, file_path: str) -> List[str]:
|
||||||
"""AST-aware chunking for Python code."""
|
"""AST-aware chunking for Python code."""
|
||||||
try:
|
try:
|
||||||
from src.file_cache import ASTParser
|
parser = ASTParser("python")
|
||||||
parser = ASTParser("python")
|
tree = parser.parse(content)
|
||||||
tree = parser.parse(content)
|
chunks = []
|
||||||
chunks = []
|
|
||||||
|
|
||||||
for node in tree.root_node.children:
|
for node in tree.root_node.children:
|
||||||
if node.type in ("function_definition", "class_definition"):
|
if node.type in ("function_definition", "class_definition"):
|
||||||
chunks.append(content[node.start_byte:node.end_byte])
|
chunks.append(content[node.start_byte:node.end_byte])
|
||||||
|
|
||||||
if not chunks or len(content) < self.config.chunk_size:
|
if not chunks or len(content) < self.config.chunk_size:
|
||||||
return self._chunk_text(content)
|
return self._chunk_text(content)
|
||||||
return chunks
|
return chunks
|
||||||
except Exception:
|
except Exception:
|
||||||
return self._chunk_text(content)
|
return self._chunk_text(content)
|
||||||
|
|
||||||
def index_file(self, file_path: str):
|
def index_file(self, file_path: str):
|
||||||
"""Reads, chunks, and indexes a file into the vector store."""
|
"""Reads, chunks, and indexes a file into the vector store."""
|
||||||
if not self.config.enabled or self.collection == "mock":
|
if not self.config.enabled or self.collection == "mock":
|
||||||
return
|
return
|
||||||
|
|
||||||
full_path = os.path.join(self.base_dir, file_path)
|
full_path = os.path.join(self.base_dir, file_path)
|
||||||
if not os.path.exists(full_path):
|
if not os.path.exists(full_path):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mtime = os.path.getmtime(full_path)
|
mtime = os.path.getmtime(full_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = self.collection.get(where={"path": file_path}, limit=1, include=["metadatas"])
|
res = self.collection.get(where={"path": file_path}, limit=1, include=["metadatas"])
|
||||||
if res and res["metadatas"] and res["metadatas"][0]:
|
if res and res["metadatas"] and res["metadatas"][0]:
|
||||||
if res["metadatas"][0].get("mtime") == mtime:
|
if res["metadatas"][0].get("mtime") == mtime:
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
|
with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.collection.delete(where={"path": file_path})
|
self.collection.delete(where={"path": file_path})
|
||||||
|
|
||||||
if file_path.lower().endswith(".py"):
|
if file_path.lower().endswith(".py"):
|
||||||
chunks = self._chunk_code(content, file_path)
|
chunks = self._chunk_code(content, file_path)
|
||||||
else:
|
else:
|
||||||
chunks = self._chunk_text(content)
|
chunks = self._chunk_text(content)
|
||||||
|
|
||||||
if not chunks:
|
if not chunks:
|
||||||
return
|
return
|
||||||
|
|
||||||
ids = [f"{file_path}_{i}" for i in range(len(chunks))]
|
ids = [f"{file_path}_{i}" for i in range(len(chunks))]
|
||||||
metadatas = [{"path": file_path, "chunk": i, "mtime": mtime} for i in range(len(chunks))]
|
metadatas = [{"path": file_path, "chunk": i, "mtime": mtime} for i in range(len(chunks))]
|
||||||
self.add_documents(ids, chunks, metadatas)
|
self.add_documents(ids, chunks, metadatas)
|
||||||
|
|
||||||
def _search_mcp(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
|
def _search_mcp(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
|
||||||
async def _async_search_mcp():
|
async def _async_search_mcp():
|
||||||
tool_name = self.config.vector_store.mcp_tool or "rag_search"
|
tool_name = self.config.vector_store.mcp_tool or "rag_search"
|
||||||
args = {"query": query, "top_k": top_k}
|
args = {"query": query, "top_k": top_k}
|
||||||
res_str = await mcp_client.async_dispatch(tool_name, args)
|
res_str = await mcp_client.async_dispatch(tool_name, args)
|
||||||
try:
|
try:
|
||||||
data = json.loads(res_str)
|
data = json.loads(res_str)
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
return data
|
return data
|
||||||
elif isinstance(data, dict) and "results" in data:
|
elif isinstance(data, dict) and "results" in data:
|
||||||
return data["results"]
|
return data["results"]
|
||||||
return []
|
return []
|
||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return asyncio.run(_async_search_mcp())
|
return asyncio.run(_async_search_mcp())
|
||||||
|
|
||||||
def search(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
|
def search(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
[C: tests/mock_concurrent_mma.py:main, tests/test_rag_engine.py:test_rag_engine_chroma]
|
[C: tests/mock_concurrent_mma.py:main, tests/test_rag_engine.py:test_rag_engine_chroma]
|
||||||
"""
|
"""
|
||||||
if not self.config.enabled:
|
if not self.config.enabled: return []
|
||||||
return []
|
if self.config.vector_store.provider == 'mcp': return self._search_mcp(query, top_k)
|
||||||
if self.config.vector_store.provider == 'mcp':
|
if self.collection == "mock": return []
|
||||||
return self._search_mcp(query, top_k)
|
|
||||||
if self.collection == "mock":
|
|
||||||
return []
|
|
||||||
|
|
||||||
query_embedding = self.embedding_provider.embed([query])[0]
|
query_embedding = self.embedding_provider.embed([query])[0]
|
||||||
results = self.collection.query(
|
results = self.collection.query(
|
||||||
query_embeddings=[query_embedding],
|
query_embeddings = [query_embedding],
|
||||||
n_results=top_k
|
n_results = top_k
|
||||||
)
|
)
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
if results and results["ids"] and results["ids"][0]:
|
if results and results["ids"] and results["ids"][0]:
|
||||||
for i in range(len(results["ids"][0])):
|
for i in range(len(results["ids"][0])):
|
||||||
ret.append({
|
ret.append({
|
||||||
"id": results["ids"][0][i],
|
"id": results["ids"][0][i],
|
||||||
"document": results["documents"][0][i],
|
"document": results["documents"][0][i],
|
||||||
"metadata": results["metadatas"][0][i] if results["metadatas"] else {},
|
"metadata": results["metadatas"][0][i] if results["metadatas"] else {},
|
||||||
"distance": results["distances"][0][i] if "distances" in results and results["distances"] else 0.0
|
"distance": results["distances"][0][i] if "distances" in results and results["distances"] else 0.0
|
||||||
})
|
})
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def delete_documents(self, ids: List[str]):
|
def delete_documents(self, ids: List[str]):
|
||||||
"""
|
"""
|
||||||
[C: tests/test_rag_engine.py:test_rag_engine_chroma]
|
[C: tests/test_rag_engine.py:test_rag_engine_chroma]
|
||||||
"""
|
"""
|
||||||
if not self.config.enabled or self.collection == "mock":
|
if not self.config.enabled or self.collection == "mock":
|
||||||
return
|
return
|
||||||
self.collection.delete(ids=ids)
|
self.collection.delete(ids=ids)
|
||||||
|
|
||||||
def get_all_indexed_paths(self) -> List[str]:
|
def get_all_indexed_paths(self) -> List[str]:
|
||||||
if not self.config.enabled or self.collection == "mock":
|
if not self.config.enabled or self.collection == "mock":
|
||||||
return []
|
return []
|
||||||
res = self.collection.get(include=["metadatas"])
|
res = self.collection.get(include=["metadatas"])
|
||||||
if not res or not res["metadatas"]:
|
if not res or not res["metadatas"]:
|
||||||
return []
|
return []
|
||||||
return list(set(m.get("path") for m in res["metadatas"] if m.get("path")))
|
return list(set(m.get("path") for m in res["metadatas"] if m.get("path")))
|
||||||
|
|
||||||
def delete_documents_by_path(self, file_paths: List[str]):
|
def delete_documents_by_path(self, file_paths: List[str]):
|
||||||
if not self.config.enabled or self.collection == "mock":
|
if not self.config.enabled or self.collection == "mock":
|
||||||
return
|
return
|
||||||
for path in file_paths:
|
for path in file_paths:
|
||||||
self.collection.delete(where={"path": path})
|
self.collection.delete(where={"path": path})
|
||||||
|
|||||||
+34
-35
@@ -27,27 +27,27 @@ from typing import Any, Optional, TextIO
|
|||||||
from src import paths
|
from src import paths
|
||||||
|
|
||||||
|
|
||||||
_ts: str = "" # session timestamp string e.g. "20260301_142233"
|
_ts: str = "" # session timestamp string e.g. "20260301_142233"
|
||||||
_session_id: str = "" # YYYYMMDD_HHMMSS[_Label]
|
_session_id: str = "" # YYYYMMDD_HHMMSS[_Label]
|
||||||
_session_dir: Optional[Path] = None # Path to the sub-directory for this session
|
_session_dir: Optional[Path] = None # Path to the sub-directory for this session
|
||||||
_seq: int = 0 # monotonic counter for script files this session
|
_seq: int = 0 # monotonic counter for script files this session
|
||||||
_output_seq: int = 0 # monotonic counter for output files this session
|
_output_seq: int = 0 # monotonic counter for output files this session
|
||||||
_seq_lock: threading.Lock = threading.Lock()
|
_seq_lock: threading.Lock = threading.Lock()
|
||||||
_output_seq_lock: threading.Lock = threading.Lock()
|
_output_seq_lock: threading.Lock = threading.Lock()
|
||||||
|
|
||||||
_comms_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/comms.log
|
_comms_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/comms.log
|
||||||
_tool_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/toolcalls.log
|
_tool_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/toolcalls.log
|
||||||
_api_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/apihooks.log
|
_api_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/apihooks.log
|
||||||
_cli_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/clicalls.log
|
_cli_fh: Optional[TextIO] = None # file handle: logs/sessions/<session_id>/clicalls.log
|
||||||
|
|
||||||
def _now_ts() -> str:
|
def _now_ts() -> str:
|
||||||
return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
def open_session(label: Optional[str] = None) -> None:
|
def open_session(label: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Called once at GUI startup. Creates the log directories if needed and
|
Called once at GUI startup. Creates the log directories if needed and
|
||||||
opens the log files for this session within a sub-directory.
|
opens the log files for this session within a sub-directory.
|
||||||
[C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs, tests/test_session_logger_optimization.py:test_session_directory_and_subdirectories_creation, tests/test_session_logger_reset.py:test_reset_session, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry]
|
[C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs, tests/test_session_logger_optimization.py:test_session_directory_and_subdirectories_creation, tests/test_session_logger_reset.py:test_reset_session, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry]
|
||||||
"""
|
"""
|
||||||
global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq, _output_seq
|
global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq, _output_seq
|
||||||
if _comms_fh is not None:
|
if _comms_fh is not None:
|
||||||
@@ -66,12 +66,12 @@ def open_session(label: Optional[str] = None) -> None:
|
|||||||
|
|
||||||
paths.get_scripts_dir().mkdir(parents=True, exist_ok=True)
|
paths.get_scripts_dir().mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
_seq = 0
|
_seq = 0
|
||||||
_output_seq = 0
|
_output_seq = 0
|
||||||
_comms_fh = open(_session_dir / "comms.log", "w", encoding="utf-8", buffering=1)
|
_comms_fh = open(_session_dir / "comms.log", "w", encoding="utf-8", buffering=1)
|
||||||
_tool_fh = open(_session_dir / "toolcalls.log", "w", encoding="utf-8", buffering=1)
|
_tool_fh = open(_session_dir / "toolcalls.log", "w", encoding="utf-8", buffering=1)
|
||||||
_api_fh = open(_session_dir / "apihooks.log", "w", encoding="utf-8", buffering=1)
|
_api_fh = open(_session_dir / "apihooks.log", "w", encoding="utf-8", buffering=1)
|
||||||
_cli_fh = open(_session_dir / "clicalls.log", "w", encoding="utf-8", buffering=1)
|
_cli_fh = open(_session_dir / "clicalls.log", "w", encoding="utf-8", buffering=1)
|
||||||
|
|
||||||
_tool_fh.write(f"# Tool-call log — session {_session_id}\n\n")
|
_tool_fh.write(f"# Tool-call log — session {_session_id}\n\n")
|
||||||
_tool_fh.flush()
|
_tool_fh.flush()
|
||||||
@@ -90,9 +90,8 @@ def open_session(label: Optional[str] = None) -> None:
|
|||||||
|
|
||||||
def close_session() -> None:
|
def close_session() -> None:
|
||||||
"""
|
"""
|
||||||
|
Flush and close all log files. Called on clean exit.
|
||||||
Flush and close all log files. Called on clean exit.
|
[C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:e2e_setup, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:temp_session_setup, tests/test_session_logger_reset.py:temp_logs, tests/test_session_logging.py:temp_logs]
|
||||||
[C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:e2e_setup, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:temp_session_setup, tests/test_session_logger_reset.py:temp_logs, tests/test_session_logging.py:temp_logs]
|
|
||||||
"""
|
"""
|
||||||
global _comms_fh, _tool_fh, _api_fh, _cli_fh, _session_id
|
global _comms_fh, _tool_fh, _api_fh, _cli_fh, _session_id
|
||||||
if _comms_fh is None:
|
if _comms_fh is None:
|
||||||
@@ -137,9 +136,9 @@ def log_api_hook(method: str, path: str, payload: str) -> None:
|
|||||||
|
|
||||||
def log_comms(entry: dict[str, Any]) -> None:
|
def log_comms(entry: dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Append one comms entry to the comms log file as a JSON-L line.
|
Append one comms entry to the comms log file as a JSON-L line.
|
||||||
Thread-safe (GIL + line-buffered file).
|
Thread-safe (GIL + line-buffered file).
|
||||||
[C: tests/test_logging_e2e.py:test_logging_e2e]
|
[C: tests/test_logging_e2e.py:test_logging_e2e]
|
||||||
"""
|
"""
|
||||||
if _comms_fh is None:
|
if _comms_fh is None:
|
||||||
return
|
return
|
||||||
@@ -150,9 +149,9 @@ def log_comms(entry: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optional[str]:
|
def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Append a tool-call record to the toolcalls log and write the PS1 script to
|
Append a tool-call record to the toolcalls log and write the PS1 script to
|
||||||
the session's scripts directory. Returns the path of the written script file.
|
the session's scripts directory. Returns the path of the written script file.
|
||||||
[C: tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts]
|
[C: tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts]
|
||||||
"""
|
"""
|
||||||
global _seq
|
global _seq
|
||||||
if _tool_fh is None:
|
if _tool_fh is None:
|
||||||
@@ -193,9 +192,9 @@ def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optio
|
|||||||
|
|
||||||
def log_tool_output(content: str) -> Optional[str]:
|
def log_tool_output(content: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Save tool output content to a unique file in the session's outputs directory.
|
Save tool output content to a unique file in the session's outputs directory.
|
||||||
Returns the path of the written file.
|
Returns the path of the written file.
|
||||||
[C: tests/test_session_logger_optimization.py:test_log_tool_output_returns_none_if_no_session, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs]
|
[C: tests/test_session_logger_optimization.py:test_log_tool_output_returns_none_if_no_session, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs]
|
||||||
"""
|
"""
|
||||||
global _output_seq
|
global _output_seq
|
||||||
if _session_dir is None:
|
if _session_dir is None:
|
||||||
@@ -221,14 +220,14 @@ def log_cli_call(command: str, stdin_content: Optional[str], stdout_content: Opt
|
|||||||
ts_entry = datetime.datetime.now().strftime("%H:%M:%S")
|
ts_entry = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
try:
|
try:
|
||||||
log_data = {
|
log_data = {
|
||||||
"timestamp": ts_entry,
|
"timestamp": ts_entry,
|
||||||
"command": command,
|
"command": command,
|
||||||
"stdin": stdin_content,
|
"stdin": stdin_content,
|
||||||
"stdout": stdout_content,
|
"stdout": stdout_content,
|
||||||
"stderr": stderr_content,
|
"stderr": stderr_content,
|
||||||
"latency_sec": latency
|
"latency_sec": latency
|
||||||
}
|
}
|
||||||
_cli_fh.write(json.dumps(log_data, ensure_ascii=False, default=str) + "\n")
|
_cli_fh.write(json.dumps(log_data, ensure_ascii=False, default=str) + "\n")
|
||||||
_cli_fh.flush()
|
_cli_fh.flush()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+6
-9
@@ -3,13 +3,12 @@ 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:
|
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:
|
||||||
"""
|
"""
|
||||||
Simulates a soft shadow effect by drawing multiple concentric rounded rectangles
|
Simulates a soft shadow effect by drawing multiple concentric rounded rectangles
|
||||||
with decreasing alpha values. This is a faux-shader effect using primitive batching.
|
with decreasing alpha values. This is a faux-shader effect using primitive batching.
|
||||||
"""
|
"""
|
||||||
r, g, b, a = color.x, color.y, color.z, color.w
|
r, g, b, a = color.x, color.y, color.z, color.w
|
||||||
steps = int(shadow_size)
|
steps = int(shadow_size)
|
||||||
if steps <= 0:
|
if steps <= 0: return
|
||||||
return
|
|
||||||
|
|
||||||
alpha_step = a / steps
|
alpha_step = a / steps
|
||||||
|
|
||||||
@@ -17,12 +16,10 @@ def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: im
|
|||||||
current_alpha = a - (i * alpha_step)
|
current_alpha = a - (i * alpha_step)
|
||||||
# Apply an easing function (e.g., cubic) for a smoother shadow falloff
|
# Apply an easing function (e.g., cubic) for a smoother shadow falloff
|
||||||
current_alpha = current_alpha * (1.0 - (i / steps)**2)
|
current_alpha = current_alpha * (1.0 - (i / steps)**2)
|
||||||
|
|
||||||
if current_alpha <= 0.01:
|
if current_alpha <= 0.01:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
expand = float(i)
|
|
||||||
|
|
||||||
|
expand = float(i)
|
||||||
c_min = imgui.ImVec2(p_min.x - expand, p_min.y - expand)
|
c_min = imgui.ImVec2(p_min.x - expand, p_min.y - expand)
|
||||||
c_max = imgui.ImVec2(p_max.x + expand, p_max.y + expand)
|
c_max = imgui.ImVec2(p_max.x + expand, p_max.y + expand)
|
||||||
|
|
||||||
@@ -35,4 +32,4 @@ def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: im
|
|||||||
rounding + expand if rounding > 0 else 0.0,
|
rounding + expand if rounding > 0 else 0.0,
|
||||||
flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
|
flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
|
||||||
thickness=1.0
|
thickness=1.0
|
||||||
)
|
)
|
||||||
|
|||||||
+14
-14
@@ -55,24 +55,24 @@ def _build_subprocess_env() -> dict[str, str]:
|
|||||||
|
|
||||||
def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str:
|
def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str:
|
||||||
"""
|
"""
|
||||||
Run a PowerShell script with working directory set to base_dir.
|
Run a PowerShell script with working directory set to base_dir.
|
||||||
Returns a string combining stdout, stderr, and exit code.
|
Returns a string combining stdout, stderr, and exit code.
|
||||||
Environment is configured via mcp_env.toml (project root).
|
Environment is configured via mcp_env.toml (project root).
|
||||||
If qa_callback is provided and the command fails or has stderr,
|
If qa_callback is provided and the command fails or has stderr,
|
||||||
the callback is called with the stderr content and its result is appended.
|
the callback is called with the stderr content and its result is appended.
|
||||||
If patch_callback is provided, it receives (error, file_context) and returns patch text.
|
If patch_callback is provided, it receives (error, file_context) and returns patch text.
|
||||||
[C: tests/test_tier4_interceptor.py:test_run_powershell_no_qa_callback_on_success, tests/test_tier4_interceptor.py:test_run_powershell_optional_qa_callback, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_failure, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_stderr_only]
|
[C: tests/test_tier4_interceptor.py:test_run_powershell_no_qa_callback_on_success, tests/test_tier4_interceptor.py:test_run_powershell_optional_qa_callback, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_failure, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_stderr_only]
|
||||||
"""
|
"""
|
||||||
safe_dir: str = str(base_dir).replace("'", "''")
|
safe_dir: str = str(base_dir).replace("'", "''")
|
||||||
full_script: str = f"Set-Location -LiteralPath '{safe_dir}'\n{script}"
|
full_script: str = f"Set-Location -LiteralPath '{safe_dir}'\n{script}"
|
||||||
exe: Optional[str] = next((x for x in ["powershell.exe", "pwsh.exe", "powershell", "pwsh"] if shutil.which(x)), None)
|
exe: Optional[str] = next((x for x in ["powershell.exe", "pwsh.exe", "powershell", "pwsh"] if shutil.which(x)), None)
|
||||||
if not exe: return "ERROR: Neither powershell nor pwsh found in PATH"
|
if not exe: return "ERROR: Neither powershell nor pwsh found in PATH"
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
[exe, "-NoProfile", "-NonInteractive", "-Command", full_script],
|
[exe, "-NoProfile", "-NonInteractive", "-Command", full_script],
|
||||||
stdin=subprocess.DEVNULL,
|
stdin = subprocess.DEVNULL,
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
stdout = subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||||
cwd=base_dir, env=_build_subprocess_env(),
|
cwd = base_dir, env=_build_subprocess_env(),
|
||||||
)
|
)
|
||||||
stdout, stderr = process.communicate(timeout=TIMEOUT_SECONDS)
|
stdout, stderr = process.communicate(timeout=TIMEOUT_SECONDS)
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
@@ -99,4 +99,4 @@ def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[s
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'process' in locals() and process:
|
if 'process' in locals() and process:
|
||||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)], capture_output=True)
|
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)], capture_output=True)
|
||||||
return f"ERROR: {e}"
|
return f"ERROR: {e}"
|
||||||
|
|||||||
+40
-56
@@ -1,23 +1,12 @@
|
|||||||
# summarize.py
|
# summarize.py
|
||||||
"""
|
"""
|
||||||
Note(Gemini):
|
|
||||||
Local heuristic summariser. Doesn't use any AI or network.
|
|
||||||
Uses Python's AST to reliably pull out classes, methods, and functions.
|
|
||||||
Regex is used for TOML and Markdown.
|
|
||||||
|
|
||||||
The rationale here is simple: giving the AI the *structure* of a codebase is 90%
|
|
||||||
as good as giving it the full source, but costs 1% of the tokens.
|
|
||||||
If it needs the full source of a file after reading the summary, it can just call read_file.
|
|
||||||
"""
|
|
||||||
# summarize.py
|
|
||||||
"""
|
|
||||||
Local symbolic summariser — no AI calls, no network.
|
Local symbolic summariser — no AI calls, no network.
|
||||||
|
|
||||||
For each file, extracts structural information:
|
For each file, extracts structural information:
|
||||||
.py : imports, classes (with methods), top-level functions, global constants
|
.py : imports, classes (with methods), top-level functions, global constants
|
||||||
.toml : top-level table keys + array lengths
|
.toml : top-level table keys + array lengths
|
||||||
.md : headings (h1-h3)
|
.md : headings (h1-h3)
|
||||||
other : line count + first 8 lines as preview
|
other : line count + first 8 lines as preview
|
||||||
|
|
||||||
Returns a compact markdown string per file, suitable for use as a low-token
|
Returns a compact markdown string per file, suitable for use as a low-token
|
||||||
context block that replaces full file contents in the initial <context> send.
|
context block that replaces full file contents in the initial <context> send.
|
||||||
@@ -28,6 +17,8 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
|
|
||||||
|
from src import ai_client
|
||||||
|
|
||||||
from src.summary_cache import SummaryCache, get_file_hash
|
from src.summary_cache import SummaryCache, get_file_hash
|
||||||
|
|
||||||
|
|
||||||
@@ -37,9 +28,9 @@ _summary_cache = SummaryCache()
|
|||||||
# ------------------------------------------------------------------ per-type extractors
|
# ------------------------------------------------------------------ per-type extractors
|
||||||
|
|
||||||
def _summarise_python(path: Path, content: str) -> str:
|
def _summarise_python(path: Path, content: str) -> str:
|
||||||
lines = content.splitlines()
|
lines = content.splitlines()
|
||||||
line_count = len(lines)
|
line_count = len(lines)
|
||||||
parts = [f"**Python** — {line_count} lines"]
|
parts = [f"**Python** — {line_count} lines"]
|
||||||
try:
|
try:
|
||||||
tree = ast.parse(content.lstrip(chr(0xFEFF)), filename=str(path))
|
tree = ast.parse(content.lstrip(chr(0xFEFF)), filename=str(path))
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
@@ -73,31 +64,28 @@ def _summarise_python(path: Path, content: str) -> str:
|
|||||||
n.name for n in ast.iter_child_nodes(node)
|
n.name for n in ast.iter_child_nodes(node)
|
||||||
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||||
]
|
]
|
||||||
if methods:
|
if methods: parts.append(f"class {node.name}: {', '.join(methods)}")
|
||||||
parts.append(f"class {node.name}: {', '.join(methods)}")
|
else: parts.append(f"class {node.name}")
|
||||||
else:
|
|
||||||
parts.append(f"class {node.name}")
|
|
||||||
top_fns = [
|
top_fns = [
|
||||||
node.name for node in ast.iter_child_nodes(tree)
|
node.name for node in ast.iter_child_nodes(tree)
|
||||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||||
]
|
]
|
||||||
if top_fns:
|
if top_fns: parts.append(f"functions: {', '.join(top_fns)}")
|
||||||
parts.append(f"functions: {', '.join(top_fns)}")
|
|
||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
def _summarise_toml(path: Path, content: str) -> str:
|
def _summarise_toml(path: Path, content: str) -> str:
|
||||||
lines = content.splitlines()
|
lines = content.splitlines()
|
||||||
line_count = len(lines)
|
line_count = len(lines)
|
||||||
parts = [f"**TOML** — {line_count} lines"]
|
parts = [f"**TOML** — {line_count} lines"]
|
||||||
table_pat = re.compile(r"^\s*\[{1,2}([^\[\]]+)\]{1,2}")
|
table_pat = re.compile(r"^\s*\[{1,2}([^\[\]]+)\]{1,2}")
|
||||||
tables = []
|
tables = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
m = table_pat.match(line)
|
m = table_pat.match(line)
|
||||||
if m:
|
if m:
|
||||||
tables.append(m.group(1).strip())
|
tables.append(m.group(1).strip())
|
||||||
if tables:
|
if tables:
|
||||||
parts.append(f"tables: {', '.join(tables)}")
|
parts.append(f"tables: {', '.join(tables)}")
|
||||||
kv_pat = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)\s*=")
|
kv_pat = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)\s*=")
|
||||||
in_table = False
|
in_table = False
|
||||||
top_keys = []
|
top_keys = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@@ -113,15 +101,15 @@ def _summarise_toml(path: Path, content: str) -> str:
|
|||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
def _summarise_markdown(path: Path, content: str) -> str:
|
def _summarise_markdown(path: Path, content: str) -> str:
|
||||||
lines = content.splitlines()
|
lines = content.splitlines()
|
||||||
line_count = len(lines)
|
line_count = len(lines)
|
||||||
parts = [f"**Markdown** — {line_count} lines"]
|
parts = [f"**Markdown** — {line_count} lines"]
|
||||||
headings = []
|
headings = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
m = re.match(r"^(#{1,3})\s+(.+)", line)
|
m = re.match(r"^(#{1,3})\s+(.+)", line)
|
||||||
if m:
|
if m:
|
||||||
level = len(m.group(1))
|
level = len(m.group(1))
|
||||||
text = m.group(2).strip()
|
text = m.group(2).strip()
|
||||||
indent = " " * (level - 1)
|
indent = " " * (level - 1)
|
||||||
headings.append(f"{indent}{text}")
|
headings.append(f"{indent}{text}")
|
||||||
if headings:
|
if headings:
|
||||||
@@ -129,10 +117,10 @@ def _summarise_markdown(path: Path, content: str) -> str:
|
|||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
def _summarise_generic(path: Path, content: str) -> str:
|
def _summarise_generic(path: Path, content: str) -> str:
|
||||||
lines = content.splitlines()
|
lines = content.splitlines()
|
||||||
line_count = len(lines)
|
line_count = len(lines)
|
||||||
suffix = path.suffix.lstrip(".").upper() or "TEXT"
|
suffix = path.suffix.lstrip(".").upper() or "TEXT"
|
||||||
parts = [f"**{suffix}** — {line_count} lines"]
|
parts = [f"**{suffix}** — {line_count} lines"]
|
||||||
|
|
||||||
# Heuristic for C-style languages
|
# Heuristic for C-style languages
|
||||||
important_lines = []
|
important_lines = []
|
||||||
@@ -168,24 +156,20 @@ _SUMMARISERS: dict[str, Callable[[Path, str], str]] = {
|
|||||||
|
|
||||||
def summarise_file(path: Path, content: str) -> str:
|
def summarise_file(path: Path, content: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
Return a compact markdown summary string for a single file.
|
||||||
|
`content` is the already-read file text (or an error string).
|
||||||
Return a compact markdown summary string for a single file.
|
[C: tests/test_subagent_summarization.py:test_summarise_file_integration]
|
||||||
`content` is the already-read file text (or an error string).
|
|
||||||
[C: tests/test_subagent_summarization.py:test_summarise_file_integration]
|
|
||||||
"""
|
"""
|
||||||
content_hash = get_file_hash(content)
|
content_hash = get_file_hash(content)
|
||||||
cached = _summary_cache.get_summary(str(path), content_hash)
|
cached = _summary_cache.get_summary(str(path), content_hash)
|
||||||
if cached:
|
if cached: return cached
|
||||||
return cached
|
|
||||||
suffix = path.suffix.lower() if hasattr(path, "suffix") else ""
|
suffix = path.suffix.lower() if hasattr(path, "suffix") else ""
|
||||||
fn = _SUMMARISERS.get(suffix, _summarise_generic)
|
fn = _SUMMARISERS.get(suffix, _summarise_generic)
|
||||||
try:
|
try:
|
||||||
heuristic_outline = fn(path, content)
|
heuristic_outline = fn(path, content)
|
||||||
# Smart AI Summarization
|
# Smart AI Summarization
|
||||||
is_code = suffix in [".py", ".ps1", ".js", ".ts", ".cpp", ".c", ".h", ".cs", ".go", ".rs", ".lua"]
|
is_code = suffix in [".py", ".ps1", ".js", ".ts", ".cpp", ".c", ".h", ".cs", ".go", ".rs", ".lua"]
|
||||||
try:
|
try:
|
||||||
from src import ai_client
|
|
||||||
smart_summary = ai_client.run_subagent_summarization(
|
smart_summary = ai_client.run_subagent_summarization(
|
||||||
file_path=str(path),
|
file_path=str(path),
|
||||||
content=content[:10000],
|
content=content[:10000],
|
||||||
@@ -205,31 +189,31 @@ def summarise_file(path: Path, content: str) -> str:
|
|||||||
|
|
||||||
def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Given a list of file_item dicts (as returned by aggregate.build_file_items),
|
Given a list of file_item dicts (as returned by aggregate.build_file_items),
|
||||||
return a parallel list of dicts with an added `summary` key.
|
return a parallel list of dicts with an added `summary` key.
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
for item in file_items:
|
for item in file_items:
|
||||||
path = item.get("path")
|
path = item.get("path")
|
||||||
content = item.get("content", "")
|
content = item.get("content", "")
|
||||||
error = item.get("error", False)
|
error = item.get("error", False)
|
||||||
if error or path is None:
|
if error or path is None:
|
||||||
summary = "_Error reading file_"
|
summary = "_Error reading file_"
|
||||||
else:
|
else:
|
||||||
p = Path(path) if not isinstance(path, Path) else path
|
p = Path(path) if not isinstance(path, Path) else path
|
||||||
summary = summarise_file(p, content)
|
summary = summarise_file(p, content)
|
||||||
result.append({**item, "summary": summary})
|
result.append({**item, "summary": summary})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def build_summary_markdown(file_items: list[dict[str, Any]]) -> str:
|
def build_summary_markdown(file_items: list[dict[str, Any]]) -> str:
|
||||||
"""
|
"""
|
||||||
Build a compact markdown string of file summaries, suitable for the
|
Build a compact markdown string of file summaries, suitable for the
|
||||||
initial <context> block instead of full file contents.
|
initial <context> block instead of full file contents.
|
||||||
"""
|
"""
|
||||||
summarised = summarise_items(file_items)
|
summarised = summarise_items(file_items)
|
||||||
parts = []
|
parts = []
|
||||||
for item in summarised:
|
for item in summarised:
|
||||||
path = item.get("path") or item.get("entry", "unknown")
|
path = item.get("path") or item.get("entry", "unknown")
|
||||||
summary = item.get("summary", "")
|
summary = item.get("summary", "")
|
||||||
parts.append(f"### `{path}`\n\n{summary}")
|
parts.append(f"### `{path}`\n\n{summary}")
|
||||||
return "\n\n---\n\n".join(parts)
|
return "\n\n---\n\n".join(parts)
|
||||||
|
|||||||
+13
-20
@@ -7,18 +7,15 @@ from typing import Optional, Dict
|
|||||||
|
|
||||||
def get_file_hash(content: str) -> str:
|
def get_file_hash(content: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
Returns SHA256 hash of the content.
|
||||||
Returns SHA256 hash of the content.
|
[C: tests/test_summary_cache.py:test_get_file_hash, tests/test_summary_cache.py:test_summary_cache]
|
||||||
[C: tests/test_summary_cache.py:test_get_file_hash, tests/test_summary_cache.py:test_summary_cache]
|
|
||||||
"""
|
"""
|
||||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
class SummaryCache:
|
class SummaryCache:
|
||||||
"""
|
"""
|
||||||
|
A hash-based cache for file summaries to avoid redundant processing.
|
||||||
|
Invalidates when content hash changes.
|
||||||
A hash-based cache for file summaries to avoid redundant processing.
|
|
||||||
Invalidates when content hash changes.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, cache_file: Optional[str] = None, max_entries: int = 1000):
|
def __init__(self, cache_file: Optional[str] = None, max_entries: int = 1000):
|
||||||
if cache_file:
|
if cache_file:
|
||||||
@@ -32,9 +29,8 @@ class SummaryCache:
|
|||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
Loads cache from disk.
|
||||||
Loads cache from disk.
|
[C: src/tool_presets.py:ToolPresetManager._read_raw, src/workspace_manager.py:WorkspaceManager._load_file, tests/test_gui_phase3.py:test_create_track, tests/test_history_management.py:test_save_separation, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry]
|
||||||
[C: src/tool_presets.py:ToolPresetManager._read_raw, src/workspace_manager.py:WorkspaceManager._load_file, tests/test_gui_phase3.py:test_create_track, tests/test_history_management.py:test_save_separation, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry]
|
|
||||||
"""
|
"""
|
||||||
if self.cache_file.exists():
|
if self.cache_file.exists():
|
||||||
try:
|
try:
|
||||||
@@ -54,9 +50,8 @@ class SummaryCache:
|
|||||||
|
|
||||||
def get_summary(self, file_path: str, content_hash: str) -> Optional[str]:
|
def get_summary(self, file_path: str, content_hash: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
|
Returns cached summary if hash matches, otherwise None.
|
||||||
Returns cached summary if hash matches, otherwise None.
|
[C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru]
|
||||||
[C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru]
|
|
||||||
"""
|
"""
|
||||||
entry = self.cache.get(file_path)
|
entry = self.cache.get(file_path)
|
||||||
if entry and entry.get("hash") == content_hash:
|
if entry and entry.get("hash") == content_hash:
|
||||||
@@ -68,9 +63,8 @@ class SummaryCache:
|
|||||||
|
|
||||||
def set_summary(self, file_path: str, content_hash: str, summary: str) -> None:
|
def set_summary(self, file_path: str, content_hash: str, summary: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
Stores summary in cache and saves to disk.
|
||||||
Stores summary in cache and saves to disk.
|
[C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru]
|
||||||
[C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru]
|
|
||||||
"""
|
"""
|
||||||
if file_path in self.cache:
|
if file_path in self.cache:
|
||||||
self.cache.pop(file_path)
|
self.cache.pop(file_path)
|
||||||
@@ -87,9 +81,8 @@ class SummaryCache:
|
|||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
Clears the cache both in-memory and on disk.
|
||||||
Clears the cache both in-memory and on disk.
|
[C: tests/conftest.py:reset_ai_client]
|
||||||
[C: tests/conftest.py:reset_ai_client]
|
|
||||||
"""
|
"""
|
||||||
self.cache.clear()
|
self.cache.clear()
|
||||||
if self.cache_file.exists():
|
if self.cache_file.exists():
|
||||||
@@ -109,4 +102,4 @@ class SummaryCache:
|
|||||||
return {
|
return {
|
||||||
"entries": len(self.cache),
|
"entries": len(self.cache),
|
||||||
"size_bytes": size_bytes
|
"size_bytes": size_bytes
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-67
@@ -14,7 +14,7 @@ from contextlib import nullcontext
|
|||||||
from imgui_bundle import imgui, hello_imgui
|
from imgui_bundle import imgui, hello_imgui
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import src.theme_nerv
|
from src import theme_nerv
|
||||||
|
|
||||||
from src import imgui_scopes as imscope
|
from src import imgui_scopes as imscope
|
||||||
from src.theme_nerv import DATA_GREEN
|
from src.theme_nerv import DATA_GREEN
|
||||||
@@ -60,18 +60,18 @@ def _build_semantic_colour_dict(theme: ThemeFile) -> dict[str, tuple[float, floa
|
|||||||
|
|
||||||
_BUILTIN_PALETTES: dict[str, dict[int, tuple]] = {
|
_BUILTIN_PALETTES: dict[str, dict[int, tuple]] = {
|
||||||
"ImGui Dark": {},
|
"ImGui Dark": {},
|
||||||
"NERV": {},
|
"NERV": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
_TOML_PALETTES: dict[str, ThemeFile] = {}
|
_TOML_PALETTES: dict[str, ThemeFile] = {}
|
||||||
_TOML_COLOUR_CACHE: dict[str, dict[int, tuple[float, float, float, float]]] = {}
|
_TOML_COLOUR_CACHE: dict[str, dict[int, tuple[float, float, float, float]]] = {}
|
||||||
_TOML_SEMANTIC_CACHE: dict[str, dict[str, tuple[float, float, float, float]]] = {}
|
_TOML_SEMANTIC_CACHE: dict[str, dict[str, tuple[float, float, float, float]]] = {}
|
||||||
|
|
||||||
_current_palette: str = "10x Dark"
|
_current_palette: str = "10x Dark"
|
||||||
_current_font_path: str = "fonts/Inter-Regular.ttf"
|
_current_font_path: str = "fonts/Inter-Regular.ttf"
|
||||||
_current_font_size: float = 16.0
|
_current_font_size: float = 16.0
|
||||||
_current_scale: float = 1.0
|
_current_scale: float = 1.0
|
||||||
_transparency: float = 1.0
|
_transparency: float = 1.0
|
||||||
_child_transparency: float = 1.0
|
_child_transparency: float = 1.0
|
||||||
|
|
||||||
# Per-palette tone mapping: { "Palette Name": value }
|
# Per-palette tone mapping: { "Palette Name": value }
|
||||||
@@ -106,8 +106,8 @@ def _tone_map(rgb: tuple[float, float, float, float], palette: str) -> tuple[flo
|
|||||||
r = max(0, r)**(1.0/g); g_val = max(0, g_val)**(1.0/g); bl = max(0, bl)**(1.0/g)
|
r = max(0, r)**(1.0/g); g_val = max(0, g_val)**(1.0/g); bl = max(0, bl)**(1.0/g)
|
||||||
return (max(0.0, min(1.0, r)), max(0.0, min(1.0, g_val)), max(0.0, min(1.0, bl)), a)
|
return (max(0.0, min(1.0, r)), max(0.0, min(1.0, g_val)), max(0.0, min(1.0, bl)), a)
|
||||||
|
|
||||||
_crt_filter = CRTFilter()
|
_crt_filter = CRTFilter()
|
||||||
_alert_pulsing = AlertPulsing()
|
_alert_pulsing = AlertPulsing()
|
||||||
_status_flicker = StatusFlicker()
|
_status_flicker = StatusFlicker()
|
||||||
|
|
||||||
# ------------------------------------------------------------------ public API
|
# ------------------------------------------------------------------ public API
|
||||||
@@ -160,7 +160,7 @@ def get_color(name: str, alpha: float = 1.0) -> imgui.ImVec4:
|
|||||||
if palette_name in _TOML_SEMANTIC_CACHE:
|
if palette_name in _TOML_SEMANTIC_CACHE:
|
||||||
d = _TOML_SEMANTIC_CACHE[palette_name]
|
d = _TOML_SEMANTIC_CACHE[palette_name]
|
||||||
if name in d:
|
if name in d:
|
||||||
rgba = list(d[name])
|
rgba = list(d[name])
|
||||||
rgba[3] = alpha
|
rgba[3] = alpha
|
||||||
return imgui.ImVec4(*_tone_map(tuple(rgba), palette_name))
|
return imgui.ImVec4(*_tone_map(tuple(rgba), palette_name))
|
||||||
|
|
||||||
@@ -172,25 +172,25 @@ def get_color(name: str, alpha: float = 1.0) -> imgui.ImVec4:
|
|||||||
|
|
||||||
# 2. Hardcoded fallbacks if not in TOML (matches ThemePalette defaults)
|
# 2. Hardcoded fallbacks if not in TOML (matches ThemePalette defaults)
|
||||||
fallbacks = {
|
fallbacks = {
|
||||||
"text": (200, 200, 200),
|
"text": (200, 200, 200),
|
||||||
"text_disabled": (130, 130, 130),
|
"text_disabled": (130, 130, 130),
|
||||||
"status_success": (80, 255, 80),
|
"status_success": (80, 255, 80),
|
||||||
"status_warning": (255, 152, 48),
|
"status_warning": (255, 152, 48),
|
||||||
"status_error": (255, 72, 64),
|
"status_error": (255, 72, 64),
|
||||||
"status_info": (0, 255, 255),
|
"status_info": (0, 255, 255),
|
||||||
"bubble_user": (30, 45, 75),
|
"bubble_user": (30, 45, 75),
|
||||||
"bubble_ai": (35, 65, 45),
|
"bubble_ai": (35, 65, 45),
|
||||||
"bubble_vendor": (65, 55, 30),
|
"bubble_vendor": (65, 55, 30),
|
||||||
"bubble_system": (25, 25, 25),
|
"bubble_system": (25, 25, 25),
|
||||||
"slice_manual": (255, 165, 0),
|
"slice_manual": (255, 165, 0),
|
||||||
"slice_auto": (0, 255, 0),
|
"slice_auto": (0, 255, 0),
|
||||||
"slice_selection": (100, 100, 255),
|
"slice_selection": (100, 100, 255),
|
||||||
"diff_added": (51, 230, 51),
|
"diff_added": (51, 230, 51),
|
||||||
"diff_removed": (230, 51, 51),
|
"diff_removed": (230, 51, 51),
|
||||||
"diff_header": (77, 178, 255),
|
"diff_header": (77, 178, 255),
|
||||||
"table_header_text": (255, 255, 255),
|
"table_header_text": (255, 255, 255),
|
||||||
"table_row_bg": (0, 0, 0),
|
"table_row_bg": (0, 0, 0),
|
||||||
"table_row_bg_alt": (10, 10, 10),
|
"table_row_bg_alt": (10, 10, 10),
|
||||||
}
|
}
|
||||||
if name in fallbacks:
|
if name in fallbacks:
|
||||||
rgb = fallbacks[name]
|
rgb = fallbacks[name]
|
||||||
@@ -202,8 +202,8 @@ def get_color(name: str, alpha: float = 1.0) -> imgui.ImVec4:
|
|||||||
def get_role_tint(role: str) -> imgui.ImVec4:
|
def get_role_tint(role: str) -> imgui.ImVec4:
|
||||||
"""Returns a subtle background tint color based on the message role."""
|
"""Returns a subtle background tint color based on the message role."""
|
||||||
mapping = {
|
mapping = {
|
||||||
"User": "bubble_user",
|
"User": "bubble_user",
|
||||||
"AI": "bubble_ai",
|
"AI": "bubble_ai",
|
||||||
"Vendor API": "bubble_vendor",
|
"Vendor API": "bubble_vendor",
|
||||||
}
|
}
|
||||||
return get_color(mapping.get(role, "bubble_system"), alpha=0.6)
|
return get_color(mapping.get(role, "bubble_system"), alpha=0.6)
|
||||||
@@ -213,7 +213,6 @@ def apply(palette_name: str) -> None:
|
|||||||
global _current_palette
|
global _current_palette
|
||||||
_current_palette = palette_name
|
_current_palette = palette_name
|
||||||
if palette_name == 'NERV':
|
if palette_name == 'NERV':
|
||||||
from src import theme_nerv
|
|
||||||
theme_nerv.apply_nerv()
|
theme_nerv.apply_nerv()
|
||||||
apply_syntax_palette(get_syntax_palette_for_theme(palette_name))
|
apply_syntax_palette(get_syntax_palette_for_theme(palette_name))
|
||||||
return
|
return
|
||||||
@@ -228,7 +227,7 @@ def apply(palette_name: str) -> None:
|
|||||||
elif palette_name in _TOML_PALETTES:
|
elif palette_name in _TOML_PALETTES:
|
||||||
colours = _TOML_COLOUR_CACHE.get(palette_name, {})
|
colours = _TOML_COLOUR_CACHE.get(palette_name, {})
|
||||||
if not colours:
|
if not colours:
|
||||||
theme = _TOML_PALETTES[palette_name]
|
theme = _TOML_PALETTES[palette_name]
|
||||||
colours = _build_imgui_colour_dict(theme)
|
colours = _build_imgui_colour_dict(theme)
|
||||||
_TOML_COLOUR_CACHE[palette_name] = colours
|
_TOML_COLOUR_CACHE[palette_name] = colours
|
||||||
imgui.style_colors_dark()
|
imgui.style_colors_dark()
|
||||||
@@ -243,33 +242,33 @@ def apply(palette_name: str) -> None:
|
|||||||
|
|
||||||
# 2. Professional tweaks
|
# 2. Professional tweaks
|
||||||
style = imgui.get_style()
|
style = imgui.get_style()
|
||||||
style.window_rounding = 6.0
|
style.window_rounding = 6.0
|
||||||
style.child_rounding = 4.0
|
style.child_rounding = 4.0
|
||||||
style.frame_rounding = 4.0
|
style.frame_rounding = 4.0
|
||||||
style.popup_rounding = 4.0
|
style.popup_rounding = 4.0
|
||||||
style.scrollbar_rounding = 12.0
|
style.scrollbar_rounding = 12.0
|
||||||
style.grab_rounding = 4.0
|
style.grab_rounding = 4.0
|
||||||
style.tab_rounding = 4.0
|
style.tab_rounding = 4.0
|
||||||
style.window_border_size = 1.0
|
style.window_border_size = 1.0
|
||||||
style.frame_border_size = 1.0
|
style.frame_border_size = 1.0
|
||||||
style.popup_border_size = 1.0
|
style.popup_border_size = 1.0
|
||||||
|
|
||||||
win_bg = style.color_(imgui.Col_.window_bg)
|
win_bg = style.color_(imgui.Col_.window_bg)
|
||||||
win_bg.w = _transparency
|
win_bg.w = _transparency
|
||||||
style.set_color_(imgui.Col_.window_bg, win_bg)
|
style.set_color_(imgui.Col_.window_bg, win_bg)
|
||||||
|
|
||||||
for col_idx in [imgui.Col_.child_bg, imgui.Col_.frame_bg, imgui.Col_.popup_bg]:
|
for col_idx in [imgui.Col_.child_bg, imgui.Col_.frame_bg, imgui.Col_.popup_bg]:
|
||||||
c = style.color_(col_idx)
|
c = style.color_(col_idx)
|
||||||
c.w = _child_transparency
|
c.w = _child_transparency
|
||||||
style.set_color_(col_idx, c)
|
style.set_color_(col_idx, c)
|
||||||
|
|
||||||
style.window_padding = imgui.ImVec2(8.0, 8.0)
|
style.window_padding = imgui.ImVec2(8.0, 8.0)
|
||||||
style.frame_padding = imgui.ImVec2(8.0, 4.0)
|
style.frame_padding = imgui.ImVec2(8.0, 4.0)
|
||||||
style.item_spacing = imgui.ImVec2(8.0, 4.0)
|
style.item_spacing = imgui.ImVec2(8.0, 4.0)
|
||||||
style.item_inner_spacing = imgui.ImVec2(4.0, 4.0)
|
style.item_inner_spacing = imgui.ImVec2(4.0, 4.0)
|
||||||
style.scrollbar_size = 14.0
|
style.scrollbar_size = 14.0
|
||||||
style.anti_aliased_lines = True
|
style.anti_aliased_lines = True
|
||||||
style.anti_aliased_fill = True
|
style.anti_aliased_fill = True
|
||||||
style.anti_aliased_lines_use_tex = True
|
style.anti_aliased_lines_use_tex = True
|
||||||
|
|
||||||
# 3. Sync syntax palette and clear markdown render cache
|
# 3. Sync syntax palette and clear markdown render cache
|
||||||
@@ -299,18 +298,18 @@ def get_palette_names() -> list[str]:
|
|||||||
def save_to_config(config: dict) -> None:
|
def save_to_config(config: dict) -> None:
|
||||||
"""Persist theme settings into the config dict."""
|
"""Persist theme settings into the config dict."""
|
||||||
config.setdefault("theme", {})
|
config.setdefault("theme", {})
|
||||||
config["theme"]["palette"] = _current_palette
|
config["theme"]["palette"] = _current_palette
|
||||||
config["theme"]["font_path"] = _current_font_path
|
config["theme"]["font_path"] = _current_font_path
|
||||||
config["theme"]["font_size"] = _current_font_size
|
config["theme"]["font_size"] = _current_font_size
|
||||||
config["theme"]["scale"] = _current_scale
|
config["theme"]["scale"] = _current_scale
|
||||||
config["theme"]["transparency"] = _transparency
|
config["theme"]["transparency"] = _transparency
|
||||||
config["theme"]["child_transparency"] = _child_transparency
|
config["theme"]["child_transparency"] = _child_transparency
|
||||||
tm = {}
|
tm = {}
|
||||||
for p in set(list(_brightness.keys()) + list(_contrast.keys()) + list(_gamma.keys())):
|
for p in set(list(_brightness.keys()) + list(_contrast.keys()) + list(_gamma.keys())):
|
||||||
tm[p] = {
|
tm[p] = {
|
||||||
"brightness": _brightness.get(p, 1.0),
|
"brightness": _brightness.get(p, 1.0),
|
||||||
"contrast": _contrast.get(p, 1.0),
|
"contrast": _contrast.get(p, 1.0),
|
||||||
"gamma": _gamma.get(p, 1.0)
|
"gamma": _gamma.get(p, 1.0)
|
||||||
}
|
}
|
||||||
config["theme"]["tone_mapping"] = tm
|
config["theme"]["tone_mapping"] = tm
|
||||||
|
|
||||||
@@ -322,15 +321,15 @@ def load_from_config(config: dict) -> None:
|
|||||||
_current_palette = t.get("palette", "10x Dark")
|
_current_palette = t.get("palette", "10x Dark")
|
||||||
if _current_palette in ("", "DPG Default"):
|
if _current_palette in ("", "DPG Default"):
|
||||||
_current_palette = "10x Dark"
|
_current_palette = "10x Dark"
|
||||||
_current_font_path = t.get("font_path", "fonts/Inter-Regular.ttf")
|
_current_font_path = t.get("font_path", "fonts/Inter-Regular.ttf")
|
||||||
_current_font_size = float(t.get("font_size", 16.0))
|
_current_font_size = float(t.get("font_size", 16.0))
|
||||||
_current_scale = float(t.get("scale", 1.0))
|
_current_scale = float(t.get("scale", 1.0))
|
||||||
_transparency = float(t.get("transparency", 1.0))
|
_transparency = float(t.get("transparency", 1.0))
|
||||||
_child_transparency = float(t.get("child_transparency", 1.0))
|
_child_transparency = float(t.get("child_transparency", 1.0))
|
||||||
tm = t.get("tone_mapping", {})
|
tm = t.get("tone_mapping", {})
|
||||||
_brightness = {p: float(v.get("brightness", 1.0)) for p, v in tm.items()}
|
_brightness = {p: float(v.get("brightness", 1.0)) for p, v in tm.items()}
|
||||||
_contrast = {p: float(v.get("contrast", 1.0)) for p, v in tm.items()}
|
_contrast = {p: float(v.get("contrast", 1.0)) for p, v in tm.items()}
|
||||||
_gamma = {p: float(v.get("gamma", 1.0)) for p, v in tm.items()}
|
_gamma = {p: float(v.get("gamma", 1.0)) for p, v in tm.items()}
|
||||||
|
|
||||||
# ------------------------------------------------------------------ external themes
|
# ------------------------------------------------------------------ external themes
|
||||||
|
|
||||||
@@ -341,8 +340,8 @@ def load_themes_from_disk() -> None:
|
|||||||
loaded: dict[str, ThemeFile] = {}
|
loaded: dict[str, ThemeFile] = {}
|
||||||
if themes_dir.exists() and themes_dir.is_dir():
|
if themes_dir.exists() and themes_dir.is_dir():
|
||||||
loaded.update(load_themes_from_dir(themes_dir, scope="global"))
|
loaded.update(load_themes_from_dir(themes_dir, scope="global"))
|
||||||
_TOML_PALETTES = loaded
|
_TOML_PALETTES = loaded
|
||||||
_TOML_COLOUR_CACHE = {name: _build_imgui_colour_dict(t) for name, t in loaded.items()}
|
_TOML_COLOUR_CACHE = {name: _build_imgui_colour_dict(t) for name, t in loaded.items()}
|
||||||
_TOML_SEMANTIC_CACHE = {name: _build_semantic_colour_dict(t) for name, t in loaded.items()}
|
_TOML_SEMANTIC_CACHE = {name: _build_semantic_colour_dict(t) for name, t in loaded.items()}
|
||||||
|
|
||||||
def get_syntax_palette_for_theme(theme_name: str) -> str:
|
def get_syntax_palette_for_theme(theme_name: str) -> str:
|
||||||
|
|||||||
+97
-97
@@ -13,86 +13,86 @@ VALID_SYNTAX_PALETTES: tuple[str, ...] = ("dark", "light", "mariana", "retro_blu
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ThemePalette:
|
class ThemePalette:
|
||||||
window_bg: tuple[int, int, int] = (0, 0, 0)
|
window_bg: tuple[int, int, int] = (0, 0, 0)
|
||||||
child_bg: tuple[int, int, int] = (0, 0, 0)
|
child_bg: tuple[int, int, int] = (0, 0, 0)
|
||||||
popup_bg: tuple[int, int, int] = (0, 0, 0)
|
popup_bg: tuple[int, int, int] = (0, 0, 0)
|
||||||
border: tuple[int, int, int] = (60, 60, 60)
|
border: tuple[int, int, int] = (60, 60, 60)
|
||||||
border_shadow: tuple[int, int, int] = (0, 0, 0)
|
border_shadow: tuple[int, int, int] = (0, 0, 0)
|
||||||
frame_bg: tuple[int, int, int] = (45, 45, 45)
|
frame_bg: tuple[int, int, int] = (45, 45, 45)
|
||||||
frame_bg_hovered: tuple[int, int, int] = (60, 60, 60)
|
frame_bg_hovered: tuple[int, int, int] = (60, 60, 60)
|
||||||
frame_bg_active: tuple[int, int, int] = (75, 75, 75)
|
frame_bg_active: tuple[int, int, int] = (75, 75, 75)
|
||||||
title_bg: tuple[int, int, int] = (40, 40, 40)
|
title_bg: tuple[int, int, int] = (40, 40, 40)
|
||||||
title_bg_active: tuple[int, int, int] = (60, 45, 15)
|
title_bg_active: tuple[int, int, int] = (60, 45, 15)
|
||||||
title_bg_collapsed: tuple[int, int, int] = (30, 30, 30)
|
title_bg_collapsed: tuple[int, int, int] = (30, 30, 30)
|
||||||
menu_bar_bg: tuple[int, int, int] = (35, 35, 35)
|
menu_bar_bg: tuple[int, int, int] = (35, 35, 35)
|
||||||
scrollbar_bg: tuple[int, int, int] = (30, 30, 30)
|
scrollbar_bg: tuple[int, int, int] = (30, 30, 30)
|
||||||
scrollbar_grab: tuple[int, int, int] = (80, 80, 80)
|
scrollbar_grab: tuple[int, int, int] = (80, 80, 80)
|
||||||
scrollbar_grab_hovered: tuple[int, int, int] = (100, 100, 100)
|
scrollbar_grab_hovered: tuple[int, int, int] = (100, 100, 100)
|
||||||
scrollbar_grab_active: tuple[int, int, int] = (120, 120, 120)
|
scrollbar_grab_active: tuple[int, int, int] = (120, 120, 120)
|
||||||
check_mark: tuple[int, int, int] = (200, 200, 200)
|
check_mark: tuple[int, int, int] = (200, 200, 200)
|
||||||
slider_grab: tuple[int, int, int] = (60, 60, 60)
|
slider_grab: tuple[int, int, int] = (60, 60, 60)
|
||||||
slider_grab_active: tuple[int, int, int] = (100, 100, 100)
|
slider_grab_active: tuple[int, int, int] = (100, 100, 100)
|
||||||
button: tuple[int, int, int] = (60, 60, 60)
|
button: tuple[int, int, int] = (60, 60, 60)
|
||||||
button_hovered: tuple[int, int, int] = (100, 100, 100)
|
button_hovered: tuple[int, int, int] = (100, 100, 100)
|
||||||
button_active: tuple[int, int, int] = (120, 120, 120)
|
button_active: tuple[int, int, int] = (120, 120, 120)
|
||||||
header: tuple[int, int, int] = (60, 60, 60)
|
header: tuple[int, int, int] = (60, 60, 60)
|
||||||
header_hovered: tuple[int, int, int] = (100, 100, 100)
|
header_hovered: tuple[int, int, int] = (100, 100, 100)
|
||||||
header_active: tuple[int, int, int] = (120, 120, 120)
|
header_active: tuple[int, int, int] = (120, 120, 120)
|
||||||
separator: tuple[int, int, int] = (60, 60, 60)
|
separator: tuple[int, int, int] = (60, 60, 60)
|
||||||
separator_hovered: tuple[int, int, int] = (100, 100, 100)
|
separator_hovered: tuple[int, int, int] = (100, 100, 100)
|
||||||
separator_active: tuple[int, int, int] = (200, 200, 200)
|
separator_active: tuple[int, int, int] = (200, 200, 200)
|
||||||
resize_grip: tuple[int, int, int] = (60, 60, 60)
|
resize_grip: tuple[int, int, int] = (60, 60, 60)
|
||||||
resize_grip_hovered: tuple[int, int, int] = (100, 100, 100)
|
resize_grip_hovered: tuple[int, int, int] = (100, 100, 100)
|
||||||
resize_grip_active: tuple[int, int, int] = (200, 200, 200)
|
resize_grip_active: tuple[int, int, int] = (200, 200, 200)
|
||||||
tab: tuple[int, int, int] = (60, 60, 60)
|
tab: tuple[int, int, int] = (60, 60, 60)
|
||||||
tab_hovered: tuple[int, int, int] = (100, 100, 100)
|
tab_hovered: tuple[int, int, int] = (100, 100, 100)
|
||||||
tab_selected: tuple[int, int, int] = (100, 100, 100)
|
tab_selected: tuple[int, int, int] = (100, 100, 100)
|
||||||
tab_dimmed: tuple[int, int, int] = (60, 60, 60)
|
tab_dimmed: tuple[int, int, int] = (60, 60, 60)
|
||||||
tab_dimmed_selected: tuple[int, int, int] = (100, 100, 100)
|
tab_dimmed_selected: tuple[int, int, int] = (100, 100, 100)
|
||||||
docking_preview: tuple[int, int, int] = (100, 100, 100)
|
docking_preview: tuple[int, int, int] = (100, 100, 100)
|
||||||
docking_empty_bg: tuple[int, int, int] = (20, 20, 20)
|
docking_empty_bg: tuple[int, int, int] = (20, 20, 20)
|
||||||
text: tuple[int, int, int] = (200, 200, 200)
|
text: tuple[int, int, int] = (200, 200, 200)
|
||||||
text_disabled: tuple[int, int, int] = (130, 130, 130)
|
text_disabled: tuple[int, int, int] = (130, 130, 130)
|
||||||
text_selected_bg: tuple[int, int, int] = (60, 100, 150)
|
text_selected_bg: tuple[int, int, int] = (60, 100, 150)
|
||||||
table_header_bg: tuple[int, int, int] = (55, 55, 55)
|
table_header_bg: tuple[int, int, int] = (55, 55, 55)
|
||||||
table_border_strong: tuple[int, int, int] = (60, 60, 60)
|
table_border_strong: tuple[int, int, int] = (60, 60, 60)
|
||||||
table_border_light: tuple[int, int, int] = (40, 40, 40)
|
table_border_light: tuple[int, int, int] = (40, 40, 40)
|
||||||
table_row_bg: tuple[int, int, int] = (0, 0, 0)
|
table_row_bg: tuple[int, int, int] = (0, 0, 0)
|
||||||
table_row_bg_alt: tuple[int, int, int] = (10, 10, 10)
|
table_row_bg_alt: tuple[int, int, int] = (10, 10, 10)
|
||||||
nav_cursor: tuple[int, int, int] = (100, 100, 100)
|
nav_cursor: tuple[int, int, int] = (100, 100, 100)
|
||||||
nav_windowing_dim_bg: tuple[int, int, int] = (20, 20, 20)
|
nav_windowing_dim_bg: tuple[int, int, int] = (20, 20, 20)
|
||||||
nav_windowing_highlight: tuple[int, int, int] = (200, 200, 200)
|
nav_windowing_highlight: tuple[int, int, int] = (200, 200, 200)
|
||||||
modal_window_dim_bg: tuple[int, int, int] = (10, 10, 10)
|
modal_window_dim_bg: tuple[int, int, int] = (10, 10, 10)
|
||||||
plot_lines: tuple[int, int, int] = (100, 100, 100)
|
plot_lines: tuple[int, int, int] = (100, 100, 100)
|
||||||
plot_lines_hovered: tuple[int, int, int] = (200, 100, 100)
|
plot_lines_hovered: tuple[int, int, int] = (200, 100, 100)
|
||||||
plot_histogram: tuple[int, int, int] = (100, 100, 100)
|
plot_histogram: tuple[int, int, int] = (100, 100, 100)
|
||||||
plot_histogram_hovered: tuple[int, int, int] = (200, 100, 100)
|
plot_histogram_hovered: tuple[int, int, int] = (200, 100, 100)
|
||||||
drag_drop_target: tuple[int, int, int] = (200, 200, 0)
|
drag_drop_target: tuple[int, int, int] = (200, 200, 0)
|
||||||
drag_drop_target_bg: tuple[int, int, int] = (0, 0, 0)
|
drag_drop_target_bg: tuple[int, int, int] = (0, 0, 0)
|
||||||
input_text_cursor: tuple[int, int, int] = (200, 200, 200)
|
input_text_cursor: tuple[int, int, int] = (200, 200, 200)
|
||||||
tab_dimmed_selected_overline: tuple[int, int, int] = (100, 100, 100)
|
tab_dimmed_selected_overline: tuple[int, int, int] = (100, 100, 100)
|
||||||
tab_selected_overline: tuple[int, int, int] = (100, 100, 100)
|
tab_selected_overline: tuple[int, int, int] = (100, 100, 100)
|
||||||
text_link: tuple[int, int, int] = (60, 100, 150)
|
text_link: tuple[int, int, int] = (60, 100, 150)
|
||||||
tree_lines: tuple[int, int, int] = (60, 60, 60)
|
tree_lines: tuple[int, int, int] = (60, 60, 60)
|
||||||
unsaved_marker: tuple[int, int, int] = (200, 200, 200)
|
unsaved_marker: tuple[int, int, int] = (200, 200, 200)
|
||||||
|
|
||||||
# Semantic colors
|
# Semantic colors
|
||||||
status_success: tuple[int, int, int] = (80, 255, 80)
|
status_success: tuple[int, int, int] = (80, 255, 80)
|
||||||
status_warning: tuple[int, int, int] = (255, 152, 48)
|
status_warning: tuple[int, int, int] = (255, 152, 48)
|
||||||
status_error: tuple[int, int, int] = (255, 72, 64)
|
status_error: tuple[int, int, int] = (255, 72, 64)
|
||||||
status_info: tuple[int, int, int] = (0, 255, 255)
|
status_info: tuple[int, int, int] = (0, 255, 255)
|
||||||
bubble_user: tuple[int, int, int] = (30, 45, 75)
|
bubble_user: tuple[int, int, int] = (30, 45, 75)
|
||||||
bubble_ai: tuple[int, int, int] = (35, 65, 45)
|
bubble_ai: tuple[int, int, int] = (35, 65, 45)
|
||||||
bubble_vendor: tuple[int, int, int] = (65, 55, 30)
|
bubble_vendor: tuple[int, int, int] = (65, 55, 30)
|
||||||
bubble_system: tuple[int, int, int] = (25, 25, 25)
|
bubble_system: tuple[int, int, int] = (25, 25, 25)
|
||||||
slice_manual: tuple[int, int, int] = (255, 165, 0)
|
slice_manual: tuple[int, int, int] = (255, 165, 0)
|
||||||
slice_auto: tuple[int, int, int] = (0, 255, 0)
|
slice_auto: tuple[int, int, int] = (0, 255, 0)
|
||||||
slice_selection: tuple[int, int, int] = (100, 100, 255)
|
slice_selection: tuple[int, int, int] = (100, 100, 255)
|
||||||
|
|
||||||
# Diff colors
|
# Diff colors
|
||||||
diff_added: tuple[int, int, int] = (51, 230, 51)
|
diff_added: tuple[int, int, int] = (51, 230, 51)
|
||||||
diff_removed: tuple[int, int, int] = (230, 51, 51)
|
diff_removed: tuple[int, int, int] = (230, 51, 51)
|
||||||
diff_header: tuple[int, int, int] = (77, 178, 255)
|
diff_header: tuple[int, int, int] = (77, 178, 255)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> ThemePalette:
|
def from_dict(cls, data: dict[str, Any]) -> ThemePalette:
|
||||||
@@ -108,12 +108,12 @@ class ThemePalette:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ThemeFile:
|
class ThemeFile:
|
||||||
name: str
|
name: str
|
||||||
palette: ThemePalette
|
palette: ThemePalette
|
||||||
syntax_palette: str
|
syntax_palette: str
|
||||||
source_path: Path
|
source_path: Path
|
||||||
scope: str
|
scope: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.syntax_palette not in VALID_SYNTAX_PALETTES:
|
if self.syntax_palette not in VALID_SYNTAX_PALETTES:
|
||||||
@@ -124,12 +124,12 @@ class ThemeFile:
|
|||||||
|
|
||||||
def with_scope(self, scope: str) -> ThemeFile:
|
def with_scope(self, scope: str) -> ThemeFile:
|
||||||
return ThemeFile(
|
return ThemeFile(
|
||||||
name=self.name,
|
name = self.name,
|
||||||
palette=self.palette,
|
palette = self.palette,
|
||||||
syntax_palette=self.syntax_palette,
|
syntax_palette = self.syntax_palette,
|
||||||
source_path=self.source_path,
|
source_path = self.source_path,
|
||||||
scope=scope,
|
scope = scope,
|
||||||
description=self.description,
|
description = self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
@@ -152,12 +152,12 @@ class ThemeFile:
|
|||||||
f"must be one of {VALID_SYNTAX_PALETTES}"
|
f"must be one of {VALID_SYNTAX_PALETTES}"
|
||||||
)
|
)
|
||||||
return cls(
|
return cls(
|
||||||
name=name,
|
name = name,
|
||||||
palette=ThemePalette.from_dict(data["colors"]),
|
palette = ThemePalette.from_dict(data["colors"]),
|
||||||
syntax_palette=syntax_palette,
|
syntax_palette = syntax_palette,
|
||||||
source_path=source_path,
|
source_path = source_path,
|
||||||
scope=scope,
|
scope = scope,
|
||||||
description=str(data.get("description", "")),
|
description = str(data.get("description", "")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ def load_theme_file(path: Path, scope: str) -> ThemeFile:
|
|||||||
raise ValueError(f"failed to parse theme TOML {path}: {e}") from e
|
raise ValueError(f"failed to parse theme TOML {path}: {e}") from e
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise ValueError(f"theme TOML {path} must be a top-level table")
|
raise ValueError(f"theme TOML {path} must be a top-level table")
|
||||||
name = data.get("name", path.stem)
|
name = data.get("name", path.stem)
|
||||||
theme = ThemeFile.from_dict(name, data, source_path=path, scope=scope)
|
theme = ThemeFile.from_dict(name, data, source_path=path, scope=scope)
|
||||||
return theme
|
return theme
|
||||||
|
|
||||||
@@ -204,14 +204,14 @@ def load_themes_from_toml(path: Path, scope: str) -> dict[str, ThemeFile]:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"warning: failed to parse {path}: {e}", file=sys.stderr)
|
print(f"warning: failed to parse {path}: {e}", file=sys.stderr)
|
||||||
return out
|
return out
|
||||||
if not isinstance(data, dict):
|
|
||||||
return out
|
if not isinstance(data, dict): return out
|
||||||
|
|
||||||
themes_sec = data.get("themes", {})
|
themes_sec = data.get("themes", {})
|
||||||
if not isinstance(themes_sec, dict):
|
if not isinstance(themes_sec, dict): return out
|
||||||
return out
|
|
||||||
for name, theme_data in themes_sec.items():
|
for name, theme_data in themes_sec.items():
|
||||||
if not isinstance(theme_data, dict):
|
if not isinstance(theme_data, dict): continue
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
theme = ThemeFile.from_dict(name, theme_data, source_path=path, scope=scope)
|
theme = ThemeFile.from_dict(name, theme_data, source_path=path, scope=scope)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
+63
-64
@@ -6,84 +6,83 @@ def _c(r: int, g: int, b: int, a: int = 255) -> tuple[float, float, float, float
|
|||||||
return (r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
return (r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
||||||
|
|
||||||
NERV_ORANGE = _c(255, 152, 48)
|
NERV_ORANGE = _c(255, 152, 48)
|
||||||
DATA_GREEN = _c(80, 255, 80)
|
DATA_GREEN = _c(80, 255, 80)
|
||||||
WIRE_CYAN = _c(32, 240, 255)
|
WIRE_CYAN = _c(32, 240, 255)
|
||||||
ALERT_RED = _c(255, 72, 64)
|
ALERT_RED = _c(255, 72, 64)
|
||||||
STEEL = _c(224, 224, 216)
|
STEEL = _c(224, 224, 216)
|
||||||
BLACK = _c(0, 0, 0)
|
BLACK = _c(0, 0, 0)
|
||||||
|
|
||||||
NERV_PALETTE = {
|
NERV_PALETTE = {
|
||||||
imgui.Col_.text: STEEL,
|
imgui.Col_.text: STEEL,
|
||||||
imgui.Col_.window_bg: BLACK,
|
imgui.Col_.window_bg: BLACK,
|
||||||
imgui.Col_.child_bg: BLACK,
|
imgui.Col_.child_bg: BLACK,
|
||||||
imgui.Col_.popup_bg: BLACK,
|
imgui.Col_.popup_bg: BLACK,
|
||||||
imgui.Col_.border: NERV_ORANGE,
|
imgui.Col_.border: NERV_ORANGE,
|
||||||
imgui.Col_.border_shadow: _c(0, 0, 0, 0),
|
imgui.Col_.border_shadow: _c(0, 0, 0, 0),
|
||||||
imgui.Col_.frame_bg: BLACK,
|
imgui.Col_.frame_bg: BLACK,
|
||||||
imgui.Col_.frame_bg_hovered: _c(255, 152, 48, 40),
|
imgui.Col_.frame_bg_hovered: _c(255, 152, 48, 40),
|
||||||
imgui.Col_.frame_bg_active: _c(255, 152, 48, 80),
|
imgui.Col_.frame_bg_active: _c(255, 152, 48, 80),
|
||||||
imgui.Col_.title_bg: BLACK,
|
imgui.Col_.title_bg: BLACK,
|
||||||
imgui.Col_.title_bg_active: BLACK,
|
imgui.Col_.title_bg_active: BLACK,
|
||||||
imgui.Col_.title_bg_collapsed: BLACK,
|
imgui.Col_.title_bg_collapsed: BLACK,
|
||||||
imgui.Col_.menu_bar_bg: BLACK,
|
imgui.Col_.menu_bar_bg: BLACK,
|
||||||
imgui.Col_.scrollbar_bg: BLACK,
|
imgui.Col_.scrollbar_bg: BLACK,
|
||||||
imgui.Col_.scrollbar_grab: NERV_ORANGE,
|
imgui.Col_.scrollbar_grab: NERV_ORANGE,
|
||||||
imgui.Col_.scrollbar_grab_hovered: STEEL,
|
imgui.Col_.scrollbar_grab_hovered: STEEL,
|
||||||
imgui.Col_.scrollbar_grab_active: WIRE_CYAN,
|
imgui.Col_.scrollbar_grab_active: WIRE_CYAN,
|
||||||
imgui.Col_.check_mark: DATA_GREEN,
|
imgui.Col_.check_mark: DATA_GREEN,
|
||||||
imgui.Col_.slider_grab: WIRE_CYAN,
|
imgui.Col_.slider_grab: WIRE_CYAN,
|
||||||
imgui.Col_.slider_grab_active: DATA_GREEN,
|
imgui.Col_.slider_grab_active: DATA_GREEN,
|
||||||
imgui.Col_.button: BLACK,
|
imgui.Col_.button: BLACK,
|
||||||
imgui.Col_.button_hovered: _c(255, 152, 48, 80),
|
imgui.Col_.button_hovered: _c(255, 152, 48, 80),
|
||||||
imgui.Col_.button_active: _c(255, 152, 48, 120),
|
imgui.Col_.button_active: _c(255, 152, 48, 120),
|
||||||
imgui.Col_.header: _c(255, 152, 48, 60),
|
imgui.Col_.header: _c(255, 152, 48, 60),
|
||||||
imgui.Col_.header_hovered: _c(255, 152, 48, 100),
|
imgui.Col_.header_hovered: _c(255, 152, 48, 100),
|
||||||
imgui.Col_.header_active: _c(255, 152, 48, 140),
|
imgui.Col_.header_active: _c(255, 152, 48, 140),
|
||||||
imgui.Col_.separator: STEEL,
|
imgui.Col_.separator: STEEL,
|
||||||
imgui.Col_.separator_hovered: WIRE_CYAN,
|
imgui.Col_.separator_hovered: WIRE_CYAN,
|
||||||
imgui.Col_.separator_active: DATA_GREEN,
|
imgui.Col_.separator_active: DATA_GREEN,
|
||||||
imgui.Col_.resize_grip: NERV_ORANGE,
|
imgui.Col_.resize_grip: NERV_ORANGE,
|
||||||
imgui.Col_.resize_grip_hovered: STEEL,
|
imgui.Col_.resize_grip_hovered: STEEL,
|
||||||
imgui.Col_.resize_grip_active: WIRE_CYAN,
|
imgui.Col_.resize_grip_active: WIRE_CYAN,
|
||||||
imgui.Col_.tab: BLACK,
|
imgui.Col_.tab: BLACK,
|
||||||
imgui.Col_.tab_hovered: _c(255, 152, 48, 100),
|
imgui.Col_.tab_hovered: _c(255, 152, 48, 100),
|
||||||
imgui.Col_.tab_selected: _c(255, 152, 48, 120),
|
imgui.Col_.tab_selected: _c(255, 152, 48, 120),
|
||||||
imgui.Col_.tab_dimmed: BLACK,
|
imgui.Col_.tab_dimmed: BLACK,
|
||||||
imgui.Col_.tab_dimmed_selected: _c(255, 152, 48, 80),
|
imgui.Col_.tab_dimmed_selected: _c(255, 152, 48, 80),
|
||||||
imgui.Col_.plot_lines: WIRE_CYAN,
|
imgui.Col_.plot_lines: WIRE_CYAN,
|
||||||
imgui.Col_.plot_lines_hovered: DATA_GREEN,
|
imgui.Col_.plot_lines_hovered: DATA_GREEN,
|
||||||
imgui.Col_.plot_histogram: DATA_GREEN,
|
imgui.Col_.plot_histogram: DATA_GREEN,
|
||||||
imgui.Col_.plot_histogram_hovered: WIRE_CYAN,
|
imgui.Col_.plot_histogram_hovered: WIRE_CYAN,
|
||||||
imgui.Col_.text_selected_bg: _c(255, 152, 48, 100),
|
imgui.Col_.text_selected_bg: _c(255, 152, 48, 100),
|
||||||
imgui.Col_.drag_drop_target: DATA_GREEN,
|
imgui.Col_.drag_drop_target: DATA_GREEN,
|
||||||
imgui.Col_.nav_cursor: NERV_ORANGE,
|
imgui.Col_.nav_cursor: NERV_ORANGE,
|
||||||
imgui.Col_.nav_windowing_highlight: DATA_GREEN,
|
imgui.Col_.nav_windowing_highlight: DATA_GREEN,
|
||||||
imgui.Col_.nav_windowing_dim_bg: _c(0, 0, 0, 150),
|
imgui.Col_.nav_windowing_dim_bg: _c(0, 0, 0, 150),
|
||||||
imgui.Col_.modal_window_dim_bg: _c(0, 0, 0, 150),
|
imgui.Col_.modal_window_dim_bg: _c(0, 0, 0, 150),
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply_nerv() -> None:
|
def apply_nerv() -> None:
|
||||||
"""
|
"""
|
||||||
|
Apply NERV theme with hard edges and specific palette.
|
||||||
Apply NERV theme with hard edges and specific palette.
|
[C: tests/test_theme_nerv.py:test_apply_nerv_sets_rounding_and_colors]
|
||||||
[C: tests/test_theme_nerv.py:test_apply_nerv_sets_rounding_and_colors]
|
|
||||||
"""
|
"""
|
||||||
style = imgui.get_style()
|
style = imgui.get_style()
|
||||||
for col_enum, rgba in NERV_PALETTE.items():
|
for col_enum, rgba in NERV_PALETTE.items():
|
||||||
style.set_color_(col_enum, imgui.ImVec4(*rgba))
|
style.set_color_(col_enum, imgui.ImVec4(*rgba))
|
||||||
|
|
||||||
# Hard Edges
|
# Hard Edges
|
||||||
style.window_rounding = 0.0
|
style.window_rounding = 0.0
|
||||||
style.child_rounding = 0.0
|
style.child_rounding = 0.0
|
||||||
style.frame_rounding = 0.0
|
style.frame_rounding = 0.0
|
||||||
style.popup_rounding = 0.0
|
style.popup_rounding = 0.0
|
||||||
style.scrollbar_rounding = 0.0
|
style.scrollbar_rounding = 0.0
|
||||||
style.grab_rounding = 0.0
|
style.grab_rounding = 0.0
|
||||||
style.tab_rounding = 0.0
|
style.tab_rounding = 0.0
|
||||||
|
|
||||||
# Border sizes
|
# Border sizes
|
||||||
style.window_border_size = 1.0
|
style.window_border_size = 1.0
|
||||||
style.frame_border_size = 1.0
|
style.frame_border_size = 1.0
|
||||||
style.popup_border_size = 1.0
|
style.popup_border_size = 1.0
|
||||||
style.child_border_size = 1.0
|
style.child_border_size = 1.0
|
||||||
style.tab_border_size = 1.0
|
style.tab_border_size = 1.0
|
||||||
|
|||||||
+13
-15
@@ -11,20 +11,19 @@ class CRTFilter:
|
|||||||
|
|
||||||
def render(self, width: float, height: float):
|
def render(self, width: float, height: float):
|
||||||
"""
|
"""
|
||||||
[C: 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: 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 self.enabled:
|
if not self.enabled: return
|
||||||
return
|
|
||||||
draw_list = imgui.get_foreground_draw_list()
|
draw_list = imgui.get_foreground_draw_list()
|
||||||
|
|
||||||
# 1. Enhanced Scanlines (Horizontal)
|
# 1. Enhanced Scanlines (Horizontal)
|
||||||
# Vary thickness and alpha for a more "analog" feel
|
# Vary thickness and alpha for a more "analog" feel
|
||||||
for y in range(0, int(height), 2):
|
for y in range(0, int(height), 2):
|
||||||
# Thicker/Darker every 4 pixels
|
# Thicker/Darker every 4 pixels
|
||||||
is_major = (y % 4 == 0)
|
is_major = (y % 4 == 0)
|
||||||
alpha = 0.08 if is_major else 0.04
|
alpha = 0.08 if is_major else 0.04
|
||||||
thickness = 1.2 if is_major else 0.8
|
thickness = 1.2 if is_major else 0.8
|
||||||
s_color = imgui.get_color_u32((0.0, 0.0, 0.0, alpha))
|
s_color = imgui.get_color_u32((0.0, 0.0, 0.0, alpha))
|
||||||
draw_list.add_line((0.0, float(y)), (float(width), float(y)), s_color, thickness)
|
draw_list.add_line((0.0, float(y)), (float(width), float(y)), s_color, thickness)
|
||||||
|
|
||||||
# 2. Shadow Mask (Vertical)
|
# 2. Shadow Mask (Vertical)
|
||||||
@@ -38,12 +37,11 @@ class CRTFilter:
|
|||||||
v_steps = 20
|
v_steps = 20
|
||||||
for i in range(v_steps):
|
for i in range(v_steps):
|
||||||
# Exponential alpha for smoother falloff
|
# Exponential alpha for smoother falloff
|
||||||
alpha = (i / v_steps) ** 3.0 * 0.25
|
alpha = (i / v_steps) ** 3.0 * 0.25
|
||||||
v_color = imgui.get_color_u32((0.0, 0.0, 0.0, alpha))
|
v_color = imgui.get_color_u32((0.0, 0.0, 0.0, alpha))
|
||||||
|
|
||||||
# Inset and rounding grow to simulate tube curvature
|
# Inset and rounding grow to simulate tube curvature
|
||||||
inset = (v_steps - i) * 4.5
|
inset = (v_steps - i) * 4.5
|
||||||
rounding = 60.0 + (v_steps - i) * 8.0
|
rounding = 60.0 + (v_steps - i) * 8.0
|
||||||
thickness = 15.0
|
thickness = 15.0
|
||||||
|
|
||||||
if width > inset * 2.0 and height > inset * 2.0:
|
if width > inset * 2.0 and height > inset * 2.0:
|
||||||
@@ -70,7 +68,7 @@ class StatusFlicker:
|
|||||||
def get_alpha(self) -> float:
|
def get_alpha(self) -> float:
|
||||||
# Modulate between 0.7 and 1.0 using sin wave
|
# Modulate between 0.7 and 1.0 using sin wave
|
||||||
"""
|
"""
|
||||||
[C: tests/test_theme_nerv_fx.py:TestThemeNervFx.test_status_flicker_get_alpha]
|
[C: tests/test_theme_nerv_fx.py:TestThemeNervFx.test_status_flicker_get_alpha]
|
||||||
"""
|
"""
|
||||||
return 0.85 + 0.15 * math.sin(time.time() * 20.0)
|
return 0.85 + 0.15 * math.sin(time.time() * 20.0)
|
||||||
|
|
||||||
@@ -80,13 +78,13 @@ class AlertPulsing:
|
|||||||
|
|
||||||
def update(self, status: str):
|
def update(self, status: str):
|
||||||
"""
|
"""
|
||||||
[C: tests/test_spawn_interception_v2.py:MockDialog.wait, tests/test_theme_nerv_alert.py:test_alert_pulsing_update, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_update]
|
[C: tests/test_spawn_interception_v2.py:MockDialog.wait, tests/test_theme_nerv_alert.py:test_alert_pulsing_update, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_update]
|
||||||
"""
|
"""
|
||||||
self.active = status.lower().startswith("error")
|
self.active = status.lower().startswith("error")
|
||||||
|
|
||||||
def render(self, width: float, height: float):
|
def render(self, width: float, height: float):
|
||||||
"""
|
"""
|
||||||
[C: 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: 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 self.active:
|
if not self.active:
|
||||||
return
|
return
|
||||||
@@ -96,4 +94,4 @@ class AlertPulsing:
|
|||||||
# multiply by (0.2 - 0.05) = 0.15 and add 0.05
|
# multiply by (0.2 - 0.05) = 0.15 and add 0.05
|
||||||
alpha = 0.05 + 0.15 * ((math.sin(time.time() * 4.0) + 1.0) / 2.0)
|
alpha = 0.05 + 0.15 * ((math.sin(time.time() * 4.0) + 1.0) / 2.0)
|
||||||
color = imgui.get_color_u32((1.0, 0.0, 0.0, alpha))
|
color = imgui.get_color_u32((1.0, 0.0, 0.0, alpha))
|
||||||
draw_list.add_rect((0.0, 0.0), (width, height), color, 0.0, 0, 10.0)
|
draw_list.add_rect((0.0, 0.0), (width, height), color, 0.0, 0, 10.0)
|
||||||
|
|||||||
+7
-12
@@ -7,18 +7,15 @@ from src.models import ThinkingSegment
|
|||||||
|
|
||||||
def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
||||||
"""
|
"""
|
||||||
|
Parses thinking segments from text and returns (segments, response_content).
|
||||||
|
Support extraction of thinking traces from <thinking>...</thinking>, <thought>...</thought>,
|
||||||
Parses thinking segments from text and returns (segments, response_content).
|
and blocks prefixed with Thinking:.
|
||||||
Support extraction of thinking traces from <thinking>...</thinking>, <thought>...</thought>,
|
[C: tests/test_thinking_trace.py:test_parse_empty_response, tests/test_thinking_trace.py:test_parse_multiple_markers, tests/test_thinking_trace.py:test_parse_no_thinking, tests/test_thinking_trace.py:test_parse_text_thinking_prefix, tests/test_thinking_trace.py:test_parse_thinking_with_empty_response, tests/test_thinking_trace.py:test_parse_xml_thinking_tag, tests/test_thinking_trace.py:test_parse_xml_thought_tag]
|
||||||
and blocks prefixed with Thinking:.
|
|
||||||
[C: tests/test_thinking_trace.py:test_parse_empty_response, tests/test_thinking_trace.py:test_parse_multiple_markers, tests/test_thinking_trace.py:test_parse_no_thinking, tests/test_thinking_trace.py:test_parse_text_thinking_prefix, tests/test_thinking_trace.py:test_parse_thinking_with_empty_response, tests/test_thinking_trace.py:test_parse_xml_thinking_tag, tests/test_thinking_trace.py:test_parse_xml_thought_tag]
|
|
||||||
"""
|
"""
|
||||||
segments = []
|
segments = []
|
||||||
|
|
||||||
# 1. Extract <thinking> and <thought> tags
|
# 1. Extract <thinking> and <thought> tags
|
||||||
current_text = text
|
current_text = text
|
||||||
|
|
||||||
# Combined pattern for tags
|
# Combined pattern for tags
|
||||||
tag_pattern = re.compile(r'<(thinking|thought)>(.*?)</\1>', re.DOTALL | re.IGNORECASE)
|
tag_pattern = re.compile(r'<(thinking|thought)>(.*?)</\1>', re.DOTALL | re.IGNORECASE)
|
||||||
|
|
||||||
@@ -26,7 +23,7 @@ def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
|||||||
found_segments = []
|
found_segments = []
|
||||||
|
|
||||||
def replace_func(match):
|
def replace_func(match):
|
||||||
marker = match.group(1).lower()
|
marker = match.group(1).lower()
|
||||||
content = match.group(2).strip()
|
content = match.group(2).strip()
|
||||||
found_segments.append(ThinkingSegment(content=content, marker=marker))
|
found_segments.append(ThinkingSegment(content=content, marker=marker))
|
||||||
return ""
|
return ""
|
||||||
@@ -46,8 +43,7 @@ def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
|||||||
|
|
||||||
def replace_func(match):
|
def replace_func(match):
|
||||||
content = match.group(1).strip()
|
content = match.group(1).strip()
|
||||||
if content:
|
if content: found_segments.append(ThinkingSegment(content=content, marker="Thinking:"))
|
||||||
found_segments.append(ThinkingSegment(content=content, marker="Thinking:"))
|
|
||||||
return "\n\n"
|
return "\n\n"
|
||||||
|
|
||||||
res = thinking_colon_pattern.sub(replace_func, txt)
|
res = thinking_colon_pattern.sub(replace_func, txt)
|
||||||
@@ -55,5 +51,4 @@ def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
|||||||
|
|
||||||
colon_segments, final_remaining = extract_colon_blocks(remaining)
|
colon_segments, final_remaining = extract_colon_blocks(remaining)
|
||||||
segments.extend(colon_segments)
|
segments.extend(colon_segments)
|
||||||
|
return segments, final_remaining.strip()
|
||||||
return segments, final_remaining.strip()
|
|
||||||
|
|||||||
+9
-15
@@ -6,7 +6,7 @@ from src.models import Tool, ToolPreset, BiasProfile
|
|||||||
class ToolBiasEngine:
|
class ToolBiasEngine:
|
||||||
def apply_semantic_nudges(self, tool_definitions: List[Dict[str, Any]], preset: ToolPreset) -> List[Dict[str, Any]]:
|
def apply_semantic_nudges(self, tool_definitions: List[Dict[str, Any]], preset: ToolPreset) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_tool_bias.py:test_apply_semantic_nudges, tests/test_tool_bias.py:test_parameter_bias_nudging]
|
[C: tests/test_tool_bias.py:test_apply_semantic_nudges, tests/test_tool_bias.py:test_parameter_bias_nudging]
|
||||||
"""
|
"""
|
||||||
weight_map = {
|
weight_map = {
|
||||||
5: "[HIGH PRIORITY] ",
|
5: "[HIGH PRIORITY] ",
|
||||||
@@ -42,7 +42,7 @@ class ToolBiasEngine:
|
|||||||
|
|
||||||
def generate_tooling_strategy(self, preset: ToolPreset, global_bias: BiasProfile) -> str:
|
def generate_tooling_strategy(self, preset: ToolPreset, global_bias: BiasProfile) -> str:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_tool_bias.py:test_generate_tooling_strategy]
|
[C: tests/test_tool_bias.py:test_generate_tooling_strategy]
|
||||||
"""
|
"""
|
||||||
lines = ["### Tooling Strategy"]
|
lines = ["### Tooling Strategy"]
|
||||||
|
|
||||||
@@ -51,23 +51,17 @@ class ToolBiasEngine:
|
|||||||
for cat_tools in preset.categories.values():
|
for cat_tools in preset.categories.values():
|
||||||
for t in cat_tools:
|
for t in cat_tools:
|
||||||
if not isinstance(t, Tool): continue
|
if not isinstance(t, Tool): continue
|
||||||
if t.weight >= 5:
|
if t.weight >= 5: preferred.append(f"{t.name} [HIGH PRIORITY]")
|
||||||
preferred.append(f"{t.name} [HIGH PRIORITY]")
|
elif t.weight == 4: preferred.append(f"{t.name} [PREFERRED]")
|
||||||
elif t.weight == 4:
|
elif t.weight == 2: low_priority.append(f"{t.name} [NOT RECOMMENDED]")
|
||||||
preferred.append(f"{t.name} [PREFERRED]")
|
elif t.weight <= 1: low_priority.append(f"{t.name} [LOW PRIORITY]")
|
||||||
elif t.weight == 2:
|
|
||||||
low_priority.append(f"{t.name} [NOT RECOMMENDED]")
|
|
||||||
elif t.weight <= 1:
|
|
||||||
low_priority.append(f"{t.name} [LOW PRIORITY]")
|
|
||||||
|
|
||||||
if preferred:
|
if preferred: lines.append(f"Preferred tools: {', '.join(preferred)}.")
|
||||||
lines.append(f"Preferred tools: {', '.join(preferred)}.")
|
if low_priority: lines.append(f"Low-priority tools: {', '.join(low_priority)}.")
|
||||||
if low_priority:
|
|
||||||
lines.append(f"Low-priority tools: {', '.join(low_priority)}.")
|
|
||||||
|
|
||||||
if global_bias.category_multipliers:
|
if global_bias.category_multipliers:
|
||||||
lines.append("Category focus multipliers:")
|
lines.append("Category focus multipliers:")
|
||||||
for cat, mult in global_bias.category_multipliers.items():
|
for cat, mult in global_bias.category_multipliers.items():
|
||||||
lines.append(f"- {cat}: {mult}x")
|
lines.append(f"- {cat}: {mult}x")
|
||||||
|
|
||||||
return "\n\n".join(lines)
|
return "\n\n".join(lines)
|
||||||
|
|||||||
+11
-12
@@ -14,7 +14,7 @@ class ToolPresetManager:
|
|||||||
|
|
||||||
def _get_path(self, scope: str) -> Path:
|
def _get_path(self, scope: str) -> Path:
|
||||||
"""
|
"""
|
||||||
[C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile]
|
[C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||||
"""
|
"""
|
||||||
if scope == "global":
|
if scope == "global":
|
||||||
return paths.get_global_tool_presets_path()
|
return paths.get_global_tool_presets_path()
|
||||||
@@ -41,7 +41,7 @@ class ToolPresetManager:
|
|||||||
|
|
||||||
def load_all_presets(self) -> Dict[str, ToolPreset]:
|
def load_all_presets(self) -> Dict[str, ToolPreset]:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_tool_preset_manager.py:test_load_all_presets_merged]
|
[C: tests/test_tool_preset_manager.py:test_load_all_presets_merged]
|
||||||
"""
|
"""
|
||||||
global_path = paths.get_global_tool_presets_path()
|
global_path = paths.get_global_tool_presets_path()
|
||||||
global_data = self._read_raw(global_path).get("presets", {})
|
global_data = self._read_raw(global_path).get("presets", {})
|
||||||
@@ -62,15 +62,14 @@ class ToolPresetManager:
|
|||||||
|
|
||||||
def load_all(self) -> Dict[str, ToolPreset]:
|
def load_all(self) -> Dict[str, ToolPreset]:
|
||||||
"""
|
"""
|
||||||
|
Backward compatibility for load_all().
|
||||||
Backward compatibility for load_all().
|
[C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||||
[C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
|
||||||
"""
|
"""
|
||||||
return self.load_all_presets()
|
return self.load_all_presets()
|
||||||
|
|
||||||
def save_preset(self, preset: ToolPreset, scope: str = "project") -> None:
|
def save_preset(self, preset: ToolPreset, scope: str = "project") -> None:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_preset_manager.py:test_save_preset_project_no_root, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
[C: tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_preset_manager.py:test_save_preset_project_no_root, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||||
"""
|
"""
|
||||||
path = self._get_path(scope)
|
path = self._get_path(scope)
|
||||||
data = self._read_raw(path)
|
data = self._read_raw(path)
|
||||||
@@ -81,7 +80,7 @@ class ToolPresetManager:
|
|||||||
|
|
||||||
def delete_preset(self, name: str, scope: str = "project") -> None:
|
def delete_preset(self, name: str, scope: str = "project") -> None:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_preset_manager.py:test_delete_preset, tests/test_presets.py:TestPresetManager.test_delete_preset]
|
[C: tests/test_preset_manager.py:test_delete_preset, tests/test_presets.py:TestPresetManager.test_delete_preset]
|
||||||
"""
|
"""
|
||||||
path = self._get_path(scope)
|
path = self._get_path(scope)
|
||||||
data = self._read_raw(path)
|
data = self._read_raw(path)
|
||||||
@@ -91,7 +90,7 @@ class ToolPresetManager:
|
|||||||
|
|
||||||
def load_all_bias_profiles(self) -> Dict[str, BiasProfile]:
|
def load_all_bias_profiles(self) -> Dict[str, BiasProfile]:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_tool_preset_manager.py:test_bias_profiles_merged, tests/test_tool_preset_manager.py:test_delete_bias_profile, tests/test_tool_preset_manager.py:test_save_bias_profile]
|
[C: tests/test_tool_preset_manager.py:test_bias_profiles_merged, tests/test_tool_preset_manager.py:test_delete_bias_profile, tests/test_tool_preset_manager.py:test_save_bias_profile]
|
||||||
"""
|
"""
|
||||||
global_path = paths.get_global_tool_presets_path()
|
global_path = paths.get_global_tool_presets_path()
|
||||||
global_data = self._read_raw(global_path).get("bias_profiles", {})
|
global_data = self._read_raw(global_path).get("bias_profiles", {})
|
||||||
@@ -110,7 +109,7 @@ class ToolPresetManager:
|
|||||||
for name, config in project_data.items():
|
for name, config in project_data.items():
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
cfg = dict(config)
|
cfg = dict(config)
|
||||||
if "name" not in cfg:
|
if "name" not in cfg:
|
||||||
cfg["name"] = name
|
cfg["name"] = name
|
||||||
profiles[name] = BiasProfile.from_dict(cfg)
|
profiles[name] = BiasProfile.from_dict(cfg)
|
||||||
|
|
||||||
@@ -118,7 +117,7 @@ class ToolPresetManager:
|
|||||||
|
|
||||||
def save_bias_profile(self, profile: BiasProfile, scope: str = "project") -> None:
|
def save_bias_profile(self, profile: BiasProfile, scope: str = "project") -> None:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_tool_preset_manager.py:test_save_bias_profile]
|
[C: tests/test_tool_preset_manager.py:test_save_bias_profile]
|
||||||
"""
|
"""
|
||||||
path = self._get_path(scope)
|
path = self._get_path(scope)
|
||||||
data = self._read_raw(path)
|
data = self._read_raw(path)
|
||||||
@@ -129,10 +128,10 @@ class ToolPresetManager:
|
|||||||
|
|
||||||
def delete_bias_profile(self, name: str, scope: str = "project") -> None:
|
def delete_bias_profile(self, name: str, scope: str = "project") -> None:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_tool_preset_manager.py:test_delete_bias_profile]
|
[C: tests/test_tool_preset_manager.py:test_delete_bias_profile]
|
||||||
"""
|
"""
|
||||||
path = self._get_path(scope)
|
path = self._get_path(scope)
|
||||||
data = self._read_raw(path)
|
data = self._read_raw(path)
|
||||||
if "bias_profiles" in data and name in data["bias_profiles"]:
|
if "bias_profiles" in data and name in data["bias_profiles"]:
|
||||||
del data["bias_profiles"][name]
|
del data["bias_profiles"][name]
|
||||||
self._write_raw(path, data)
|
self._write_raw(path, data)
|
||||||
|
|||||||
+38
-38
@@ -6,10 +6,10 @@ class VendorMetric:
|
|||||||
"""Atomic vendor-state metric.
|
"""Atomic vendor-state metric.
|
||||||
[C: src/gui_2.py:render_vendor_state]
|
[C: src/gui_2.py:render_vendor_state]
|
||||||
"""
|
"""
|
||||||
key: str
|
key: str
|
||||||
label: str
|
label: str
|
||||||
value: str
|
value: str
|
||||||
state: str
|
state: str
|
||||||
tooltip: str
|
tooltip: str
|
||||||
|
|
||||||
def get_vendor_state(app) -> list[VendorMetric]:
|
def get_vendor_state(app) -> list[VendorMetric]:
|
||||||
@@ -18,64 +18,64 @@ def get_vendor_state(app) -> list[VendorMetric]:
|
|||||||
"""
|
"""
|
||||||
out: list[VendorMetric] = []
|
out: list[VendorMetric] = []
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="provider_model",
|
key = "provider_model",
|
||||||
label="Provider / Model",
|
label = "Provider / Model",
|
||||||
value=f"{app.current_provider} / {app.current_model}",
|
value = f"{app.current_provider} / {app.current_model}",
|
||||||
state="info",
|
state = "info",
|
||||||
tooltip="The vendor and model that will handle the next request."
|
tooltip = "The vendor and model that will handle the next request."
|
||||||
))
|
))
|
||||||
ctrl = getattr(app, "controller", None)
|
ctrl = getattr(app, "controller", None)
|
||||||
tt = getattr(ctrl, "token_tracker", None) if ctrl else None
|
tt = getattr(ctrl, "token_tracker", None) if ctrl else None
|
||||||
if tt and getattr(tt, "limit", 0):
|
if tt and getattr(tt, "limit", 0):
|
||||||
pct = 100.0 * getattr(tt, "used", 0) / tt.limit
|
pct = 100.0 * getattr(tt, "used", 0) / tt.limit
|
||||||
state = "warn" if pct > 75 else "ok"
|
state = "warn" if pct > 75 else "ok"
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="context_window",
|
key = "context_window",
|
||||||
label="Context Window",
|
label = "Context Window",
|
||||||
value=f"{tt.used:,} / {tt.limit:,} ({pct:.0f}%)",
|
value = f"{tt.used:,} / {tt.limit:,} ({pct:.0f}%)",
|
||||||
state=state,
|
state = state,
|
||||||
tooltip="Used vs total context window for the current session."
|
tooltip = "Used vs total context window for the current session."
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="context_window", label="Context Window", value="—", state="info",
|
key = "context_window", label="Context Window", value="—", state="info",
|
||||||
tooltip="No token tracker attached for the current provider."
|
tooltip = "No token tracker attached for the current provider."
|
||||||
))
|
))
|
||||||
if tt is not None:
|
if tt is not None:
|
||||||
hits = getattr(tt, "cache_hits", 0)
|
hits = getattr(tt, "cache_hits", 0)
|
||||||
miss = getattr(tt, "cache_misses", 0)
|
miss = getattr(tt, "cache_misses", 0)
|
||||||
total = hits + miss
|
total = hits + miss
|
||||||
rate = (100.0 * hits / total) if total else 0.0
|
rate = (100.0 * hits / total) if total else 0.0
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="cache", label="Cache Hit Rate",
|
key = "cache", label="Cache Hit Rate",
|
||||||
value=f"{rate:.0f}% ({hits:,}/{total:,})",
|
value = f"{rate:.0f}% ({hits:,}/{total:,})",
|
||||||
state="ok" if rate > 50 else "info",
|
state = "ok" if rate > 50 else "info",
|
||||||
tooltip="Server-side prompt cache hit rate for the current session."
|
tooltip = "Server-side prompt cache hit rate for the current session."
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="cache", label="Cache Hit Rate", value="—", state="info",
|
key = "cache", label="Cache Hit Rate", value="—", state="info",
|
||||||
tooltip="No token tracker attached for the current provider."
|
tooltip = "No token tracker attached for the current provider."
|
||||||
))
|
))
|
||||||
quota = (getattr(ctrl, "vendor_quota", {}) or {}) if ctrl else {}
|
quota = (getattr(ctrl, "vendor_quota", {}) or {}) if ctrl else {}
|
||||||
pct_left = quota.get("remaining_pct")
|
pct_left = quota.get("remaining_pct")
|
||||||
if pct_left is None:
|
if pct_left is None:
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="quota", label="Vendor Quota", value="—", state="info",
|
key = "quota", label="Vendor Quota", value="—", state="info",
|
||||||
tooltip="Vendor did not report quota for the current billing period."
|
tooltip = "Vendor did not report quota for the current billing period."
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="quota", label="Vendor Quota",
|
key = "quota", label="Vendor Quota",
|
||||||
value=f"{pct_left}% remaining",
|
value = f"{pct_left}% remaining",
|
||||||
state="ok" if pct_left > 25 else "warn",
|
state = "ok" if pct_left > 25 else "warn",
|
||||||
tooltip="Approximate quota remaining for the current billing period."
|
tooltip = "Approximate quota remaining for the current billing period."
|
||||||
))
|
))
|
||||||
err = getattr(ctrl, "last_error", None) if ctrl else None
|
err = getattr(ctrl, "last_error", None) if ctrl else None
|
||||||
out.append(VendorMetric(
|
out.append(VendorMetric(
|
||||||
key="last_error", label="Last Error",
|
key = "last_error", label="Last Error",
|
||||||
value=err.get("class", "none") if err else "none",
|
value = err.get("class", "none") if err else "none",
|
||||||
state="error" if err else "ok",
|
state = "error" if err else "ok",
|
||||||
tooltip=err.get("message", "No error since session start.") if err else "No error since session start."
|
tooltip = err.get("message", "No error since session start.") if err else "No error since session start."
|
||||||
))
|
))
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ class WorkspaceManager:
|
|||||||
|
|
||||||
def load_all_profiles(self) -> Dict[str, WorkspaceProfile]:
|
def load_all_profiles(self) -> Dict[str, WorkspaceProfile]:
|
||||||
"""
|
"""
|
||||||
|
Merges global and project profiles into a single dictionary.
|
||||||
Merges global and project profiles into a single dictionary.
|
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_load_all_profiles_merged, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
||||||
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_load_all_profiles_merged, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
|
||||||
"""
|
"""
|
||||||
profiles = {}
|
profiles = {}
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ class WorkspaceManager:
|
|||||||
|
|
||||||
def save_profile(self, profile: WorkspaceProfile, scope: str = "project") -> None:
|
def save_profile(self, profile: WorkspaceProfile, scope: str = "project") -> None:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
||||||
"""
|
"""
|
||||||
path = self._get_path(scope)
|
path = self._get_path(scope)
|
||||||
data = self._load_file(path)
|
data = self._load_file(path)
|
||||||
@@ -62,7 +61,7 @@ class WorkspaceManager:
|
|||||||
|
|
||||||
def delete_profile(self, name: str, scope: str = "project") -> None:
|
def delete_profile(self, name: str, scope: str = "project") -> None:
|
||||||
"""
|
"""
|
||||||
[C: tests/test_workspace_manager.py:test_delete_profile]
|
[C: tests/test_workspace_manager.py:test_delete_profile]
|
||||||
"""
|
"""
|
||||||
path = self._get_path(scope)
|
path = self._get_path(scope)
|
||||||
data = self._load_file(path)
|
data = self._load_file(path)
|
||||||
|
|||||||
Reference in New Issue
Block a user