7 Commits

Author SHA1 Message Date
ed 74737ac9c7 fix(core): Anchor config.toml path to manual slop root
This fixes an issue where config.toml was erroneously saved to the current working directory (e.g. project dir) rather than the global manual slop directory.
2026-03-08 21:29:54 -04:00
ed 1d18150570 conductor(plan): Mark Phase 1 as complete 2026-03-08 21:27:18 -04:00
ed ef942bb2a2 feat(ui): Implement _render_selectable_label helper and complete UI audit 2026-03-08 21:26:59 -04:00
ed b7a0c4fa7e conductor(plan): Add PopStyleColor crash fix to plan 2026-03-08 21:20:30 -04:00
ed 27b98ffe1e fix(ui): Prevent PopStyleColor crash by using frame-scoped tint flag 2026-03-08 21:20:13 -04:00
ed a6f7f82f02 conductor(plan): Add session restoration hardening to plan 2026-03-08 21:17:46 -04:00
ed bbe0209403 feat(logs): Harden session restoration for legacy logs and offloaded data resolution 2026-03-08 21:17:27 -04:00
8 changed files with 136 additions and 19 deletions
@@ -17,8 +17,10 @@
- [ ] Implement logic to load all related logs (comms, mma, tools) for that session. - [ ] Implement logic to load all related logs (comms, mma, tools) for that session.
- [ ] Ensure that for entries referencing external files (scripts/outputs), the content is loaded on-demand or during the restoration process. - [ ] Ensure that for entries referencing external files (scripts/outputs), the content is loaded on-demand or during the restoration process.
- [x] Task: Implement "Historical Replay" UI mode. 1b3fc5b - [x] Task: Implement "Historical Replay" UI mode. 1b3fc5b
- [ ] In `src/gui_2.py`, implement logic to tint the UI (as already partially done for comms) when `is_viewing_prior_session` is True. - [x] In `src/gui_2.py`, implement logic to tint the UI (as already partially done for comms) when `is_viewing_prior_session` is True.
- [ ] Populate `disc_entries`, `_comms_log`, and MMA Dashboard states from the loaded session logs. - [x] Populate `disc_entries`, `_comms_log`, and MMA Dashboard states from the loaded session logs.
- [x] Harden `cb_load_prior_log` for legacy compatibility and reference resolution. bbe0209
- [x] Fix `PopStyleColor()` crash in `_gui_func` using frame-scoped flag. 27b98ff
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Session-Level Restoration' (Protocol in workflow.md) - [ ] Task: Conductor - User Manual Verification 'Phase 2: Session-Level Restoration' (Protocol in workflow.md)
## Phase 3: Diagnostic Log & Discussion Cleanup ## Phase 3: Diagnostic Log & Discussion Cleanup
@@ -1,10 +1,10 @@
# Implementation Plan: Selectable GUI Text & UX Improvements # Implementation Plan: Selectable GUI Text & UX Improvements
## Phase 1: Research & Core Widget Wrapping ## Phase 1: Research & Core Widget Wrapping [checkpoint: ef942bb]
- [ ] Task: Audit `gui_2.py` for all `imgui.text()` and `imgui.text_wrapped()` calls in target areas. - [x] Task: Audit `gui_2.py` for all `imgui.text()` and `imgui.text_wrapped()` calls in target areas.
- [ ] Identify the exact locations in `_render_discussion_panel`, `_render_comms_history_panel`, and `_render_ai_settings_panel`. - [x] Identify the exact locations in `_render_discussion_panel`, `_render_comms_history_panel`, and `_render_ai_settings_panel`. Findings: `_render_discussion_panel` (historical/current entries, commit SHA), `_render_heavy_text` (comms/tool payloads), `_render_provider_panel` (Session ID), `_render_token_budget_panel` (telemetry metrics).
- [ ] Task: Implement a helper function/component for "Selectable Label". - [x] Task: Implement a helper function/component for "Selectable Label".
- [ ] This helper should wrap `imgui.input_text` with `InputTextFlags_.read_only` and proper styling to mimic a standard label. - [x] This helper should wrap `imgui.input_text` with `InputTextFlags_.read_only` and proper styling to mimic a standard label. Implemented `_render_selectable_label` in `gui_2.py`.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Research & Core Widget' (Protocol in workflow.md) - [ ] Task: Conductor - User Manual Verification 'Phase 1: Research & Core Widget' (Protocol in workflow.md)
## Phase 2: Discussion History & Comms Log ## Phase 2: Discussion History & Comms Log
+49
View File
@@ -0,0 +1,49 @@
import sys
def insert_method(file_path):
with open(file_path, 'r', encoding='utf-8', newline='') as f:
lines = f.readlines()
target_line = -1
for i, line in enumerate(lines):
if 'def _render_heavy_text' in line:
# Find the end of this method
for j in range(i + 1, len(lines)):
if lines[j].startswith(' def '):
target_line = j
break
if target_line != -1:
break
if target_line == -1:
print("Could not find insertion point")
sys.exit(1)
new_method = [
'\n',
' def _render_selectable_label(self, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None:\n',
' imgui.push_id(label + str(hash(value)))\n',
' pops = 2\n',
' imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0))\n',
' imgui.push_style_color(imgui.Col_.border, vec4(0, 0, 0, 0))\n',
' if color:\n',
' imgui.push_style_color(imgui.Col_.text, color)\n',
' pops += 1\n',
' if multiline:\n',
' imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)\n',
' else:\n',
' if width > 0: imgui.set_next_item_width(width)\n',
' imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only)\n',
' imgui.pop_style_color(pops)\n',
' imgui.pop_id()\n'
]
lines[target_line:target_line] = new_method
with open(file_path, 'w', encoding='utf-8', newline='') as f:
f.writelines(lines)
print("Successfully inserted _render_selectable_label")
if __name__ == "__main__":
insert_method("src/gui_2.py")
+4 -3
View File
@@ -342,16 +342,17 @@ def run(config: dict[str, Any]) -> tuple[str, Path, list[dict[str, Any]]]:
def main() -> None: def main() -> None:
# Load global config to find active project # Load global config to find active project
config_path = Path(os.environ.get("SLOP_CONFIG", "config.toml")) from src.paths import get_config_path
config_path = get_config_path()
if not config_path.exists(): if not config_path.exists():
print("config.toml not found.") print(f"{config_path} not found.")
return return
with open(config_path, "rb") as f: with open(config_path, "rb") as f:
global_cfg = tomllib.load(f) global_cfg = tomllib.load(f)
active_path = global_cfg.get("projects", {}).get("active") active_path = global_cfg.get("projects", {}).get("active")
if not active_path: if not active_path:
print("No active project found in config.toml.") print(f"No active project found in {config_path}.")
return return
# Use project_manager to load project (handles history segregation) # Use project_manager to load project (handles history segregation)
proj = project_manager.load_project(active_path) proj = project_manager.load_project(active_path)
+50 -6
View File
@@ -821,13 +821,35 @@ class AppController:
log_path = Path(path) log_path = Path(path)
if log_path.is_dir(): if log_path.is_dir():
log_file = log_path / "comms.log" log_file = log_path / "comms.log"
session_dir = log_path
else: else:
log_file = log_path log_file = log_path
session_dir = log_path.parent
if not log_file.exists(): if not log_file.exists():
self._set_status(f"log file not found: {log_file}") self._set_status(f"log file not found: {log_file}")
return return
def _resolve_log_ref(content: Any, session_dir: Path) -> str:
if not content or not isinstance(content, str) or "[REF:" not in content:
return str(content) if content is not None else ""
pattern = r'\[REF:([^\]]+)\]'
def replace_ref(match):
ref_file = match.group(1)
paths_to_check = [
session_dir / "outputs" / ref_file,
session_dir / "scripts" / ref_file
]
for p in paths_to_check:
if p.exists():
try:
with open(p, "r", encoding="utf-8") as rf:
return rf.read()
except Exception:
return f"[ERROR READING REF: {ref_file}]"
return match.group(0)
return re.sub(pattern, replace_ref, content)
entries = [] entries = []
disc_entries = [] disc_entries = []
try: try:
@@ -838,35 +860,57 @@ class AppController:
try: try:
entry = json.loads(line) entry = json.loads(line)
entries.append(entry) entries.append(entry)
kind = entry.get("kind") kind = entry.get("kind", entry.get("type", ""))
payload = entry.get("payload", {}) payload = entry.get("payload", {})
ts = entry.get("ts", "") ts = entry.get("ts", "")
if kind == "history_add": if kind == "history_add":
content = payload.get("content", payload.get("text", payload.get("message", "")))
content = _resolve_log_ref(content, session_dir)
disc_entries.append({ disc_entries.append({
"role": payload.get("role", "AI"), "role": payload.get("role", "AI"),
"content": payload.get("content", ""), "content": content,
"collapsed": payload.get("collapsed", False), "collapsed": payload.get("collapsed", False),
"ts": ts "ts": ts
}) })
elif kind == "request": elif kind == "request":
content = payload.get("message", payload.get("content", payload.get("text", "")))
content = _resolve_log_ref(content, session_dir)
disc_entries.append({ disc_entries.append({
"role": "User", "role": "User",
"content": payload.get("message", ""), "content": content,
"collapsed": False, "collapsed": False,
"ts": ts "ts": ts
}) })
elif kind == "response": elif kind == "response":
text = payload.get("text", payload.get("content", payload.get("message", "")))
text = _resolve_log_ref(text, session_dir)
tool_calls = payload.get("tool_calls", [])
content = text
if tool_calls:
try:
tc_str = json.dumps(tool_calls, indent=1)
if content:
content += f"\n\n[TOOL CALLS]\n{tc_str}"
else:
content = f"[TOOL CALLS]\n{tc_str}"
except:
if content:
content += f"\n\n[TOOL CALLS PRESENT]"
else:
content = "[TOOL CALLS PRESENT]"
disc_entries.append({ disc_entries.append({
"role": "AI", "role": "AI",
"content": payload.get("text", ""), "content": content,
"collapsed": False, "collapsed": False,
"ts": ts "ts": ts
}) })
elif kind == "tool_result": elif kind == "tool_result":
output = payload.get("output", payload.get("content", ""))
output = _resolve_log_ref(output, session_dir)
disc_entries.append({ disc_entries.append({
"role": "Tool", "role": "Tool",
"content": f"[TOOL RESULT]\n{payload.get('output', '')}", "content": f"[TOOL RESULT]\n{output}",
"collapsed": True, "collapsed": True,
"ts": ts "ts": ts
}) })
@@ -879,7 +923,7 @@ class AppController:
self.prior_session_entries = entries self.prior_session_entries = entries
self.prior_disc_entries = disc_entries self.prior_disc_entries = disc_entries
self.is_viewing_prior_session = True self.is_viewing_prior_session = True
self._set_status(f"viewing prior session: {log_path.name} ({len(entries)} entries)") self._set_status(f"viewing prior session: {session_dir.name} ({len(entries)} entries)")
def cb_prune_logs(self) -> None: def cb_prune_logs(self) -> None:
"""Manually triggers the log pruning process with aggressive thresholds.""" """Manually triggers the log pruning process with aggressive thresholds."""
+19 -1
View File
@@ -222,6 +222,22 @@ class App:
imgui.text_unformatted(content) imgui.text_unformatted(content)
# ---------------------------------------------------------------- gui # ---------------------------------------------------------------- gui
def _render_selectable_label(self, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None:
imgui.push_id(label + str(hash(value)))
pops = 2
imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0))
imgui.push_style_color(imgui.Col_.border, vec4(0, 0, 0, 0))
if color:
imgui.push_style_color(imgui.Col_.text, color)
pops += 1
if multiline:
imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only)
else:
if width > 0: imgui.set_next_item_width(width)
imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only)
imgui.pop_style_color(pops)
imgui.pop_id()
def _show_menus(self) -> None: def _show_menus(self) -> None:
if imgui.begin_menu("manual slop"): if imgui.begin_menu("manual slop"):
if imgui.menu_item("Quit", "Ctrl+Q", False)[0]: if imgui.menu_item("Quit", "Ctrl+Q", False)[0]:
@@ -256,9 +272,11 @@ class App:
imgui.end_menu() imgui.end_menu()
def _gui_func(self) -> None: def _gui_func(self) -> None:
pushed_prior_tint = False
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func") if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
if self.is_viewing_prior_session: if self.is_viewing_prior_session:
imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20)) imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20))
pushed_prior_tint = True
try: try:
self.perf_monitor.start_frame() self.perf_monitor.start_frame()
self._autofocus_response_tab = self.controller._autofocus_response_tab self._autofocus_response_tab = self.controller._autofocus_response_tab
@@ -761,7 +779,7 @@ class App:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if self.is_viewing_prior_session: if pushed_prior_tint:
imgui.pop_style_color() imgui.pop_style_color()
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func") if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
+2 -1
View File
@@ -49,7 +49,8 @@ from typing import Optional
_RESOLVED: dict[str, Path] = {} _RESOLVED: dict[str, Path] = {}
def get_config_path() -> Path: def get_config_path() -> Path:
return Path(os.environ.get("SLOP_CONFIG", "config.toml")) root_dir = Path(__file__).resolve().parent.parent
return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml"))
def _resolve_path(env_var: str, config_key: str, default: str) -> Path: def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
if env_var in os.environ: if env_var in os.environ:
+3 -1
View File
@@ -13,7 +13,9 @@ def test_default_paths():
assert paths.get_conductor_dir() == Path("conductor") assert paths.get_conductor_dir() == Path("conductor")
assert paths.get_logs_dir() == Path("logs/sessions") assert paths.get_logs_dir() == Path("logs/sessions")
assert paths.get_scripts_dir() == Path("scripts/generated") assert paths.get_scripts_dir() == Path("scripts/generated")
assert paths.get_config_path() == Path("config.toml") # config path should now be an absolute path relative to src/paths.py
root_dir = Path(paths.__file__).resolve().parent.parent
assert paths.get_config_path() == root_dir / "config.toml"
assert paths.get_tracks_dir() == Path("conductor/tracks") assert paths.get_tracks_dir() == Path("conductor/tracks")
assert paths.get_archive_dir() == Path("conductor/archive") assert paths.get_archive_dir() == Path("conductor/archive")