Skip to content

Commit a2e4955

Browse files
authored
Merge pull request #2865 from core-power/feat/web-console-improvements
feat: message management and code block enhancements
2 parents fde4b6f + c62175c commit a2e4955

4 files changed

Lines changed: 748 additions & 11 deletions

File tree

agent/memory/conversation_store.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class ConversationStore:
291291

292292
def __init__(self, db_path: Path):
293293
self._db_path = db_path
294-
self._lock = threading.Lock()
294+
self._lock = threading.RLock() # Use RLock to allow reentrant locking
295295
self._init_db()
296296

297297
# ------------------------------------------------------------------
@@ -524,6 +524,109 @@ def clear_session(self, session_id: str) -> None:
524524
finally:
525525
conn.close()
526526

527+
def delete_message_pair(self, session_id: str, user_seq: int, delete_user: bool = True, cascade: bool = False) -> int:
528+
"""Delete a user message and/or its corresponding assistant reply.
529+
530+
The assistant reply is identified as all messages between user_seq
531+
and the next visible user message (or end of session).
532+
533+
Args:
534+
session_id: Session identifier.
535+
user_seq: The seq number of the user message.
536+
delete_user: If True (default), delete the user message too.
537+
If False, only delete assistant reply (for regenerate scenarios).
538+
cascade: If True, also delete all subsequent turns after this one.
539+
Used by edit-message which removes this turn and everything after.
540+
541+
Returns:
542+
Number of message rows deleted.
543+
"""
544+
with self._lock:
545+
conn = self._connect()
546+
try:
547+
with conn:
548+
# Verify this is a user message
549+
row = conn.execute(
550+
"SELECT role FROM messages WHERE session_id = ? AND seq = ?",
551+
(session_id, user_seq),
552+
).fetchone()
553+
if not row or row[0] != "user":
554+
return 0
555+
556+
if cascade:
557+
# Delete from this message to end of session
558+
start_seq = user_seq if delete_user else user_seq + 1
559+
end_seq_row = conn.execute(
560+
"SELECT MAX(seq) FROM messages WHERE session_id = ?",
561+
(session_id,),
562+
).fetchone()
563+
end_seq = (end_seq_row[0] or user_seq) + 1
564+
else:
565+
# Find the next visible user message seq (exclude tool_result)
566+
# Use batched query to avoid loading too many rows at once
567+
next_user_seq = None
568+
batch_size = 100
569+
offset = 0
570+
while True:
571+
batch = conn.execute(
572+
"""
573+
SELECT seq, content FROM messages
574+
WHERE session_id = ? AND seq > ? AND role = 'user'
575+
ORDER BY seq ASC
576+
LIMIT ? OFFSET ?
577+
""",
578+
(session_id, user_seq, batch_size, offset),
579+
).fetchall()
580+
if not batch:
581+
break
582+
for seq, content in batch:
583+
try:
584+
content_obj = json.loads(content)
585+
except Exception:
586+
content_obj = content
587+
if _is_visible_user_message(content_obj):
588+
next_user_seq = seq
589+
break
590+
if next_user_seq is not None:
591+
break
592+
offset += batch_size
593+
594+
# Determine the end boundary for deletion
595+
if next_user_seq is not None:
596+
end_seq = next_user_seq
597+
else:
598+
end_seq_row = conn.execute(
599+
"SELECT MAX(seq) FROM messages WHERE session_id = ?",
600+
(session_id,),
601+
).fetchone()
602+
end_seq = (end_seq_row[0] or user_seq) + 1
603+
604+
# Determine the start boundary for deletion
605+
start_seq = user_seq if delete_user else user_seq + 1
606+
607+
# Delete messages from start_seq to end_seq (exclusive)
608+
cur = conn.execute(
609+
"DELETE FROM messages WHERE session_id = ? AND seq >= ? AND seq < ?",
610+
(session_id, start_seq, end_seq),
611+
)
612+
deleted = cur.rowcount
613+
614+
# Update session msg_count
615+
conn.execute(
616+
"""
617+
UPDATE sessions
618+
SET msg_count = (
619+
SELECT COUNT(*) FROM messages WHERE session_id = ?
620+
)
621+
WHERE session_id = ?
622+
""",
623+
(session_id, session_id),
624+
)
625+
626+
return deleted
627+
finally:
628+
conn.close()
629+
527630
def prune_scheduled_messages(
528631
self,
529632
session_id: str,
@@ -1053,3 +1156,4 @@ def get_conversation_store() -> ConversationStore:
10531156
_store_instance = ConversationStore(db_path)
10541157
logger.debug(f"[ConversationStore] Using shared DB at: {db_path}")
10551158
return _store_instance
1159+

channel/web/static/css/console.css

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,3 +1399,171 @@
13991399
.agent-cancelled-tag {
14001400
font-style: italic;
14011401
}
1402+
1403+
/* =====================================================================
1404+
Code Block Enhancements
1405+
===================================================================== */
1406+
.code-block-wrapper {
1407+
position: relative;
1408+
margin: 1em 0;
1409+
border-radius: 8px;
1410+
overflow: hidden;
1411+
background: #f8f9fa;
1412+
border: 1px solid #e2e8f0;
1413+
}
1414+
1415+
.dark .code-block-wrapper {
1416+
background: #1e293b;
1417+
border-color: #334155;
1418+
}
1419+
1420+
.code-block-header {
1421+
display: flex;
1422+
justify-content: space-between;
1423+
align-items: center;
1424+
padding: 0.5em 1em;
1425+
background: #e2e8f0;
1426+
border-bottom: 1px solid #cbd5e1;
1427+
font-size: 0.85em;
1428+
}
1429+
1430+
.dark .code-block-header {
1431+
background: #0f172a;
1432+
border-bottom-color: #334155;
1433+
}
1434+
1435+
.code-block-lang {
1436+
color: #64748b;
1437+
font-weight: 500;
1438+
text-transform: lowercase;
1439+
}
1440+
1441+
.dark .code-block-lang {
1442+
color: #94a3b8;
1443+
}
1444+
1445+
.code-copy-btn {
1446+
background: transparent;
1447+
border: none;
1448+
color: #64748b;
1449+
cursor: pointer;
1450+
padding: 0.25em 0.5em;
1451+
border-radius: 4px;
1452+
transition: all 0.2s;
1453+
font-size: 0.9em;
1454+
}
1455+
1456+
.code-copy-btn:hover {
1457+
background: rgba(100, 116, 139, 0.1);
1458+
color: #475569;
1459+
}
1460+
1461+
.dark .code-copy-btn {
1462+
color: #94a3b8;
1463+
}
1464+
1465+
.dark .code-copy-btn:hover {
1466+
background: rgba(148, 163, 184, 0.1);
1467+
color: #cbd5e1;
1468+
}
1469+
1470+
.code-block-wrapper pre {
1471+
margin: 0;
1472+
border-radius: 0;
1473+
border: none;
1474+
}
1475+
1476+
/* =====================================================================
1477+
Drag and Drop Overlay
1478+
===================================================================== */
1479+
.drag-overlay {
1480+
position: absolute;
1481+
top: 0;
1482+
left: 0;
1483+
right: 0;
1484+
bottom: 0;
1485+
background: rgba(59, 130, 246, 0.1);
1486+
backdrop-filter: blur(2px);
1487+
display: flex;
1488+
align-items: center;
1489+
justify-content: center;
1490+
z-index: 9999;
1491+
pointer-events: none;
1492+
opacity: 0;
1493+
transition: opacity 0.2s;
1494+
}
1495+
1496+
.drag-overlay.active {
1497+
opacity: 1;
1498+
}
1499+
1500+
.drag-overlay.hidden {
1501+
display: none;
1502+
}
1503+
1504+
.drag-overlay-content {
1505+
background: white;
1506+
border: 3px dashed #3b82f6;
1507+
border-radius: 16px;
1508+
padding: 3em 4em;
1509+
text-align: center;
1510+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
1511+
animation: bounce 1s ease infinite;
1512+
}
1513+
1514+
.dark .drag-overlay-content {
1515+
background: #1e293b;
1516+
border-color: #60a5fa;
1517+
}
1518+
1519+
.drag-overlay-content i {
1520+
font-size: 4em;
1521+
color: #3b82f6;
1522+
margin-bottom: 0.5em;
1523+
}
1524+
1525+
.dark .drag-overlay-content i {
1526+
color: #60a5fa;
1527+
}
1528+
1529+
.drag-overlay-content p {
1530+
font-size: 1.5em;
1531+
font-weight: 600;
1532+
color: #1e293b;
1533+
margin: 0;
1534+
}
1535+
1536+
.dark .drag-overlay-content p {
1537+
color: #f1f5f9;
1538+
}
1539+
1540+
@keyframes bounce {
1541+
0%, 100% { transform: translateY(0); }
1542+
50% { transform: translateY(-10px); }
1543+
}
1544+
1545+
/* =====================================================================
1546+
Message Action Buttons
1547+
===================================================================== */
1548+
.edit-msg-btn,
1549+
.delete-msg-btn,
1550+
.regenerate-msg-btn {
1551+
opacity: 0;
1552+
transition: opacity 0.2s, color 0.2s;
1553+
}
1554+
1555+
.user-message-group:hover .edit-msg-btn,
1556+
.user-message-group:hover .delete-msg-btn,
1557+
.flex.gap-3:hover .regenerate-msg-btn,
1558+
.flex.gap-3:hover .delete-msg-btn {
1559+
opacity: 1;
1560+
}
1561+
1562+
.edit-msg-btn:hover,
1563+
.regenerate-msg-btn:hover {
1564+
color: #3b82f6 !important;
1565+
}
1566+
1567+
.delete-msg-btn:hover {
1568+
color: #ef4444 !important;
1569+
}

0 commit comments

Comments
 (0)