|
38 | 38 | from .commands import find_command |
39 | 39 | from .diffrender import render_diff |
40 | 40 | from .models import Message |
41 | | -from .session_store import SessionStore, title_from_filename |
| 41 | +from .session_store import ( |
| 42 | + SessionStore, |
| 43 | + escape_role_headers, |
| 44 | + split_role_header, |
| 45 | + title_from_filename, |
| 46 | + unescape_role_header, |
| 47 | +) |
42 | 48 |
|
43 | 49 | SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] |
44 | 50 |
|
@@ -1345,7 +1351,8 @@ def _conversation_text(self) -> str: |
1345 | 1351 | msgs = self.session.last_messages or [] |
1346 | 1352 | parts = [] |
1347 | 1353 | for m in msgs: |
1348 | | - body = m.text() |
| 1354 | + # escaped: see session_store.escape_role_headers |
| 1355 | + body = escape_role_headers(m.text()) |
1349 | 1356 | if body: |
1350 | 1357 | parts.append(f"**{m.role}**: {body}") |
1351 | 1358 | return "\n\n".join(parts) |
@@ -1462,27 +1469,32 @@ def _parse_saved_body(body: str) -> list[Message]: |
1462 | 1469 | ``tool_calls``). The following assistant reply already |
1463 | 1470 | summarizes the results, so dropping them loses no essential |
1464 | 1471 | context. |
| 1472 | +
|
| 1473 | + Body lines that merely look like a block header are escaped by |
| 1474 | + the renderer (see `escape_role_headers`) and unescaped here, so |
| 1475 | + a message quoting this format no longer splits into extra |
| 1476 | + messages. Sessions saved before escaping existed can still |
| 1477 | + split — that ambiguity is in the file, not in this parser. |
1465 | 1478 | """ |
1466 | 1479 | messages: list[Message] = [] |
1467 | 1480 | current_role: str | None = None |
1468 | 1481 | current_lines: list[str] = [] |
1469 | 1482 |
|
1470 | 1483 | for line in body.splitlines(): |
1471 | 1484 | # Check for a role header: **user**: ... or **assistant**: ... |
1472 | | - if line.startswith("**") and "**: " in line: |
1473 | | - prefix, _, rest = line.partition("**: ") |
1474 | | - role = prefix.strip("*").strip() |
1475 | | - if role in ("user", "assistant", "system", "tool"): |
1476 | | - # Save the previous block (tool blocks are dropped: |
1477 | | - # their tool_call_id/name were not persisted) |
1478 | | - if current_role is not None and current_role != "tool": |
1479 | | - content = "\n".join(current_lines).strip() |
1480 | | - if content: |
1481 | | - messages.append(Message(role=current_role, content=content)) |
1482 | | - current_role = role |
1483 | | - current_lines = [rest] |
1484 | | - continue |
1485 | | - current_lines.append(line) |
| 1485 | + header = split_role_header(line) |
| 1486 | + if header is not None: |
| 1487 | + role, rest = header |
| 1488 | + # Save the previous block (tool blocks are dropped: |
| 1489 | + # their tool_call_id/name were not persisted) |
| 1490 | + if current_role is not None and current_role != "tool": |
| 1491 | + content = "\n".join(current_lines).strip() |
| 1492 | + if content: |
| 1493 | + messages.append(Message(role=current_role, content=content)) |
| 1494 | + current_role = role |
| 1495 | + current_lines = [unescape_role_header(rest)] |
| 1496 | + continue |
| 1497 | + current_lines.append(unescape_role_header(line)) |
1486 | 1498 |
|
1487 | 1499 | # Don't forget the last block (tool blocks are dropped) |
1488 | 1500 | if current_role is not None and current_role != "tool": |
|
0 commit comments