feat(webui): add tabbed pane workbench (#5322)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user