feat(desktop): polish desktop shell and shared WebUI surfaces (#4195)
* feat(desktop): add native host scaffold * feat(webui): track turns and usage in gateway * feat(webui): polish desktop chat experience * feat(apps): add ArcGIS and Joplin logos * feat(desktop): polish shell and shared surfaces * fix(webui): avoid preview chips for glob references * test: align CI expectations for token fallback * feat(webui): preview prompt rail entries * feat(webui): add prompt navigator drawer * style(webui): refine prompt navigator placement * style(webui): align prompt navigator with header actions * style(webui): simplify prompt navigator header * refactor(webui): clean thread resource refresh * feat(desktop): add native reply notifications * fix(webui): preserve desktop restart and replay state * fix(desktop): harden gateway proxy startup * fix(web): fall back when readability is unavailable * fix(desktop): hide window instead of closing on macos * fix(webui): unify desktop header actions * fix(webui): simplify prompt history rows * fix(desktop): log notification delivery failures * chore(desktop): clean source package artifacts * fix(cron): support one-time relative reminders * fix(webui): reveal scroll button in place * Revert "fix(cron): support one-time relative reminders" This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b. * refactor(webui): extract token usage heatmap * docs(desktop): clarify contributor guides --------- Co-authored-by: chengyongru <2755839590@qq.com>
This commit is contained in:
+118
-4
@@ -9,17 +9,37 @@ import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from nanobot.bus.events import OutboundMessage
|
||||
from nanobot.cli.commands import app
|
||||
from nanobot.providers.factory import make_provider
|
||||
from nanobot.cli.commands import _proactive_delivery_metadata, app
|
||||
from nanobot.config.schema import Config
|
||||
from nanobot.cron.types import CronJob, CronPayload
|
||||
from nanobot.providers.factory import ProviderSnapshot
|
||||
from nanobot.providers.factory import ProviderSnapshot, make_provider
|
||||
from nanobot.providers.openai_codex_provider import _strip_model_prefix
|
||||
from nanobot.providers.registry import find_by_name
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
|
||||
metadata = {
|
||||
"webui": True,
|
||||
"webui_turn_id": "turn-that-created-the-reminder",
|
||||
"workspace_scope": {"mode": "default"},
|
||||
}
|
||||
|
||||
out = _proactive_delivery_metadata(
|
||||
"websocket",
|
||||
metadata,
|
||||
turn_seed="cron:drink-water",
|
||||
source_label="drink water",
|
||||
)
|
||||
|
||||
assert out["webui"] is True
|
||||
assert out["workspace_scope"] == {"mode": "default"}
|
||||
assert out["webui_turn_id"].startswith("cron:drink-water:")
|
||||
assert out["webui_turn_id"] != metadata["webui_turn_id"]
|
||||
assert out["_webui_message_source"] == {"kind": "cron", "label": "drink water"}
|
||||
|
||||
|
||||
def _fake_provider():
|
||||
"""Return a minimal fake provider that satisfies AgentLoop.__init__."""
|
||||
p = MagicMock()
|
||||
@@ -542,8 +562,8 @@ def test_openai_compat_provider_passes_model_through():
|
||||
|
||||
|
||||
def test_make_provider_uses_github_copilot_backend():
|
||||
from nanobot.providers.factory import make_provider
|
||||
from nanobot.config.schema import Config
|
||||
from nanobot.providers.factory import make_provider
|
||||
|
||||
config = Config.model_validate(
|
||||
{
|
||||
@@ -1317,6 +1337,41 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context(
|
||||
}
|
||||
]
|
||||
|
||||
bus.publish_outbound.reset_mock()
|
||||
old_turn_id = "turn-that-created-the-reminder"
|
||||
websocket_job = CronJob(
|
||||
id="drink-water",
|
||||
name="drink water",
|
||||
payload=CronPayload(
|
||||
message="Remind me to drink water.",
|
||||
deliver=True,
|
||||
channel="websocket",
|
||||
to="chat-1",
|
||||
channel_meta={
|
||||
"webui": True,
|
||||
"webui_turn_id": old_turn_id,
|
||||
"workspace_scope": {"mode": "default"},
|
||||
},
|
||||
session_key="websocket:chat-1",
|
||||
),
|
||||
)
|
||||
|
||||
response = asyncio.run(cron.on_job(websocket_job))
|
||||
|
||||
assert response == "Time to stretch."
|
||||
bus.publish_outbound.assert_awaited_once()
|
||||
delivered = bus.publish_outbound.await_args.args[0]
|
||||
assert delivered.channel == "websocket"
|
||||
assert delivered.chat_id == "chat-1"
|
||||
assert delivered.metadata["webui"] is True
|
||||
assert delivered.metadata["workspace_scope"] == {"mode": "default"}
|
||||
assert delivered.metadata["webui_turn_id"].startswith("cron:drink-water:")
|
||||
assert delivered.metadata["webui_turn_id"] != old_turn_id
|
||||
assert delivered.metadata["_webui_message_source"] == {
|
||||
"kind": "cron",
|
||||
"label": "drink water",
|
||||
}
|
||||
|
||||
|
||||
def test_gateway_cron_job_suppresses_intermediate_progress(
|
||||
monkeypatch, tmp_path: Path
|
||||
@@ -1599,6 +1654,65 @@ def test_configure_desktop_gateway_forces_local_websocket_only() -> None:
|
||||
assert extras["websocket"]["websocket_requires_token"] is True
|
||||
|
||||
|
||||
def test_load_or_create_desktop_config_bootstraps_without_api_key(tmp_path: Path) -> None:
|
||||
from nanobot.cli.commands import _load_or_create_desktop_config
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
loaded = _load_or_create_desktop_config(
|
||||
str(config_path),
|
||||
str(tmp_path / "workspace"),
|
||||
)
|
||||
|
||||
assert loaded.agents.defaults.provider == "openai_codex"
|
||||
assert loaded.agents.defaults.model == "openai-codex/gpt-5.1-codex"
|
||||
assert loaded.agents.defaults.model_preset is None
|
||||
assert config_path.exists()
|
||||
saved = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["agents"]["defaults"]["provider"] == ""
|
||||
assert saved["agents"]["defaults"]["model"] == ""
|
||||
assert make_provider(loaded).get_default_model() == "openai-codex/gpt-5.1-codex"
|
||||
|
||||
|
||||
def test_load_or_create_desktop_config_repairs_existing_unconfigured_default(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from nanobot.cli.commands import _load_or_create_desktop_config
|
||||
from nanobot.config.loader import save_config
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
save_config(Config(), config_path)
|
||||
|
||||
loaded = _load_or_create_desktop_config(str(config_path), None)
|
||||
saved = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert loaded.agents.defaults.provider == "openai_codex"
|
||||
assert loaded.agents.defaults.model == "openai-codex/gpt-5.1-codex"
|
||||
assert saved["agents"]["defaults"]["provider"] == ""
|
||||
assert saved["agents"]["defaults"]["model"] == ""
|
||||
assert make_provider(loaded).get_default_model() == "openai-codex/gpt-5.1-codex"
|
||||
|
||||
|
||||
def test_load_or_create_desktop_config_unwinds_persisted_bootstrap(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from nanobot.cli.commands import _load_or_create_desktop_config
|
||||
from nanobot.config.loader import save_config
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
config = Config()
|
||||
config.agents.defaults.provider = "openai_codex"
|
||||
config.agents.defaults.model = "openai-codex/gpt-5.1-codex"
|
||||
save_config(config, config_path)
|
||||
|
||||
loaded = _load_or_create_desktop_config(str(config_path), None)
|
||||
saved = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert loaded.agents.defaults.provider == "openai_codex"
|
||||
assert loaded.agents.defaults.model == "openai-codex/gpt-5.1-codex"
|
||||
assert saved["agents"]["defaults"]["provider"] == ""
|
||||
assert saved["agents"]["defaults"]["model"] == ""
|
||||
|
||||
|
||||
def test_gateway_health_endpoint_binds_and_serves_expected_responses(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user