chat_live_turns.py (90 dòng) là phần tinh tế nhất của khung chat: người dùng mở phiên khác rồi quay lại trong khi lượt cũ vẫn đang chạy. Phải nối vào đúng luồng đó và đúng danh sách tin nhắn đang sống, chứ không đọc bản trên đĩa (đã cũ) hay khởi động lại. Sai thì hoặc mất phần agent viết lúc mình vắng mặt, hoặc hai bên cùng ghi vào một file. Giờ Gamma không còn file production nào vượt 400 dòng. 756 test xanh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
4.4 KiB
Python
91 lines
4.4 KiB
Python
"""Nối lại lượt đang chạy khi người dùng quay về phiên cũ — R08-T06.
|
|
|
|
Phần tinh tế nhất của khung chat. Người dùng mở phiên khác rồi quay lại trong
|
|
khi lượt cũ VẪN đang chạy: phải nối vào đúng luồng đó và đúng danh sách tin
|
|
nhắn đang sống, chứ không được đọc bản trên đĩa (đã cũ) hay khởi động lại.
|
|
|
|
``_detach_live_turns`` gỡ ra khi rời phiên, ``_reattach_running_turn`` nối
|
|
lại khi quay về. Sai một trong hai thì hoặc mất phần agent viết trong lúc
|
|
vắng mặt, hoặc hai bên cùng ghi vào một file.
|
|
|
|
Cùng kiểu mixin, xem ghi chú ở đầu ``presentation/shell/nav_rail.py``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtWidgets import QMessageBox
|
|
from ...core.worker import AgentWorker
|
|
from ...i18n import tr
|
|
|
|
|
|
class ChatLiveTurnsMixin:
|
|
"""Nối lại lượt đang chạy. Trộn vào ChatPanel."""
|
|
|
|
def _detach_live_turns(self) -> None:
|
|
"""Before switching away from the current conversation, turn its running
|
|
turns into background jobs: they stop rendering into the (about-to-be-
|
|
cleared) transcript but keep running and save to their own conversation."""
|
|
for c in self._active.values():
|
|
if c.get("home_id") == self.session_id:
|
|
c["detached"] = True
|
|
c["assistant"] = None # its bubbles are about to be cleared
|
|
|
|
def _running_ctx_for(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
"""The in-progress turn's context for a conversation (one at a time), or None."""
|
|
for c in self._active.values():
|
|
if c.get("home_id") == session_id:
|
|
return c
|
|
return None
|
|
|
|
def _reattach_running_turn(self, ctx: Dict[str, Any]) -> None:
|
|
"""Re-render an in-progress turn into the current transcript and re-attach it
|
|
so it keeps streaming live — used when reopening a running conversation, so
|
|
the user sees the CURRENT task (message + steps so far + live plan), not just
|
|
the last saved state."""
|
|
record = ctx["record"]
|
|
record["bubbles"] = [] # the old bubbles were cleared on the view switch
|
|
# 1) the user's message that is being processed
|
|
ub = self.chat_view.add_user(ctx.get("display_text") or "(attachment)")
|
|
record["bubbles"].append(ub)
|
|
# 2) steps already completed this turn (assistant text / tool results); found
|
|
# by identity after the user message (a system prompt may sit before it).
|
|
# Snapshot the list — the worker thread may still be appending to it.
|
|
msgs = list(ctx.get("messages", []))
|
|
ui = next((i for i, m in enumerate(msgs) if m is ctx.get("user_msg")), -1)
|
|
for m in (msgs[ui + 1:] if ui >= 0 else []):
|
|
role = m.get("role")
|
|
if role == "assistant" and (m.get("content") or "").strip():
|
|
b = self.chat_view.add_assistant(self.assistant_title())
|
|
b.set_markdown(m["content"])
|
|
record["bubbles"].append(b)
|
|
elif role == "tool":
|
|
b = self.chat_view.add_tool(m.get("name", "tool"), m.get("content", ""), True)
|
|
record["bubbles"].append(b)
|
|
# 3) the live plan checklist (if any) — inline, expandable
|
|
steps = ctx.get("plan_steps") or []
|
|
if steps:
|
|
self.on_plan(steps)
|
|
from ...ui.chat_panel import _format_plan_steps
|
|
pb = self.chat_view.add_plan(_format_plan_steps(steps))
|
|
record["bubbles"].append(pb)
|
|
ctx["plan_bubble"] = pb
|
|
# 4) the partial answer of the step currently streaming — re-attach so new
|
|
# deltas keep appending to this bubble.
|
|
ctx["assistant"] = None
|
|
ctx["reasoning"] = None
|
|
if (ctx.get("partial") or "").strip():
|
|
ab = self.chat_view.add_assistant(self.assistant_title())
|
|
ab.set_markdown(ctx["partial"])
|
|
record["bubbles"].append(ab)
|
|
ctx["assistant"] = ab
|
|
# 5) live again → future events render here
|
|
ctx["detached"] = False
|
|
self.chat_view.scroll_to_bottom()
|
|
|
|
def running_session_ids(self):
|
|
"""Set of conversation ids that currently have a turn running (for the
|
|
History status markers)."""
|
|
return set(self._sessions_live)
|