feat(logs): Implement session restoration and historical replay mode
This commit is contained in:
@@ -287,6 +287,9 @@ class AppController:
|
|||||||
self._tier_stream_last_len: Dict[str, int] = {}
|
self._tier_stream_last_len: Dict[str, int] = {}
|
||||||
self.is_viewing_prior_session: bool = False
|
self.is_viewing_prior_session: bool = False
|
||||||
self.prior_session_entries: List[Dict[str, Any]] = []
|
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.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
|
||||||
self.ui_manual_approve: bool = False
|
self.ui_manual_approve: bool = False
|
||||||
# Injection state
|
# Injection state
|
||||||
@@ -804,32 +807,79 @@ class AppController:
|
|||||||
label = self.project.get("project", {}).get("name", "")
|
label = self.project.get("project", {}).get("name", "")
|
||||||
session_logger.open_session(label=label)
|
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()
|
root = hide_tk_root()
|
||||||
path = filedialog.askopenfilename(
|
if path is None:
|
||||||
title="Load Session Log",
|
path = filedialog.askdirectory(
|
||||||
initialdir=str(paths.get_logs_dir()),
|
title="Select Session Directory",
|
||||||
filetypes=[("Log/JSONL", "*.log *.jsonl"), ("All Files", "*.*")]
|
initialdir=str(paths.get_logs_dir())
|
||||||
)
|
)
|
||||||
root.destroy()
|
root.destroy()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
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 = []
|
entries = []
|
||||||
|
disc_entries = []
|
||||||
try:
|
try:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(log_file, "r", encoding="utf-8") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line:
|
if line:
|
||||||
try:
|
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:
|
except json.JSONDecodeError:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._set_status(f"log load error: {e}")
|
self._set_status(f"log load error: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.prior_session_entries = entries
|
self.prior_session_entries = 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: {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:
|
def cb_prune_logs(self) -> None:
|
||||||
"""Manually triggers the log pruning process with aggressive thresholds."""
|
"""Manually triggers the log pruning process with aggressive thresholds."""
|
||||||
|
|||||||
57
src/gui_2.py
57
src/gui_2.py
@@ -257,6 +257,8 @@ class App:
|
|||||||
|
|
||||||
def _gui_func(self) -> None:
|
def _gui_func(self) -> None:
|
||||||
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:
|
||||||
|
imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20))
|
||||||
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
|
||||||
@@ -832,6 +834,11 @@ class App:
|
|||||||
print(f"ERROR in _gui_func: {e}")
|
print(f"ERROR in _gui_func: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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:
|
def _render_projects_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")
|
||||||
@@ -1093,6 +1100,9 @@ class App:
|
|||||||
imgui.table_next_column()
|
imgui.table_next_column()
|
||||||
imgui.text(str(metadata.get("message_count", "")))
|
imgui.text(str(metadata.get("message_count", "")))
|
||||||
imgui.table_next_column()
|
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 whitelisted:
|
||||||
if imgui.button(f"Unstar##{session_id}"):
|
if imgui.button(f"Unstar##{session_id}"):
|
||||||
registry.update_session_metadata(
|
registry.update_session_metadata(
|
||||||
@@ -1233,32 +1243,43 @@ class App:
|
|||||||
if imgui.button("Exit Prior Session"):
|
if imgui.button("Exit Prior Session"):
|
||||||
self.is_viewing_prior_session = False
|
self.is_viewing_prior_session = False
|
||||||
self.prior_session_entries.clear()
|
self.prior_session_entries.clear()
|
||||||
|
self.prior_disc_entries.clear()
|
||||||
|
self._comms_log_dirty = True
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.begin_child("prior_scroll", imgui.ImVec2(0, 0), False)
|
imgui.begin_child("prior_scroll", imgui.ImVec2(0, 0), False)
|
||||||
clipper = imgui.ListClipper()
|
clipper = imgui.ListClipper()
|
||||||
clipper.begin(len(self.prior_session_entries))
|
clipper.begin(len(self.prior_disc_entries))
|
||||||
while clipper.step():
|
while clipper.step():
|
||||||
for idx in range(clipper.display_start, clipper.display_end):
|
for idx in range(clipper.display_start, clipper.display_end):
|
||||||
entry = self.prior_session_entries[idx]
|
entry = self.prior_disc_entries[idx]
|
||||||
imgui.push_id(f"prior_{idx}")
|
imgui.push_id(f"prior_disc_{idx}")
|
||||||
kind = entry.get("kind", entry.get("type", ""))
|
collapsed = entry.get("collapsed", False)
|
||||||
imgui.text_colored(C_LBL, f"#{idx+1}")
|
if imgui.button("+" if collapsed else "-"):
|
||||||
|
entry["collapsed"] = not collapsed
|
||||||
imgui.same_line()
|
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:
|
if ts:
|
||||||
imgui.text_colored(vec4(160, 160, 160), str(ts))
|
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(C_KEY, str(kind))
|
imgui.text_colored(vec4(160, 160, 160), str(ts))
|
||||||
payload = entry.get("payload", entry)
|
|
||||||
text = payload.get("text", payload.get("message", payload.get("content", "")))
|
content = entry.get("content", "")
|
||||||
if text:
|
if collapsed:
|
||||||
preview = str(text).replace("\n", " ")[:200]
|
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:
|
if self.ui_word_wrap:
|
||||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||||
imgui.text(preview)
|
imgui.text_unformatted(content)
|
||||||
imgui.pop_text_wrap_pos()
|
imgui.pop_text_wrap_pos()
|
||||||
else:
|
else:
|
||||||
imgui.text(preview)
|
imgui.text_unformatted(content)
|
||||||
|
imgui.end_child()
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
@@ -2146,6 +2167,10 @@ class App:
|
|||||||
|
|
||||||
def _render_mma_dashboard(self) -> None:
|
def _render_mma_dashboard(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard")
|
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
|
# Task 5.3: Dense Summary Line
|
||||||
track_name = self.active_track.description if self.active_track else "None"
|
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}
|
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:
|
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.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:
|
if stream_key is not None:
|
||||||
content = self.mma_streams.get(stream_key, "")
|
content = self.mma_streams.get(stream_key, "")
|
||||||
imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1))
|
imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1))
|
||||||
|
|||||||
Reference in New Issue
Block a user