feat(stapp): restore chat history bubbles after /continue N (#138)
When the user runs '/continue N' in stapp, the agent's in-memory context is restored, but the UI previously showed only a single '✅ restored' line — all prior chat bubbles were missing. This change parses the target session log and reconstructs the user/assistant message pairs into st.session_state.messages, so reopening a session feels like the conversation was never interrupted. * continue_cmd.py: add extract_ui_messages(path) - parses model_responses log into [{role, content}, ...] - groups multi-turn LLM calls (prompts whose text starts with the '### [WORKING MEMORY]' header) into a single assistant bubble, inserting the existing '**LLM Running (Turn N) ...**' marker so fold_turns() renders them as collapsible segments. - two small helpers (_user_text / _assistant_text) keep parsing local. * stapp.py: in the /continue branch, resolve the target log path BEFORE calling handle_frontend_command (which snapshots the current log and would otherwise shift list_sessions indices), then replace session_state.messages with the reconstructed history on success. Falls back to the previous behavior for bare /continue or failure. Co-authored-by: wjl2023 <wjl2023@users.noreply.github.com>
This commit is contained in:
@@ -214,6 +214,57 @@ def handle(agent, query, display_queue):
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
def _user_text(prompt_body):
|
||||||
|
"""User-typed text from a prompt JSON; '' if this is an agent auto-continuation."""
|
||||||
|
try: msg = json.loads(prompt_body)
|
||||||
|
except Exception: return ''
|
||||||
|
if not isinstance(msg, dict): return ''
|
||||||
|
for blk in msg.get('content', []) or []:
|
||||||
|
if isinstance(blk, dict) and blk.get('type') == 'text':
|
||||||
|
t = (blk.get('text') or '').strip()
|
||||||
|
if t and not t.startswith('### [WORKING MEMORY]'): return t
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _assistant_text(response_body):
|
||||||
|
"""Joined text from a response blocks repr; '' on parse failure."""
|
||||||
|
try: blocks = ast.literal_eval(response_body)
|
||||||
|
except Exception: return ''
|
||||||
|
if not isinstance(blocks, list): return ''
|
||||||
|
return '\n'.join(b['text'] for b in blocks
|
||||||
|
if isinstance(b, dict) and b.get('type') == 'text'
|
||||||
|
and isinstance(b.get('text'), str) and b['text'].strip())
|
||||||
|
|
||||||
|
|
||||||
|
_TURN_MARK = '**LLM Running (Turn {}) ...**\n\n'
|
||||||
|
|
||||||
|
|
||||||
|
def extract_ui_messages(path):
|
||||||
|
"""Parse a model_responses log into [{role, content}, ...] for UI replay.
|
||||||
|
|
||||||
|
Auto-continuation turns are folded into one assistant bubble with Turn markers,
|
||||||
|
matching live chat rendering via fold_turns().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(path, encoding='utf-8', errors='replace') as f: content = f.read()
|
||||||
|
except Exception: return []
|
||||||
|
|
||||||
|
rounds = [] # [(user_text, [turn_text, ...]), ...]
|
||||||
|
for prompt, response in _pairs(content):
|
||||||
|
user = _user_text(prompt)
|
||||||
|
if user or not rounds: rounds.append((user, []))
|
||||||
|
rounds[-1][1].append(_assistant_text(response))
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for user, turns in rounds:
|
||||||
|
if not user or not any(turns): continue
|
||||||
|
body = '\n\n'.join(t if i == 0 else _TURN_MARK.format(i + 1) + t
|
||||||
|
for i, t in enumerate(turns))
|
||||||
|
out += [{'role': 'user', 'content': user},
|
||||||
|
{'role': 'assistant', 'content': body}]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def handle_frontend_command(agent, query, exclude_pid=None):
|
def handle_frontend_command(agent, query, exclude_pid=None):
|
||||||
"""Frontend-friendly /continue entry that returns text directly."""
|
"""Frontend-friendly /continue entry that returns text directly."""
|
||||||
s = (query or '').strip()
|
s = (query or '').strip()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import streamlit as st
|
|||||||
import time, json, re, threading, queue
|
import time, json, re, threading, queue
|
||||||
from agentmain import GeneraticAgent
|
from agentmain import GeneraticAgent
|
||||||
import chatapp_common # activate /continue command (monkey patches GeneraticAgent)
|
import chatapp_common # activate /continue command (monkey patches GeneraticAgent)
|
||||||
from continue_cmd import handle_frontend_command, reset_conversation
|
from continue_cmd import handle_frontend_command, reset_conversation, list_sessions, extract_ui_messages
|
||||||
|
|
||||||
st.set_page_config(page_title="Cowork", layout="wide")
|
st.set_page_config(page_title="Cowork", layout="wide")
|
||||||
|
|
||||||
@@ -190,10 +190,19 @@ if prompt := st.chat_input("any task?"):
|
|||||||
st.session_state.messages = [{"role": "assistant", "content": reset_conversation(agent), "time": ts}]
|
st.session_state.messages = [{"role": "assistant", "content": reset_conversation(agent), "time": ts}]
|
||||||
_reset_and_rerun()
|
_reset_and_rerun()
|
||||||
if cmd.startswith("/continue"):
|
if cmd.startswith("/continue"):
|
||||||
st.session_state.messages = list(st.session_state.messages) + [
|
m = re.match(r'/continue\s+(\d+)\s*$', cmd.strip())
|
||||||
{"role": "user", "content": cmd, "time": ts},
|
sessions = list_sessions(exclude_pid=os.getpid()) if m else []
|
||||||
{"role": "assistant", "content": handle_frontend_command(agent, cmd), "time": ts},
|
idx = int(m.group(1)) - 1 if m else -1
|
||||||
]
|
# Resolve target path BEFORE handle (which snapshots current log, shifting indices).
|
||||||
|
target = sessions[idx][0] if 0 <= idx < len(sessions) else None
|
||||||
|
result = handle_frontend_command(agent, cmd)
|
||||||
|
history = extract_ui_messages(target) if target and result.startswith('✅') else None
|
||||||
|
tail = [{"role": "assistant", "content": result, "time": ts}]
|
||||||
|
if history:
|
||||||
|
st.session_state.messages = history + tail
|
||||||
|
else:
|
||||||
|
st.session_state.messages = list(st.session_state.messages) + \
|
||||||
|
[{"role": "user", "content": cmd, "time": ts}] + tail
|
||||||
_reset_and_rerun()
|
_reset_and_rerun()
|
||||||
st.session_state.messages.append({"role": "user", "content": prompt})
|
st.session_state.messages.append({"role": "user", "content": prompt})
|
||||||
if hasattr(agent, '_pet_req') and not prompt.startswith('/'): agent._pet_req('state=walk')
|
if hasattr(agent, '_pet_req') and not prompt.startswith('/'): agent._pet_req('state=walk')
|
||||||
|
|||||||
Reference in New Issue
Block a user