From 59128f14a3b6021782e2e0b8cfda4b10b5e0ffe8 Mon Sep 17 00:00:00 2001
From: yanghang <2390375302@qq.com>
Date: Tue, 2 Jun 2026 19:19:55 +0800
Subject: [PATCH] fix(continue): robust session preview for malformed logs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
/continue list previews showed JSON debris (e.g. `{`, `" } ]} === Response ===`)
for some sessions. Two causes in _preview_from_file:
1. Models sometimes emit an unclosed , so the non-greedy DOTALL regex
pairs it with a far-away and captures === block headers / JSON
across rounds. Now only the latest is used, and it is rejected if
it looks like such a cross-segment capture (contains `=== `/`"role"` or
>200 chars) — falling through instead of digging for older summaries.
2. The fallback scanned for the first non-=== line, returning JSON structure
lines. It now uses the last real user prompt via a new _last_user(), which
scans Prompt blocks directly (so response-less/aborted sessions still
preview) and reuses _user_text() to skip tool_result continuations and all
_INJECT_MARKERS (WORKING MEMORY / SYSTEM TIPS / DANGER ...).
Scanned 261 local logs: garbage previews 26 -> 0.
---
frontends/continue_cmd.py | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/frontends/continue_cmd.py b/frontends/continue_cmd.py
index 260a8c34..f4d53370 100644
--- a/frontends/continue_cmd.py
+++ b/frontends/continue_cmd.py
@@ -46,6 +46,20 @@ def _first_user(pairs):
return ''
+def _last_user(text):
+ """Last real user prompt. Scans `=== Prompt ===` blocks directly (no
+ Prompt/Response pairing, so response-less/aborted sessions still preview),
+ newest-first, returning the first one `_user_text` accepts (it drops
+ tool_result continuations + all _INJECT_MARKERS). Better preview anchor than
+ the first prompt — reflects what the session was most recently about."""
+ for label, body in reversed(_BLOCK_RE.findall(text or '')):
+ if label == 'Prompt':
+ t = _user_text(body)
+ if t:
+ return t
+ return ''
+
+
def _last_summary(pairs):
for _, response_body in reversed(pairs):
try:
@@ -173,12 +187,20 @@ def _preview_from_file(path):
fh.seek(-_PREVIEW_WIN, 2); tail = fh.read()
except OSError: return ''
tail_s = tail.decode('utf-8', errors='replace')
- m = _SUMMARY_RE.findall(tail_s)
- if m: return m[-1].strip()
- for line in head.decode('utf-8', errors='replace').splitlines():
- s = line.strip()
- if s and not s.startswith('===') and not s.startswith('###') and '' not in s:
+ # Use only the latest , and reject it if dirty. Models sometimes emit
+ # an unclosed , so the non-greedy DOTALL match pairs it with a far-away
+ # and swallows === block headers / JSON across rounds. Treat such a
+ # match as invalid and fall through to the last user prompt (don't dig older ones).
+ cands = _SUMMARY_RE.findall(tail_s)
+ if cands:
+ s = ' '.join(cands[-1].split())
+ if s and '=== ' not in s and '"role"' not in s and len(s) <= 200:
return s
+ # Summary invalid/absent -> last real user prompt (JSON-aware, skips anchors;
+ # scans Prompt blocks directly so response-less sessions still preview).
+ lu = _last_user(tail_s) or _last_user(head.decode('utf-8', errors='replace'))
+ if lu:
+ return ' '.join(lu.split())[:120]
return ''