feat(webui): add tabbed pane workbench (#5322)

This commit is contained in:
chengyongru
2026-08-12 14:45:12 +08:00
committed by GitHub
parent 1656664a47
commit 4b5319b760
38 changed files with 5706 additions and 527 deletions
+15 -1
View File
@@ -530,6 +530,10 @@ class WebSocketChannel(BaseChannel):
except Exception as e:
self.logger.warning("failed to send {} event: {}", event, e)
async def _broadcast_webui_event(self, event: str, **fields: Any) -> None:
for connection in tuple(self._webui_connections):
await self._send_event(connection, event, **fields)
@classmethod
def default_config(cls) -> dict[str, Any]:
return WebSocketConfig().model_dump(by_alias=True)
@@ -848,7 +852,7 @@ class WebSocketChannel(BaseChannel):
)
return
try:
await asyncio.to_thread(
saved_state = await asyncio.to_thread(
write_webui_sidebar_state,
cast(dict[str, Any], state),
)
@@ -858,6 +862,11 @@ class WebSocketChannel(BaseChannel):
"error",
detail="invalid_sidebar_state",
)
return
await self._broadcast_webui_event(
"sidebar_state_updated",
state=saved_state,
)
return
if t == "set_workspace_scope":
cid = envelope.get("chat_id")
@@ -1207,6 +1216,11 @@ class WebSocketChannel(BaseChannel):
message="WebUI mutation returned an invalid response",
)
return
if action == "sidebar.update" and isinstance(result, dict):
await self._broadcast_webui_event(
"sidebar_state_updated",
state=result,
)
await self._send_webui_response(
connection,
request_id,
@@ -1037,6 +1037,63 @@ async def test_webui_persists_sidebar_state_larger_than_http_request_line(
}
@pytest.mark.asyncio
async def test_webui_sidebar_state_update_broadcasts_workbench_to_other_devices(
bus: MagicMock,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr("nanobot.config.paths.get_data_dir", lambda: tmp_path)
channel = _ch(bus)
source = AsyncMock()
source.request = SimpleNamespace(headers=Headers())
other_device = AsyncMock()
channel._webui_connections.update({source, other_device})
request_id = "sidebar-workbench-state"
await channel._dispatch_envelope(
source,
"webui-client",
{
"type": "webui_request",
"request_id": request_id,
"action": "sidebar.update",
"payload": {
"state": {
"workbench": {
"version": 1,
"tabs": {
"tab:websocket:a": {
"explicit": True,
"title": "Research",
"paneKeys": ["websocket:a", "websocket:b"],
"layoutPaneKeys": ["websocket:b", "websocket:a"],
"layout": "columns",
"splitRatios": [0.35],
}
},
}
}
},
},
)
await asyncio.gather(*tuple(channel._webui_request_tasks.values()))
event = json.loads(other_device.send.await_args.args[0])
assert event["event"] == "sidebar_state_updated"
assert event["state"]["workbench"]["tabs"]["tab:websocket:a"]["paneKeys"] == [
"websocket:a",
"websocket:b",
]
assert event["state"]["workbench"]["tabs"]["tab:websocket:a"]["layoutPaneKeys"] == [
"websocket:b",
"websocket:a",
]
assert event["state"]["workbench"]["tabs"]["tab:websocket:a"]["splitRatios"] == [
0.35
]
@pytest.mark.asyncio
async def test_client_cannot_self_assert_webui_quote_context(bus: MagicMock) -> None:
channel = _ch(bus)