From 1b3fc5ba2f3583ddaafdea12175271f47db1872d Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 8 Mar 2026 21:03:37 -0400 Subject: [PATCH] feat(logs): Implement session restoration and historical replay mode --- src/app_controller.py | 68 +++++++++++++++++++++++++++++++++++++------ src/gui_2.py | 57 +++++++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 23 deletions(-) diff --git a/src/app_controller.py b/src/app_controller.py index 8aeb244..28b6905 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -287,6 +287,9 @@ class AppController: self._tier_stream_last_len: Dict[str, int] = {} self.is_viewing_prior_session: bool = False self.prior_session_entries: List[Dict[str, Any]] = [] + self.prior_tool_calls: List[Dict[str, Any]] = [] + self.prior_disc_entries: List[Dict[str, Any]] = [] + self.prior_mma_dashboard_state: Dict[str, Any] = {} self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1") self.ui_manual_approve: bool = False # Injection state @@ -804,32 +807,79 @@ class AppController: label = self.project.get("project", {}).get("name", "") session_logger.open_session(label=label) - def cb_load_prior_log(self) -> None: + def cb_load_prior_log(self, path: Optional[str] = None) -> None: root = hide_tk_root() - path = filedialog.askopenfilename( - title="Load Session Log", - initialdir=str(paths.get_logs_dir()), - filetypes=[("Log/JSONL", "*.log *.jsonl"), ("All Files", "*.*")] - ) + if path is None: + path = filedialog.askdirectory( + title="Select Session Directory", + initialdir=str(paths.get_logs_dir()) + ) root.destroy() if not path: return + + log_path = Path(path) + if log_path.is_dir(): + log_file = log_path / "comms.log" + else: + log_file = log_path + + if not log_file.exists(): + self._set_status(f"log file not found: {log_file}") + return + entries = [] + disc_entries = [] try: - with open(path, "r", encoding="utf-8") as f: + with open(log_file, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line: try: - entries.append(json.loads(line)) + entry = json.loads(line) + entries.append(entry) + kind = entry.get("kind") + payload = entry.get("payload", {}) + ts = entry.get("ts", "") + + if kind == "history_add": + disc_entries.append({ + "role": payload.get("role", "AI"), + "content": payload.get("content", ""), + "collapsed": payload.get("collapsed", False), + "ts": ts + }) + elif kind == "request": + disc_entries.append({ + "role": "User", + "content": payload.get("message", ""), + "collapsed": False, + "ts": ts + }) + elif kind == "response": + disc_entries.append({ + "role": "AI", + "content": payload.get("text", ""), + "collapsed": False, + "ts": ts + }) + elif kind == "tool_result": + disc_entries.append({ + "role": "Tool", + "content": f"[TOOL RESULT]\n{payload.get('output', '')}", + "collapsed": True, + "ts": ts + }) except json.JSONDecodeError: continue except Exception as e: self._set_status(f"log load error: {e}") return + self.prior_session_entries = entries + self.prior_disc_entries = disc_entries self.is_viewing_prior_session = True - self._set_status(f"viewing prior session: {Path(path).name} ({len(entries)} entries)") + self._set_status(f"viewing prior session: {log_path.name} ({len(entries)} entries)") def cb_prune_logs(self) -> None: """Manually triggers the log pruning process with aggressive thresholds.""" diff --git a/src/gui_2.py b/src/gui_2.py index 69c06c7..1b7ec32 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -257,6 +257,8 @@ class App: def _gui_func(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func") + if self.is_viewing_prior_session: + imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20)) try: self.perf_monitor.start_frame() self._autofocus_response_tab = self.controller._autofocus_response_tab @@ -832,6 +834,11 @@ class App: print(f"ERROR in _gui_func: {e}") import traceback traceback.print_exc() + + if self.is_viewing_prior_session: + imgui.pop_style_color() + + if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func") def _render_projects_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel") @@ -1093,6 +1100,9 @@ class App: imgui.table_next_column() imgui.text(str(metadata.get("message_count", ""))) imgui.table_next_column() + if imgui.button(f"Load##{session_id}"): + self.cb_load_prior_log(s_data.get("path")) + imgui.same_line() if whitelisted: if imgui.button(f"Unstar##{session_id}"): registry.update_session_metadata( @@ -1233,32 +1243,43 @@ class App: if imgui.button("Exit Prior Session"): self.is_viewing_prior_session = False self.prior_session_entries.clear() + self.prior_disc_entries.clear() + self._comms_log_dirty = True imgui.separator() imgui.begin_child("prior_scroll", imgui.ImVec2(0, 0), False) clipper = imgui.ListClipper() - clipper.begin(len(self.prior_session_entries)) + clipper.begin(len(self.prior_disc_entries)) while clipper.step(): for idx in range(clipper.display_start, clipper.display_end): - entry = self.prior_session_entries[idx] - imgui.push_id(f"prior_{idx}") - kind = entry.get("kind", entry.get("type", "")) - imgui.text_colored(C_LBL, f"#{idx+1}") + entry = self.prior_disc_entries[idx] + imgui.push_id(f"prior_disc_{idx}") + collapsed = entry.get("collapsed", False) + if imgui.button("+" if collapsed else "-"): + entry["collapsed"] = not collapsed imgui.same_line() - ts = entry.get("ts", entry.get("timestamp", "")) + role = entry.get("role", "??") + ts = entry.get("ts", "") + imgui.text_colored(C_LBL, f"[{role}]") if ts: - imgui.text_colored(vec4(160, 160, 160), str(ts)) imgui.same_line() - imgui.text_colored(C_KEY, str(kind)) - payload = entry.get("payload", entry) - text = payload.get("text", payload.get("message", payload.get("content", ""))) - if text: - preview = str(text).replace("\n", " ")[:200] + imgui.text_colored(vec4(160, 160, 160), str(ts)) + + content = entry.get("content", "") + if collapsed: + imgui.same_line() + preview = content.replace("\n", " ")[:80] + if len(content) > 80: preview += "..." + imgui.text_colored(vec4(180, 180, 180), preview) + else: + imgui.begin_child(f"prior_content_{idx}", imgui.ImVec2(0, 150), True) if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(preview) + imgui.text_unformatted(content) imgui.pop_text_wrap_pos() else: - imgui.text(preview) + imgui.text_unformatted(content) + imgui.end_child() + imgui.separator() imgui.pop_id() imgui.end_child() @@ -2146,6 +2167,10 @@ class App: def _render_mma_dashboard(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard") + if self.is_viewing_prior_session: + imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard") + return # Task 5.3: Dense Summary Line track_name = self.active_track.description if self.active_track else "None" track_stats = {"percentage": 0.0, "completed": 0, "total": 0, "in_progress": 0, "blocked": 0, "todo": 0} @@ -2578,6 +2603,10 @@ class App: def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tier_stream_panel") + if self.is_viewing_prior_session: + imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tier_stream_panel") + return if stream_key is not None: content = self.mma_streams.get(stream_key, "") imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1))