Private
Public Access
0
0

progress on context composition

This commit is contained in:
2026-05-17 07:27:55 -04:00
parent c1487d32bb
commit a5c0569417
4 changed files with 194 additions and 119 deletions
+71 -40
View File
@@ -199,52 +199,83 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
mtime = path.stat().st_mtime
error = False
if not error and view_mode != "full":
if view_mode == "summary":
content = summarize.summarise_file(path, content)
elif view_mode == "skeleton":
if path.suffix == ".py":
if not parser: parser = ASTParser("python")
content = parser.get_skeleton(content, path=str(path))
elif path.suffix in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
from src import mcp_client
if path.suffix in ['.c', '.h']: content = mcp_client.ts_c_get_skeleton(str(path))
else: content = mcp_client.ts_cpp_get_skeleton(str(path))
else:
content = summarize.summarise_file(path, content)
elif view_mode == "outline":
if path.suffix == ".py":
if not parser: parser = ASTParser("python")
content = parser.get_code_outline(content, path=str(path))
elif path.suffix in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
from src import mcp_client
if path.suffix in ['.c', '.h']: content = mcp_client.ts_c_get_code_outline(str(path))
else: content = mcp_client.ts_cpp_get_code_outline(str(path))
else:
content = summarize.summarise_file(path, content)
elif view_mode == "none":
content = "(context excluded)"
elif view_mode == "custom":
if custom_slices:
lines = content.splitlines()
slices_text = []
for s in custom_slices:
start = s.get("start_line", 1)
end = s.get("end_line", len(lines))
tag = s.get("tag", "unnamed")
comment = s.get("comment", "")
s_idx = max(0, start - 1)
e_idx = min(len(lines), end)
chunk = "\n".join(lines[s_idx:e_idx])
slices_text.append(f"---\n[Slice: {tag}] ({comment})\nLines {start}-{end}:\n{chunk}")
content = "\n\n".join(slices_text)
else:
try:
if view_mode == "summary":
content = summarize.summarise_file(path, content)
elif view_mode == "skeleton":
suffix_lower = path.suffix.lower()
if suffix_lower == ".py":
if not parser: parser = ASTParser("python")
content = parser.get_skeleton(content, path=str(path))
elif suffix_lower in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
from src import mcp_client
if suffix_lower in ['.c', '.h']: content = mcp_client.ts_c_get_skeleton(str(path))
else: content = mcp_client.ts_cpp_get_skeleton(str(path))
else:
content = summarize.summarise_file(path, content)
elif view_mode == "outline":
suffix_lower = path.suffix.lower()
if suffix_lower == ".py":
if not parser: parser = ASTParser("python")
content = parser.get_code_outline(content, path=str(path))
elif suffix_lower in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
from src import mcp_client
if suffix_lower in ['.c', '.h']: content = mcp_client.ts_c_get_code_outline(str(path))
else: content = mcp_client.ts_cpp_get_code_outline(str(path))
else:
content = summarize.summarise_file(path, content)
elif view_mode == "masked":
suffix_lower = path.suffix.lower()
if ast_mask:
mask_sections = []
from src import mcp_client
for symbol, mode in ast_mask.items():
if mode == "hide": continue
res = ""
if suffix_lower == ".py":
res = mcp_client.py_get_definition(str(path), symbol) if mode == "def" else mcp_client.py_get_signature(str(path), symbol)
elif suffix_lower in [".c", ".h", ".cpp", ".hpp", ".cxx", ".cc"]:
is_cpp = any(ext in suffix_lower for ext in [".cpp", ".hpp", ".cxx", ".cc"])
if mode == "def":
res = mcp_client.ts_cpp_get_definition(str(path), symbol) if is_cpp else mcp_client.ts_c_get_definition(str(path), symbol)
else:
res = mcp_client.ts_cpp_get_signature(str(path), symbol) if is_cpp else mcp_client.ts_c_get_signature(str(path), symbol)
if res: mask_sections.append(res)
if mask_sections:
content = "\n\n".join(mask_sections)
else:
content = "(no masked sections visible)"
else:
content = "(no ast mask defined)"
elif view_mode == "none":
content = "(context excluded)"
elif view_mode == "custom":
if custom_slices:
lines = content.splitlines()
slices_text = []
for s in custom_slices:
start = s.get("start_line", 1)
end = s.get("end_line", len(lines))
tag = s.get("tag", "unnamed")
comment = s.get("comment", "")
s_idx = max(0, start - 1)
e_idx = min(len(lines), end)
chunk = "\n".join(lines[s_idx:e_idx])
slices_text.append(f"---\n[Slice: {tag}] ({comment})\nLines {start}-{end}:\n{chunk}")
content = "\n\n".join(slices_text)
else:
content = summarize.summarise_file(path, content)
except Exception as e:
import traceback
content = f"ERROR in {view_mode} view mode for {path}:\n{traceback.format_exc()}"
error = True
except FileNotFoundError:
content = f"ERROR: file not found: {path}"
mtime = 0.0
error = True
except Exception as e:
content = f"ERROR: {e}"
import traceback
content = f"ERROR reading {path}:\n{traceback.format_exc()}"
mtime = 0.0
error = True
items.append({"path": path, "entry": entry, "content": content, "error": error, "mtime": mtime, "tier": tier, "auto_aggregate": auto_aggregate, "force_full": force_full, "view_mode": view_mode, "ast_signatures": ast_signatures, "ast_definitions": ast_definitions, "ast_mask": ast_mask, "custom_slices": custom_slices})
+25 -26
View File
@@ -2929,29 +2929,20 @@ class AppController:
if name not in presets:
raise KeyError(f"Context preset '{name}' not found.")
preset = presets[name]
# Apply it to the current state
self.ui_file_paths = [f.path for f in preset.files]
self.screenshots = list(preset.screenshots)
self._save_active_project()
# We need to tell gui_2 to populate the full FileItem state from the preset,
# which it does in _handle_refresh_from_project. But we also need to pass the detailed properties.
# We will let the project_manager handle merging in the preset files via configuration next turn.
# Wait, project manager doesn't load preset files into self.files automatically here.
# Let's write the preset files into self.project["files"] directly.
import copy
self.project.setdefault("files", {})["paths"] = [
{
"path": f.path,
"view_mode": f.view_mode,
"custom_slices": copy.deepcopy(f.custom_slices),
"ast_mask": copy.deepcopy(f.ast_mask),
"ast_signatures": getattr(f, "ast_signatures", False),
"ast_definitions": getattr(f, "ast_definitions", False)
} for f in preset.files
]
self._save_active_project()
return preset
# Update only temporary context state, not project files
import copy
self.context_files = []
for f in preset.files:
fi = models.FileItem(path=f.path, view_mode=f.view_mode)
fi.custom_slices = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
fi.ast_mask = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
fi.ast_signatures = getattr(f, 'ast_signatures', False)
fi.ast_definitions = getattr(f, 'ast_definitions', False)
self.context_files.append(fi)
self.screenshots = list(preset.screenshots)
return preset
def _cb_load_track(self, track_id: str) -> None:
"""
[C: src/gui_2.py:App._render_mma_track_browser]
@@ -3459,11 +3450,19 @@ class AppController:
models.save_config(self.config)
track_id = self.active_track.id if self.active_track else None
flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id)
flat.setdefault("files", {})["paths"] = self.context_files
import copy
flat["files"] = copy.copy(flat.get("files", {}))
flat["files"]["paths"] = self.context_files
# Configure MCP so that aggregate.py can fetch skeletons for external files (e.g. gencpp)
file_dicts = [f.to_dict() if hasattr(f, 'to_dict') else {"path": str(f)} for f in self.context_files]
mcp_client.configure(file_dicts, [self.active_project_root] if self.active_project_root else None)
import os
file_dicts = []
for f in self.context_files:
p = f.path if hasattr(f, 'path') else str(f)
if not os.path.isabs(p):
p = os.path.join(self.ui_files_base_dir, p)
file_dicts.append({"path": p})
mcp_client.configure(file_dicts, [self.ui_files_base_dir])
persona = self.personas.get(self.ui_active_persona)
strategy = persona.aggregation_strategy if persona else "auto"
+92 -40
View File
@@ -627,7 +627,16 @@ class App:
[C: tests/test_context_presets.py:test_load_context_preset, tests/test_context_presets.py:test_load_nonexistent_preset]
"""
preset = self.controller.load_context_preset(name)
self.context_files = [models.FileItem(path=f.path, view_mode=f.view_mode) for f in preset.files]
from src import models
import copy
self.context_files = []
for f in preset.files:
fi = models.FileItem(path=f.path, view_mode=f.view_mode)
fi.custom_slices = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
fi.ast_mask = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
fi.ast_signatures = getattr(f, 'ast_signatures', False)
fi.ast_definitions = getattr(f, 'ast_definitions', False)
self.context_files.append(fi)
self.screenshots = list(preset.screenshots)
self.ui_file_paths = [f.path for f in preset.files]
self.ui_screenshot_paths = list(preset.screenshots)
@@ -3017,7 +3026,6 @@ def render_ast_inspector_modal(app: App) -> None:
if imgui.is_item_hovered():
app._hovered_ast_node = full_path
# Calculate space left and align radio buttons to the right
btn_width = 150 # Estimated width of the 3 radio buttons
avail_width = imgui.get_content_region_avail().x
if avail_width > btn_width:
@@ -3255,7 +3263,6 @@ def render_context_files_table(app: App) -> None:
imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]")
def render_context_presets(app: App) -> None:
imgui.text("Presets")
presets = app.controller.project.get('context_presets', {})
preset_names = [""] + sorted(presets.keys())
active = getattr(app, "ui_active_context_preset", "")
@@ -3264,47 +3271,86 @@ def render_context_presets(app: App) -> None:
idx = preset_names.index(active)
except ValueError:
idx = 0
ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names)
if ch:
app.ui_active_context_preset = preset_names[new_idx]
if preset_names[new_idx]: app.load_context_preset(preset_names[new_idx])
imgui.same_line()
changed, new_name = imgui.input_text("##new_preset", getattr(app, "ui_new_context_preset_name", ""))
if changed: app.ui_new_context_preset_name = new_name
imgui.same_line()
if imgui.button("Save##ctx") or getattr(app, "_pending_save_ctx_click", False):
app._pending_save_ctx_click = False
name = getattr(app, "ui_new_context_preset_name", "").strip()
if name:
missing = []
root = app.controller.active_project_root
for f in app.context_files:
path = f.path if hasattr(f, "path") else str(f)
if not os.path.isabs(path):
full_path = os.path.join(root, path)
else:
full_path = path
if not os.path.exists(full_path):
missing.append(path)
if missing:
app.missing_context_files = missing
app.show_missing_files_modal = True
app.target_context_preset_name = name
else:
with imscope.table("ctx_presets_layout", 2, imgui.TableFlags_.none):
imgui.table_next_column()
imgui.set_next_item_width(-1)
ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names)
if ch:
app.ui_active_context_preset = preset_names[new_idx]
if preset_names[new_idx]:
app.controller.load_context_preset(preset_names[new_idx])
app.controller._refresh_from_project()
app.context_files = list(app.controller.files)
imgui.table_next_column()
if active:
if imgui.button("Update##override", imgui.ImVec2(-1, 0)):
preset_files = []
for f in app.context_files:
import copy
from src import models
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm))
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
preset = models.ContextPreset(name=active, files=preset_files, screenshots=list(app.screenshots))
app.controller.save_context_preset(preset)
app.ui_new_context_preset_name = ""
imgui.same_line()
if imgui.button("Delete##ctx"):
if getattr(app, "ui_active_context_preset", ""):
app.delete_context_preset(app.ui_active_context_preset)
app.ui_active_context_preset = ""
else:
imgui.text_disabled("No active preset")
imgui.table_next_row()
imgui.table_next_column()
imgui.set_next_item_width(-1)
changed, new_name = imgui.input_text("##new_preset", getattr(app, "ui_new_context_preset_name", ""))
if changed: app.ui_new_context_preset_name = new_name
imgui.table_next_column()
if imgui.button("Save As##ctx", imgui.ImVec2(-1, 0)) or getattr(app, "_pending_save_ctx_click", False):
app._pending_save_ctx_click = False
name = getattr(app, "ui_new_context_preset_name", "").strip()
if name:
missing = []
root = app.controller.active_project_root
for f in app.context_files:
path = f.path if hasattr(f, "path") else str(f)
if not os.path.isabs(path):
full_path = os.path.join(root, path)
else:
full_path = path
if not os.path.exists(full_path):
missing.append(path)
if missing:
app.missing_context_files = missing
app.show_missing_files_modal = True
app.target_context_preset_name = name
else:
preset_files = []
for f in app.context_files:
import copy
from src import models
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
app.controller.save_context_preset(preset)
app.ui_new_context_preset_name = ""
if active:
imgui.table_next_row()
imgui.table_next_column()
imgui.table_next_column()
if imgui.button("Delete Active", imgui.ImVec2(-1, 0)):
app.delete_context_preset(active)
app.ui_active_context_preset = ""
def render_snapshot_tab(app: App) -> None:
if imgui.begin_tab_bar("snapshot_tabs"):
@@ -5380,9 +5426,15 @@ def render_context_modals(app: App) -> None:
name = app.target_context_preset_name
preset_files = []
for f in app.context_files:
import copy
from src import models
p = f.path if hasattr(f, 'path') else str(f)
vm = f.view_mode if hasattr(f, 'view_mode') else 'summary'
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm))
slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else []
msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {}
sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False
dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False
preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn))
preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots))
app.controller.save_context_preset(preset)
app.ui_new_context_preset_name = ""
+6 -13
View File
@@ -22,7 +22,6 @@ For each file, extracts structural information:
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.
"""
import ast
import re
from pathlib import Path
@@ -42,7 +41,6 @@ def _summarise_python(path: Path, content: str) -> str:
except SyntaxError as e:
parts.append(f"_Parse error: {e}_")
return "\n".join(parts)
# Imports
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
@@ -54,7 +52,6 @@ def _summarise_python(path: Path, content: str) -> str:
if imports:
unique_imports = sorted(set(imports))
parts.append(f"imports: {', '.join(unique_imports)}")
# Top-level constants (ALL_CAPS assignments)
constants = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Assign):
@@ -66,7 +63,6 @@ def _summarise_python(path: Path, content: str) -> str:
constants.append(node.target.id)
if constants:
parts.append(f"constants: {', '.join(constants)}")
# Classes + their methods
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.ClassDef):
methods = [
@@ -77,7 +73,6 @@ def _summarise_python(path: Path, content: str) -> str:
parts.append(f"class {node.name}: {', '.join(methods)}")
else:
parts.append(f"class {node.name}")
# Top-level functions
top_fns = [
node.name for node in ast.iter_child_nodes(tree)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
@@ -90,7 +85,6 @@ def _summarise_toml(path: Path, content: str) -> str:
lines = content.splitlines()
line_count = len(lines)
parts = [f"**TOML** — {line_count} lines"]
# Extract top-level table headers [key] and [[key]]
table_pat = re.compile(r"^\s*\[{1,2}([^\[\]]+)\]{1,2}")
tables = []
for line in lines:
@@ -99,7 +93,6 @@ def _summarise_toml(path: Path, content: str) -> str:
tables.append(m.group(1).strip())
if tables:
parts.append(f"tables: {', '.join(tables)}")
# Top-level key = value (not inside a [table])
kv_pat = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)\s*=")
in_table = False
top_keys = []
@@ -140,7 +133,6 @@ def _summarise_generic(path: Path, content: str) -> str:
if preview:
parts.append("preview:\n```\n" + "\n".join(preview) + "\n```")
return "\n".join(parts)
# ------------------------------------------------------------------ dispatch
_SUMMARISERS: dict[str, Callable[[Path, str], str]] = {
".py": _summarise_python,
@@ -148,6 +140,10 @@ _SUMMARISERS: dict[str, Callable[[Path, str], str]] = {
".md": _summarise_markdown,
".ini": _summarise_generic,
".txt": _summarise_generic,
".c": _summarise_generic,
".h": _summarise_generic,
".cpp": _summarise_generic,
".hpp": _summarise_generic,
".ps1": _summarise_generic,
}
@@ -163,19 +159,17 @@ def summarise_file(path: Path, content: str) -> str:
cached = _summary_cache.get_summary(str(path), content_hash)
if cached:
return cached
suffix = path.suffix.lower() if hasattr(path, "suffix") else ""
fn = _SUMMARISERS.get(suffix, _summarise_generic)
try:
heuristic_outline = fn(path, content)
# Smart AI Summarization
is_code = suffix in [".py", ".ps1", ".js", ".ts", ".cpp", ".c", ".h", ".cs", ".go", ".rs", ".lua"]
try:
from src import ai_client
smart_summary = ai_client.run_subagent_summarization(
file_path=str(path),
content=content[:10000], # Cap content to 10k chars for summarization
content=content[:10000],
is_code=is_code,
outline=heuristic_outline
)
@@ -184,8 +178,7 @@ def summarise_file(path: Path, content: str) -> str:
else:
summary = heuristic_outline
except Exception:
summary = heuristic_outline # Fallback
summary = heuristic_outline
_summary_cache.set_summary(str(path), content_hash, summary)
return summary
except Exception as e: