refactor: route file edit progress via channel capability

This commit is contained in:
chengyongru
2026-06-01 23:00:53 +08:00
committed by Xubin Ren
parent 8129c16b7d
commit 2f0e638bd1
6 changed files with 101 additions and 51 deletions
+2 -21
View File
@@ -44,32 +44,12 @@ def build_bus_progress_callback(
)
)
if msg.channel == "websocket":
async def _websocket_progress(
content: str,
*,
tool_hint: bool = False,
tool_events: list[dict[str, Any]] | None = None,
file_edit_events: list[dict[str, Any]] | None = None,
reasoning: bool = False,
reasoning_end: bool = False,
) -> None:
await _publish_progress(
content,
tool_hint=tool_hint,
tool_events=tool_events,
file_edit_events=file_edit_events,
reasoning=reasoning,
reasoning_end=reasoning_end,
)
return _websocket_progress
async def _bus_progress(
content: str,
*,
tool_hint: bool = False,
tool_events: list[dict[str, Any]] | None = None,
file_edit_events: list[dict[str, Any]] | None = None,
reasoning: bool = False,
reasoning_end: bool = False,
) -> None:
@@ -77,6 +57,7 @@ def build_bus_progress_callback(
content,
tool_hint=tool_hint,
tool_events=tool_events,
file_edit_events=file_edit_events,
reasoning=reasoning,
reasoning_end=reasoning_end,
)
+13
View File
@@ -155,6 +155,19 @@ class BaseChannel(ABC):
"""
return
async def send_file_edit_events(
self,
chat_id: str,
edits: list[dict[str, Any]],
metadata: dict[str, Any] | None = None,
) -> None:
"""Deliver structured live file-edit events.
Default is no-op. Channels with a rich activity surface can override
this to render editing progress without receiving empty text messages.
"""
return
async def send_reasoning(self, msg: OutboundMessage) -> None:
"""Deliver a complete reasoning block.
+7
View File
@@ -389,6 +389,13 @@ class ChannelManager:
# to a single delta + end pair so plugins only implement the
# streaming primitives.
await channel.send_reasoning(msg)
elif msg.metadata.get("_file_edit_events"):
edits = msg.metadata.get("_file_edit_events")
await channel.send_file_edit_events(
msg.chat_id,
edits if isinstance(edits, list) else [],
msg.metadata,
)
elif msg.metadata.get("_stream_delta") or msg.metadata.get("_stream_end"):
await channel.send_delta(msg.chat_id, msg.content, msg.metadata)
elif not msg.metadata.get("_streamed"):
+31 -15
View File
@@ -27,16 +27,16 @@ from websockets.exceptions import ConnectionClosed
from websockets.http11 import Request as WsRequest
from websockets.http11 import Response
from nanobot.security.workspace_access import (
WORKSPACE_SCOPE_METADATA_KEY,
WorkspaceScopeError,
)
from nanobot.bus.events import OUTBOUND_META_AGENT_UI, OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
from nanobot.command.builtin import builtin_command_palette
from nanobot.config.paths import get_media_dir, get_workspace_path
from nanobot.config.schema import Base
from nanobot.security.workspace_access import (
WORKSPACE_SCOPE_METADATA_KEY,
WorkspaceScopeError,
)
from nanobot.session.goal_state import goal_state_ws_blob
from nanobot.session.webui_turns import websocket_turn_wall_started_at
from nanobot.utils.media_decode import (
@@ -44,14 +44,14 @@ from nanobot.utils.media_decode import (
save_base64_data_url,
)
from nanobot.utils.subagent_channel_display import scrub_subagent_messages_for_channel
from nanobot.webui.settings_api import runtime_capabilities
from nanobot.webui.cli_apps_api import normalize_cli_app_mentions
from nanobot.webui.mcp_presets_api import normalize_mcp_preset_mentions
from nanobot.webui.media_api import (
serve_signed_media,
sign_media_path,
sign_or_stage_media_path,
)
from nanobot.webui.mcp_presets_api import normalize_mcp_preset_mentions
from nanobot.webui.settings_api import runtime_capabilities
from nanobot.webui.settings_routes import WebUISettingsRouter
from nanobot.webui.sidebar_state import (
read_webui_sidebar_state,
@@ -1687,15 +1687,12 @@ class WebSocketChannel(BaseChannel):
)
return
if msg.metadata.get("_file_edit_events"):
payload: dict[str, Any] = {
"event": "file_edit",
"chat_id": msg.chat_id,
"edits": msg.metadata["_file_edit_events"],
}
self._try_append_webui_transcript(msg.chat_id, payload)
raw = json.dumps(payload, ensure_ascii=False)
for connection in conns:
await self._safe_send_to(connection, raw, label=" ")
edits = msg.metadata.get("_file_edit_events")
await self.send_file_edit_events(
msg.chat_id,
edits if isinstance(edits, list) else [],
msg.metadata,
)
return
text = msg.content
wire_text = self._rewrite_local_markdown_images(text)
@@ -1787,6 +1784,25 @@ class WebSocketChannel(BaseChannel):
for connection in conns:
await self._safe_send_to(connection, raw, label=" reasoning_end ")
async def send_file_edit_events(
self,
chat_id: str,
edits: list[dict[str, Any]],
metadata: dict[str, Any] | None = None,
) -> None:
conns = list(self._subs.get(chat_id, ()))
if not conns:
return
payload: dict[str, Any] = {
"event": "file_edit",
"chat_id": chat_id,
"edits": edits,
}
self._try_append_webui_transcript(chat_id, payload)
raw = json.dumps(payload, ensure_ascii=False)
for connection in conns:
await self._safe_send_to(connection, raw, label=" file_edit ")
async def send_delta(
self,
chat_id: str,