From 113e8d67ad69115fc9d5175b4b23b041e033a1a3 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Sat, 8 Aug 2026 19:52:23 +0800 Subject: [PATCH] refactor: remove verified dead code --- nanobot/agent/automation_turns.py | 7 -- nanobot/agent/tools/filesystem.py | 16 ---- nanobot/agent/tools/path_utils.py | 10 +- nanobot/apps/cli/utils.py | 9 -- nanobot/channels/feishu/runtime.py | 9 -- nanobot/channels/mattermost/runtime.py | 8 -- nanobot/channels/msteams/runtime.py | 5 - .../channels/msteams/tests/test_msteams.py | 9 +- nanobot/channels/websocket/runtime.py | 16 ---- .../websocket/tests/test_websocket_channel.py | 31 ++---- .../tests/test_websocket_media_route.py | 31 +++--- nanobot/cron/service.py | 7 -- nanobot/cron/session_turns.py | 6 -- nanobot/session/manager.py | 4 - nanobot/triggers/local_session_turns.py | 11 --- nanobot/utils/document.py | 29 ------ nanobot/utils/file_edit_events.py | 18 ---- nanobot/utils/gitstore.py | 58 +----------- nanobot/utils/helpers.py | 12 --- nanobot/webui/media_gateway.py | 8 -- nanobot/webui/transcript.py | 29 ------ tests/agent/test_git_store.py | 13 --- tests/agent/test_runner_injections.py | 13 ++- tests/cli_apps/test_utils.py | 29 +++--- tests/test_document_parsing.py | 26 ------ tests/tools/test_edit_advanced.py | 37 +------- tests/tools/test_filesystem_tools.py | 47 ---------- tests/utils/test_file_edit_events.py | 27 +++--- tests/utils/test_gitstore.py | 88 +----------------- tests/utils/test_helpers.py | 8 -- webui/bun.lock | 5 - webui/package.json | 1 - webui/public/brand/nanobot_icon.png | Bin 7365 -> 0 bytes webui/public/brand/nanobot_logo.png | Bin 28453 -> 0 bytes webui/public/brand/nanobot_logo.webp | Bin 11938 -> 0 bytes .../src/components/thread/promptNavigation.ts | 10 -- webui/src/components/ui/dropdown-menu.tsx | 74 +-------------- webui/src/components/ui/separator.tsx | 29 ------ webui/src/globals.css | 51 ---------- webui/src/hooks/useAttachedImages.ts | 1 - 40 files changed, 76 insertions(+), 716 deletions(-) delete mode 100644 webui/public/brand/nanobot_icon.png delete mode 100644 webui/public/brand/nanobot_logo.png delete mode 100644 webui/public/brand/nanobot_logo.webp delete mode 100644 webui/src/components/ui/separator.tsx diff --git a/nanobot/agent/automation_turns.py b/nanobot/agent/automation_turns.py index c16168c48..072223b7e 100644 --- a/nanobot/agent/automation_turns.py +++ b/nanobot/agent/automation_turns.py @@ -140,10 +140,3 @@ class AutomationTurnCoordinator: if pending_id: pending_ids.add(pending_id) return pending_ids - - async def publish_next_deferred(self, session_key: str) -> bool: - return await publish_next_deferred_turn( - deferred_queues=self.deferred_queues, - publish_inbound=self._publish_inbound, - session_key=session_key, - ) diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index d1406604f..ac52ca868 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -785,22 +785,6 @@ def _best_window(old_text: str, content: str) -> tuple[float, int, list[str], li return best_ratio, best_start, best_window_lines, hints -def _find_match(content: str, old_text: str) -> tuple[str | None, int]: - """Locate old_text in content with a multi-level fallback chain: - - 1. Exact substring match - 2. Line-trimmed sliding window (handles indentation differences) - 3. Smart quote normalization (curly ↔ straight quotes) - - Both inputs should use LF line endings (caller normalises CRLF). - Returns (matched_fragment, count) or (None, 0). - """ - matches = _find_matches(content, old_text) - if not matches: - return None, 0 - return matches[0].text, len(matches) - - @tool_parameters( tool_parameters_schema( path=StringSchema("The file path to edit"), diff --git a/nanobot/agent/tools/path_utils.py b/nanobot/agent/tools/path_utils.py index ca3f10e74..3f111e147 100644 --- a/nanobot/agent/tools/path_utils.py +++ b/nanobot/agent/tools/path_utils.py @@ -3,15 +3,7 @@ from pathlib import Path from nanobot.config.paths import get_media_dir -from nanobot.security.workspace_policy import ( - is_path_within, - resolve_allowed_path, -) - - -def is_under(path: Path, directory: Path) -> bool: - """Return True when path resolves under directory.""" - return is_path_within(path, directory) +from nanobot.security.workspace_policy import resolve_allowed_path def resolve_workspace_path( diff --git a/nanobot/apps/cli/utils.py b/nanobot/apps/cli/utils.py index 5668a486d..c48bb56b5 100644 --- a/nanobot/apps/cli/utils.py +++ b/nanobot/apps/cli/utils.py @@ -12,15 +12,6 @@ def session_extra(metadata: Mapping[str, Any] | None) -> dict[str, Any]: return {"cli_apps": cli_apps} if isinstance(cli_apps, list) and cli_apps else {} -def runtime_lines(message: Any, workspace: Path, *, skip: bool = False) -> list[str]: - """Return model-visible CLI app annotations for the current turn.""" - if skip: - return [] - text = message.content if isinstance(getattr(message, "content", None), str) else "" - metadata = message.metadata if isinstance(getattr(message, "metadata", None), Mapping) else None - return runtime_lines_for_request(text, metadata, workspace) - - def runtime_lines_for_request( text: str, metadata: Mapping[str, Any] | None, diff --git a/nanobot/channels/feishu/runtime.py b/nanobot/channels/feishu/runtime.py index 96ad3a328..23bbb1569 100644 --- a/nanobot/channels/feishu/runtime.py +++ b/nanobot/channels/feishu/runtime.py @@ -470,15 +470,6 @@ def _extract_post_content(content_json: dict[str, Any]) -> tuple[str, list[str]] return "", [] -def _extract_post_text(content_json: dict[str, Any]) -> str: # pyright: ignore[reportUnusedFunction] - """Extract plain text from Feishu post (rich text) message content. - - Legacy wrapper for _extract_post_content, returns only text. - """ - text, _ = _extract_post_content(content_json) - return text - - # ============================================================================= # QR scan-to-create onboarding # diff --git a/nanobot/channels/mattermost/runtime.py b/nanobot/channels/mattermost/runtime.py index 6cd32fce3..dc58e7698 100644 --- a/nanobot/channels/mattermost/runtime.py +++ b/nanobot/channels/mattermost/runtime.py @@ -658,11 +658,6 @@ class MattermostChannel(BaseChannel): resp.raise_for_status() return cast(dict[str, Any], resp.json()) - async def _api_put(self, path: str, json_data: dict[str, Any]) -> dict[str, Any]: - resp = await self._require_http_client().put(path, json=json_data) - resp.raise_for_status() - return cast(dict[str, Any], resp.json()) - async def _create_post( self, channel_id: str, @@ -681,9 +676,6 @@ class MattermostChannel(BaseChannel): body["file_ids"] = file_ids return await self._api_post("/api/v4/posts", body) - async def _edit_post(self, post_id: str, message: str) -> dict[str, Any]: - return await self._api_put(f"/api/v4/posts/{post_id}", {"id": post_id, "message": message}) - async def _upload_file(self, channel_id: str, file_path: str) -> str | None: path = Path(file_path) if not path.exists(): diff --git a/nanobot/channels/msteams/runtime.py b/nanobot/channels/msteams/runtime.py index e040092e5..25af38bed 100644 --- a/nanobot/channels/msteams/runtime.py +++ b/nanobot/channels/msteams/runtime.py @@ -811,11 +811,6 @@ class MSTeamsChannel(BaseChannel): except Exception as e: self.logger.warning("Failed to save conversation refs: {}", e) - def _save_refs(self, *, prune: bool = True) -> None: - """Persist conversation references.""" - with self._refs_guard: - self._save_refs_locked(prune=prune) - async def _get_access_token(self) -> str: """Fetch an access token for Bot Framework / Azure Bot auth.""" diff --git a/nanobot/channels/msteams/tests/test_msteams.py b/nanobot/channels/msteams/tests/test_msteams.py index cb3fa9ace..7fdef347f 100644 --- a/nanobot/channels/msteams/tests/test_msteams.py +++ b/nanobot/channels/msteams/tests/test_msteams.py @@ -228,7 +228,8 @@ def test_save_prunes_unsupported_conversation_refs(make_channel, tmp_path, monke ), } - ch._save_refs() + with ch._refs_guard: + ch._save_refs_locked() assert set(ch._conversation_refs.keys()) == {"conv-valid"} @@ -378,7 +379,8 @@ def test_save_uses_atomic_replace_and_keeps_existing_file_on_replace_error(make_ raise OSError("replace failed") monkeypatch.setattr(msteams_module.os, "replace", _raise_replace) - ch._save_refs() + with ch._refs_guard: + ch._save_refs_locked() persisted = json.loads(refs_path.read_text(encoding="utf-8")) assert set(persisted.keys()) == {"conv-old"} @@ -934,7 +936,8 @@ def test_save_refs_prunes_webchat_and_stale_refs(make_channel): ), } - ch._save_refs() + with ch._refs_guard: + ch._save_refs_locked() assert set(ch._conversation_refs) == {"teams-good"} saved = json.loads(ch._refs_path.read_text(encoding="utf-8")) diff --git a/nanobot/channels/websocket/runtime.py b/nanobot/channels/websocket/runtime.py index dc909ed72..3db23bcbb 100644 --- a/nanobot/channels/websocket/runtime.py +++ b/nanobot/channels/websocket/runtime.py @@ -30,7 +30,6 @@ from nanobot.bus.outbound_events import ( TurnEndEvent, TurnModelUpdatedEvent, outbound_event_from_message, - outbound_message_for_event, ) from nanobot.bus.queue import MessageBus from nanobot.channels.base import BaseChannel @@ -280,21 +279,6 @@ class WebSocketConfig(Base): ) -def publish_runtime_model_update( - bus: MessageBus, - model: str, - model_preset: str | None, -) -> None: - """Enqueue a runtime model snapshot for websocket subscribers (fan-out in-channel).""" - bus.outbound.put_nowait( - outbound_message_for_event( - channel="websocket", - chat_id="*", - event=RuntimeModelUpdatedEvent(model=model, model_preset=model_preset), - ) - ) - - def _parse_inbound_payload(raw: str) -> str | None: """Parse a client frame into text; return None for empty or unrecognized content.""" text = raw.strip() diff --git a/nanobot/channels/websocket/tests/test_websocket_channel.py b/nanobot/channels/websocket/tests/test_websocket_channel.py index 2e265bd6b..70b781abd 100644 --- a/nanobot/channels/websocket/tests/test_websocket_channel.py +++ b/nanobot/channels/websocket/tests/test_websocket_channel.py @@ -32,7 +32,6 @@ from nanobot.channels.websocket.runtime import ( _is_valid_chat_id, _parse_envelope, _parse_inbound_payload, - publish_runtime_model_update, ) from nanobot.config.loader import load_config, save_config from nanobot.config.schema import Config, ModelPresetConfig @@ -1105,8 +1104,14 @@ async def test_send_broadcasts_runtime_model_updates() -> None: mock_ws = AsyncMock() channel._attach(mock_ws, "chat-1") - publish_runtime_model_update(bus, "openai/gpt-4.1", "fast") - await channel.send(bus.outbound.get_nowait()) + await channel.send( + OutboundMessage( + channel="websocket", + chat_id="*", + content="", + event=RuntimeModelUpdatedEvent(model="openai/gpt-4.1", model_preset="fast"), + ) + ) payload = json.loads(mock_ws.send.call_args[0][0]) assert payload["event"] == "runtime_model_updated" @@ -1141,26 +1146,6 @@ async def test_send_scopes_turn_model_updates_to_the_subscribed_chat() -> None: chat_two.send.assert_not_awaited() -@pytest.mark.asyncio -async def test_runtime_model_update_publisher_uses_websocket_outbound_event() -> None: - bus = MessageBus() - - publish_runtime_model_update( - bus, - "openai/gpt-4.1", - "fast", - ) - - event = bus.outbound.get_nowait() - assert event.channel == "websocket" - assert event.chat_id == "*" - assert event.content == "" - assert event.metadata == {} - assert isinstance(event.event, RuntimeModelUpdatedEvent) - assert event.event.model == "openai/gpt-4.1" - assert event.event.model_preset == "fast" - - @pytest.mark.asyncio async def test_send_stages_external_media_as_signed_url(monkeypatch, tmp_path) -> None: bus = MagicMock() diff --git a/nanobot/channels/websocket/tests/test_websocket_media_route.py b/nanobot/channels/websocket/tests/test_websocket_media_route.py index 376b5c930..ad3bb7892 100644 --- a/nanobot/channels/websocket/tests/test_websocket_media_route.py +++ b/nanobot/channels/websocket/tests/test_websocket_media_route.py @@ -22,6 +22,7 @@ from nanobot.webui.gateway_services import build_gateway_services from nanobot.webui.media_api import ( b64url_decode, b64url_encode, + sign_media_path, ) from .ws_test_client import InProcessHttpChannel @@ -84,8 +85,16 @@ def _fake_media_dir(root: Path): return inner +def _sign_media_path(channel: WebSocketChannel, path: Path) -> str | None: + return sign_media_path( + path, + secret=channel.gateway.media.secret, + media_dir=channel.gateway.media._media_dir, + ) + + # --------------------------------------------------------------------------- -# gateway.media.sign_media_path: the URL minter +# media_api.sign_media_path: the URL minter # --------------------------------------------------------------------------- @@ -105,10 +114,10 @@ def test_sign_media_path_rejects_paths_outside_media_root( media.mkdir() channel = _ch(bus, port=0) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - assert channel.gateway.media.sign_media_path(outside) is None + assert _sign_media_path(channel, outside) is None # Traversal via the media root is also rejected — the resolve() step # normalises ``..`` out before the relative_to check. - assert channel.gateway.media.sign_media_path(media / ".." / "secrets" / "cred.txt") is None + assert _sign_media_path(channel, media / ".." / "secrets" / "cred.txt") is None def test_sign_media_path_round_trips_via_hmac( @@ -120,7 +129,7 @@ def test_sign_media_path_round_trips_via_hmac( (media / "a.png").write_bytes(_PNG_BYTES) channel = _ch(bus, port=0) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - url = channel.gateway.media.sign_media_path(media / "a.png") + url = _sign_media_path(channel, media / "a.png") assert url is not None assert url.startswith("/api/media/") sig, payload = url[len("/api/media/"):].split("/", 1) @@ -235,7 +244,7 @@ async def test_media_route_serves_signed_file( channel = _ch(bus, port=29920) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - url_path = channel.gateway.media.sign_media_path(target) + url_path = _sign_media_path(channel, target) assert url_path is not None server_task = asyncio.create_task(channel.start()) try: @@ -267,7 +276,7 @@ async def test_media_route_serves_video_byte_ranges( channel = _ch(bus, port=29927) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - url_path = channel.gateway.media.sign_media_path(target) + url_path = _sign_media_path(channel, target) assert url_path is not None server_task = asyncio.create_task(channel.start()) try: @@ -298,7 +307,7 @@ async def test_media_route_serves_suffix_video_byte_ranges( channel = _ch(bus, port=29928) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - url_path = channel.gateway.media.sign_media_path(target) + url_path = _sign_media_path(channel, target) assert url_path is not None server_task = asyncio.create_task(channel.start()) try: @@ -326,7 +335,7 @@ async def test_media_route_rejects_unsatisfiable_byte_range( channel = _ch(bus, port=29929) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - url_path = channel.gateway.media.sign_media_path(target) + url_path = _sign_media_path(channel, target) assert url_path is not None server_task = asyncio.create_task(channel.start()) try: @@ -358,7 +367,7 @@ async def test_media_route_rejects_bad_signature( channel = _ch(bus, port=29921) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - good = channel.gateway.media.sign_media_path(media / "f.png") + good = _sign_media_path(channel, media / "f.png") assert good is not None _, payload = good[len("/api/media/"):].split("/", 1) # Forge a sig with a *different* secret. @@ -423,7 +432,7 @@ async def test_media_route_404s_missing_file( channel = _ch(bus, port=29923) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - url_path = channel.gateway.media.sign_media_path(target) + url_path = _sign_media_path(channel, target) assert url_path is not None target.unlink() # the file vanishes between signing and fetching server_task = asyncio.create_task(channel.start()) @@ -480,7 +489,7 @@ async def test_media_route_serves_svg_with_strict_csp( channel = _ch(bus, port=29928) with patch("nanobot.webui.media_gateway.get_media_dir", return_value=media): - url_path = channel.gateway.media.sign_media_path(target) + url_path = _sign_media_path(channel, target) assert url_path is not None server_task = asyncio.create_task(channel.start()) try: diff --git a/nanobot/cron/service.py b/nanobot/cron/service.py index ce0df35b4..20fbe4be6 100644 --- a/nanobot/cron/service.py +++ b/nanobot/cron/service.py @@ -25,9 +25,6 @@ from nanobot.cron.types import ( CronSchedule, CronStore, ) -from nanobot.utils.run_records import ( - safe_run_record_name, -) from nanobot.utils.run_records import ( write_run_record as write_automation_run_record, ) @@ -440,10 +437,6 @@ class CronService: tmp_path.unlink(missing_ok=True) raise - @staticmethod - def _safe_run_record_name(run_id: str) -> str: - return safe_run_record_name(run_id) - def write_run_record(self, run_id: str, record: dict[str, Any]) -> None: """Write an internal audit record for one cron execution.""" write_automation_run_record(self._run_records_dir, run_id, record) diff --git a/nanobot/cron/session_turns.py b/nanobot/cron/session_turns.py index 27622b83d..b0c9e847f 100644 --- a/nanobot/cron/session_turns.py +++ b/nanobot/cron/session_turns.py @@ -7,7 +7,6 @@ from typing import Any, Mapping from nanobot.cron.types import CronJob from nanobot.session.automation_turns import ( AutomationTurnSpec, - automation_history_overrides_for_spec, automation_trigger, ) @@ -63,11 +62,6 @@ def cron_run_id(metadata: Mapping[str, Any] | None) -> str | None: return value if isinstance(value, str) and value else None -def cron_history_overrides(metadata: Mapping[str, Any] | None) -> tuple[str | None, dict[str, Any]]: - """Return session-history text/metadata overrides for a cron turn.""" - return automation_history_overrides_for_spec(metadata, CRON_AUTOMATION_SPEC) - - def is_bound_cron_job(job: CronJob) -> bool: """True for session-bound cron jobs with complete delivery context.""" payload = job.payload diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index 723067ba9..81682e85b 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -1086,10 +1086,6 @@ class SessionManager: """Attempt to recover a session from a corrupt JSONL file.""" return self._jsonl_store.repair(key, path=path) - @staticmethod - def _session_payload(session: Session) -> SessionPayload: - return JsonlSessionStore.session_payload(session) - def save(self, session: Session, *, fsync: bool = False) -> None: """Persist a session and retain it in the cache.""" archiver = self._file_cap_archiver diff --git a/nanobot/triggers/local_session_turns.py b/nanobot/triggers/local_session_turns.py index 6459640cc..8ac456bdf 100644 --- a/nanobot/triggers/local_session_turns.py +++ b/nanobot/triggers/local_session_turns.py @@ -6,7 +6,6 @@ from typing import Any, Mapping from nanobot.session.automation_turns import ( AutomationTurnSpec, - automation_history_overrides_for_spec, automation_trigger, ) @@ -50,13 +49,3 @@ def local_trigger_delivery_id(metadata: Mapping[str, Any] | None) -> str | None: return None value = trigger.get("delivery_id") return value if isinstance(value, str) and value else None - - -def local_trigger_history_overrides( - metadata: Mapping[str, Any] | None, -) -> tuple[str | None, dict[str, Any]]: - """Return session-history text/metadata overrides for a local trigger turn.""" - return automation_history_overrides_for_spec( - metadata, - LOCAL_TRIGGER_AUTOMATION_SPEC, - ) diff --git a/nanobot/utils/document.py b/nanobot/utils/document.py index 6d27fadff..20b77625f 100644 --- a/nanobot/utils/document.py +++ b/nanobot/utils/document.py @@ -11,35 +11,6 @@ from loguru import logger from nanobot.utils.helpers import detect_image_mime -# Supported file extensions for text extraction -SUPPORTED_EXTENSIONS: set[str] = { - # Document formats - ".pdf", - ".docx", - ".xlsx", - ".pptx", - # Text formats - ".txt", - ".md", - ".csv", - ".json", - ".xml", - ".html", - ".htm", - ".log", - ".yaml", - ".yml", - ".toml", - ".ini", - ".cfg", - # Image formats (for future OCR support) - ".png", - ".jpg", - ".jpeg", - ".gif", - ".webp", -} - _MAX_TEXT_LENGTH = 200_000 _MAX_EXTRACT_FILE_SIZE = 50 * 1024 * 1024 # 50 MB _MAX_OFFICE_ARCHIVE_MEMBERS = 10_000 diff --git a/nanobot/utils/file_edit_events.py b/nanobot/utils/file_edit_events.py index e6740df8c..270a4ccb7 100644 --- a/nanobot/utils/file_edit_events.py +++ b/nanobot/utils/file_edit_events.py @@ -274,24 +274,6 @@ def _text_line_count(text: str) -> int: return line_count if last_was_newline else line_count + 1 -def prepare_file_edit_tracker( - *, - call_id: str, - tool_name: str, - tool: Any, - workspace: Path | None, - params: dict[str, Any] | None, -) -> FileEditTracker | None: - trackers = prepare_file_edit_trackers( - call_id=call_id, - tool_name=tool_name, - tool=tool, - workspace=workspace, - params=params, - ) - return trackers[0] if trackers else None - - def prepare_file_edit_trackers( *, call_id: str, diff --git a/nanobot/utils/gitstore.py b/nanobot/utils/gitstore.py index f09325077..4140b153b 100644 --- a/nanobot/utils/gitstore.py +++ b/nanobot/utils/gitstore.py @@ -5,14 +5,13 @@ from __future__ import annotations import io import time from dataclasses import dataclass -from datetime import datetime, timezone from pathlib import Path -from typing import TYPE_CHECKING, Iterable, cast +from typing import TYPE_CHECKING, cast from loguru import logger if TYPE_CHECKING: - from dulwich.objects import Blob, Commit, ObjectID, Tree, TreeEntry + from dulwich.objects import Blob, Commit, ObjectID, Tree from dulwich.refs import Ref from dulwich.repo import Repo @@ -45,25 +44,6 @@ class CommitInfo: return f"{header}\n(no file changes)" -@dataclass -class LineAge: - """Age of a single line based on git blame.""" - - age_days: int # days since last modification - - -def _compute_line_ages( - annotated: Iterable[tuple[tuple["Commit", "TreeEntry"], bytes]], -) -> list[LineAge]: - """Convert annotate results to per-line ages.""" - now = datetime.now(tz=timezone.utc).date() - ages: list[LineAge] = [] - for (commit, _tree_entry), _line_bytes in annotated: - dt = datetime.fromtimestamp(commit.commit_time, tz=timezone.utc).date() - ages.append(LineAge(age_days=(now - dt).days)) - return ages - - class GitStore: """Git-backed version control for memory files.""" @@ -293,33 +273,6 @@ class GitStore: except Exception as exc: raise GitStoreError("Git log failed") from exc - def line_ages(self, file_path: str) -> list[LineAge]: - """Compute the age of each line in a tracked file via git blame. - - Returns one LineAge per line, in order. - Returns an empty list if the repo is not initialized or the file is - empty. Annotation failures raise :class:`GitStoreError`. - """ - - if not self.is_initialized(): - return [] - - target = self._workspace / file_path - if not target.exists() or target.stat().st_size == 0: - return [] - - try: - from dulwich import porcelain - - annotated = porcelain.annotate(str(self._workspace), file_path) - except Exception as exc: - raise GitStoreError(f"Git line annotation failed for {file_path}") from exc - - if not annotated: - return [] - - return _compute_line_ages(annotated) - def diff_commits(self, sha1: str, sha2: str) -> str: """Show diff between two commits.""" if not self.is_initialized(): @@ -461,13 +414,6 @@ class GitStore: commit = cast("Commit", commit_obj) return cast("Tree", repo[commit.tree]) - def find_commit(self, short_sha: str, max_entries: int = 20) -> CommitInfo | None: - """Find a commit by short SHA prefix match.""" - for c in self.log(max_entries=max_entries): - if c.sha.startswith(short_sha): - return c - return None - def show_commit_diff( self, short_sha: str, diff --git a/nanobot/utils/helpers.py b/nanobot/utils/helpers.py index 8ab5f32bb..1f1720dd3 100644 --- a/nanobot/utils/helpers.py +++ b/nanobot/utils/helpers.py @@ -351,18 +351,6 @@ def timestamp() -> str: return datetime.now().isoformat() -def current_time_str(timezone: str | None = None) -> str: - """Return the current time string.""" - from zoneinfo import ZoneInfo - - tz = ZoneInfo(timezone) if timezone else None - now = datetime.now(tz=tz) if tz else datetime.now().astimezone() - offset = now.strftime("%z") - offset_fmt = f"{offset[:3]}:{offset[3:]}" if len(offset) == 5 else offset - tz_name = timezone or (time.strftime("%Z") or "UTC") - return f"{now.strftime('%Y-%m-%d %H:%M (%A)')} ({tz_name}, UTC{offset_fmt})" - - _UNSAFE_CHARS = re.compile(r'[<>:"/\\|?*]') _TOOL_RESULT_PREVIEW_CHARS = 1200 _TOOL_RESULTS_DIR = ".nanobot/tool-results" diff --git a/nanobot/webui/media_gateway.py b/nanobot/webui/media_gateway.py index 5e83be142..6e3f4d424 100644 --- a/nanobot/webui/media_gateway.py +++ b/nanobot/webui/media_gateway.py @@ -18,7 +18,6 @@ from nanobot.webui.attachment_ingress import ( from nanobot.webui.ingress_policy import AttachmentIngressLimits from nanobot.webui.media_api import ( serve_signed_media, - sign_media_path, sign_or_stage_media_path, signed_media_attachments, ) @@ -71,13 +70,6 @@ class WebUIMediaGateway: media_dir=self._media_dir, ) - def sign_media_path(self, abs_path: Path) -> str | None: - return sign_media_path( - abs_path, - secret=self.secret, - media_dir=self._media_dir, - ) - def sign_or_stage_media_path(self, path: Path) -> dict[str, str] | None: return sign_or_stage_media_path( path, diff --git a/nanobot/webui/transcript.py b/nanobot/webui/transcript.py index 0d5638e09..8fc233b39 100644 --- a/nanobot/webui/transcript.py +++ b/nanobot/webui/transcript.py @@ -1313,21 +1313,6 @@ def _recover_incomplete_turns( return recovered -def recover_incomplete_turns_from_session( - lines: list[dict[str, Any]], - session_messages: list[dict[str, Any]] | None, - *, - session_key: str, -) -> list[dict[str, Any]]: - """Recover marked transcript answers only when one durable session turn matches.""" - if not lines or not session_messages or not _needs_incomplete_turn_recovery(lines): - return lines - session_turns = _session_backfill_turns(session_key, session_messages) - if not session_turns: - return lines - return _recover_incomplete_turns(lines, session_turns) - - def _with_backfilled_user( records: list[dict[str, Any]], user_event: dict[str, Any], @@ -1365,20 +1350,6 @@ def _inject_missing_user_events( return out -def inject_missing_user_events_from_session( - session_key: str, - lines: list[dict[str, Any]], - session_messages: list[dict[str, Any]] | None, -) -> list[dict[str, Any]]: - """Backfill user rows for legacy WebUI transcripts that only stored assistant streams.""" - if not lines or not session_messages or not _needs_user_event_backfill(lines): - return lines - session_turns = _session_backfill_turns(session_key, session_messages) - if not session_turns: - return lines - return _inject_missing_user_events(lines, session_turns) - - def _format_tool_call_trace(call: Any) -> str | None: if not call or not isinstance(call, dict): return None diff --git a/tests/agent/test_git_store.py b/tests/agent/test_git_store.py index b3c281608..473624811 100644 --- a/tests/agent/test_git_store.py +++ b/tests/agent/test_git_store.py @@ -169,19 +169,6 @@ class TestDiffCommits: assert git_ready.diff_commits("deadbeef", "cafebabe") == "" -class TestFindCommit: - def test_finds_by_prefix(self, git_ready): - ws = git_ready._workspace - (ws / "SOUL.md").write_text("v2", encoding="utf-8") - sha = git_ready.auto_commit("v2") - found = git_ready.find_commit(sha[:4]) - assert found is not None - assert found.sha == sha - - def test_returns_none_for_unknown(self, git_ready): - assert git_ready.find_commit("deadbeef") is None - - class TestShowCommitDiff: def test_returns_commit_with_diff(self, git_ready): ws = git_ready._workspace diff --git a/tests/agent/test_runner_injections.py b/tests/agent/test_runner_injections.py index f76ad10bd..5fbed09b4 100644 --- a/tests/agent/test_runner_injections.py +++ b/tests/agent/test_runner_injections.py @@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from agent.runner_helpers import make_run_spec +from nanobot.agent.automation_turns import publish_next_deferred_turn from nanobot.config.schema import AgentDefaults from nanobot.providers.base import LLMResponse, ToolCallRequest @@ -1047,7 +1048,11 @@ async def test_cron_turn_deferred_while_session_active(tmp_path): assert loop._cron_turns.deferred_queues[session_key] == [msg] assert loop.pending_cron_job_ids_for_session(session_key) == {"job-1"} - await loop._cron_turns.publish_next_deferred(session_key) + await publish_next_deferred_turn( + deferred_queues=loop._cron_turns.deferred_queues, + publish_inbound=loop.bus.publish_inbound, + session_key=session_key, + ) queued = await asyncio.wait_for(loop.bus.consume_inbound(), timeout=0.5) assert queued is msg assert session_key not in loop._cron_turns.deferred_queues @@ -1097,7 +1102,11 @@ async def test_local_trigger_turn_deferred_while_session_active(tmp_path): assert loop._local_trigger_turns.deferred_queues[session_key] == [msg] assert loop.pending_local_trigger_ids_for_session(session_key) == {"trg_123"} - assert await loop._local_trigger_turns.publish_next_deferred(session_key) is True + assert await publish_next_deferred_turn( + deferred_queues=loop._local_trigger_turns.deferred_queues, + publish_inbound=loop.bus.publish_inbound, + session_key=session_key, + ) is True queued = await asyncio.wait_for(loop.bus.consume_inbound(), timeout=0.5) assert queued is msg assert session_key not in loop._local_trigger_turns.deferred_queues diff --git a/tests/cli_apps/test_utils.py b/tests/cli_apps/test_utils.py index 2a2b01d0e..4c5d7d64e 100644 --- a/tests/cli_apps/test_utils.py +++ b/tests/cli_apps/test_utils.py @@ -1,9 +1,7 @@ """Tests for CLI Apps loop helpers.""" -from types import SimpleNamespace - from nanobot.apps.cli.service import CliAppManager -from nanobot.apps.cli.utils import runtime_lines, session_extra +from nanobot.apps.cli.utils import runtime_lines_for_request, session_extra def test_session_extra_returns_cli_apps_only_when_present() -> None: @@ -30,8 +28,9 @@ def test_cli_app_mentions_inject_runtime_metadata(tmp_path, monkeypatch): } ) - lines = runtime_lines( - SimpleNamespace(content="please use @zoom tonight; ignore @krita?", metadata={}), + lines = runtime_lines_for_request( + "please use @zoom tonight; ignore @krita?", + {}, tmp_path, ) @@ -43,17 +42,15 @@ def test_cli_app_mentions_inject_runtime_metadata(tmp_path, monkeypatch): def test_structured_cli_app_attachment_injects_runtime_metadata(tmp_path): - lines = runtime_lines( - SimpleNamespace( - content="please use @zoom tonight", - metadata={ - "cli_apps": [{ - "name": "zoom", - "entry_point": "cli-anything-zoom", - "display_name": "Zoom", - }], - }, - ), + lines = runtime_lines_for_request( + "please use @zoom tonight", + { + "cli_apps": [{ + "name": "zoom", + "entry_point": "cli-anything-zoom", + "display_name": "Zoom", + }], + }, tmp_path, ) diff --git a/tests/test_document_parsing.py b/tests/test_document_parsing.py index b2ae4e7d7..abf87413d 100644 --- a/tests/test_document_parsing.py +++ b/tests/test_document_parsing.py @@ -6,7 +6,6 @@ from zipfile import ZipFile import pytest from nanobot.utils.document import ( - SUPPORTED_EXTENSIONS, PdfSafetyError, _is_text_extension, extract_pdf_pages, @@ -14,31 +13,6 @@ from nanobot.utils.document import ( ) -class TestSupportedExtensions: - """Test the SUPPORTED_EXTENSIONS constant.""" - - def test_supported_extensions_include_common_formats(self): - """Test that common document formats are included.""" - # Document formats - assert ".pdf" in SUPPORTED_EXTENSIONS - assert ".docx" in SUPPORTED_EXTENSIONS - assert ".xlsx" in SUPPORTED_EXTENSIONS - assert ".pptx" in SUPPORTED_EXTENSIONS - - # Text formats - assert ".txt" in SUPPORTED_EXTENSIONS - assert ".md" in SUPPORTED_EXTENSIONS - assert ".csv" in SUPPORTED_EXTENSIONS - assert ".json" in SUPPORTED_EXTENSIONS - assert ".yaml" in SUPPORTED_EXTENSIONS - assert ".yml" in SUPPORTED_EXTENSIONS - - # Image formats - assert ".png" in SUPPORTED_EXTENSIONS - assert ".jpg" in SUPPORTED_EXTENSIONS - assert ".jpeg" in SUPPORTED_EXTENSIONS - - class TestExtractText: """Test the extract_text function.""" diff --git a/tests/tools/test_edit_advanced.py b/tests/tools/test_edit_advanced.py index df1f001da..ea467bb43 100644 --- a/tests/tools/test_edit_advanced.py +++ b/tests/tools/test_edit_advanced.py @@ -13,7 +13,7 @@ import os import pytest from nanobot.agent.tools import file_state -from nanobot.agent.tools.filesystem import EditFileTool, ReadFileTool, _find_match +from nanobot.agent.tools.filesystem import EditFileTool, ReadFileTool @pytest.fixture(autouse=True) @@ -68,41 +68,6 @@ class TestDeleteLineCleanup: # --------------------------------------------------------------------------- -class TestSmartQuoteNormalization: - """_find_match should handle curly ↔ straight quote fallback.""" - - def test_curly_double_quotes_match_straight(self): - content = 'She said \u201chello\u201d to him' - old_text = 'She said "hello" to him' - match, count = _find_match(content, old_text) - assert match is not None - assert count == 1 - # Returned match should be the ORIGINAL content with curly quotes - assert "\u201c" in match - - def test_curly_single_quotes_match_straight(self): - content = "it\u2019s a test" - old_text = "it's a test" - match, count = _find_match(content, old_text) - assert match is not None - assert count == 1 - assert "\u2019" in match - - def test_straight_matches_curly_in_old_text(self): - content = 'x = "hello"' - old_text = 'x = \u201chello\u201d' - match, count = _find_match(content, old_text) - assert match is not None - assert count == 1 - - def test_exact_match_still_preferred_over_quote_normalization(self): - content = 'x = "hello"' - old_text = 'x = "hello"' - match, count = _find_match(content, old_text) - assert match == old_text - assert count == 1 - - class TestQuoteStylePreservation: """When quote-normalized matching occurs, replacement should preserve actual quote style.""" diff --git a/tests/tools/test_filesystem_tools.py b/tests/tools/test_filesystem_tools.py index 9b917dc6d..b221bfcc1 100644 --- a/tests/tools/test_filesystem_tools.py +++ b/tests/tools/test_filesystem_tools.py @@ -7,7 +7,6 @@ from nanobot.agent.tools.filesystem import ( ListDirTool, ReadFileTool, WriteFileTool, - _find_match, ) # --------------------------------------------------------------------------- @@ -116,52 +115,6 @@ class TestReadFileTool: assert "Maximum is 100 MiB" in result -# --------------------------------------------------------------------------- -# _find_match (unit tests for the helper) -# --------------------------------------------------------------------------- - -class TestFindMatch: - - def test_exact_match(self): - match, count = _find_match("hello world", "world") - assert match == "world" - assert count == 1 - - def test_exact_no_match(self): - match, count = _find_match("hello world", "xyz") - assert match is None - assert count == 0 - - def test_crlf_normalisation(self): - # Caller normalises CRLF before calling _find_match, so test with - # pre-normalised content to verify exact match still works. - content = "line1\nline2\nline3" - old_text = "line1\nline2\nline3" - match, count = _find_match(content, old_text) - assert match is not None - assert count == 1 - - def test_line_trim_fallback(self): - content = " def foo():\n pass\n" - old_text = "def foo():\n pass" - match, count = _find_match(content, old_text) - assert match is not None - assert count == 1 - # The returned match should be the *original* indented text - assert " def foo():" in match - - def test_line_trim_multiple_candidates(self): - content = " a\n b\n a\n b\n" - old_text = "a\nb" - match, count = _find_match(content, old_text) - assert count == 2 - - def test_empty_old_text(self): - match, count = _find_match("hello", "") - # Empty string is always "in" any string via exact match - assert match == "" - - # --------------------------------------------------------------------------- # EditFileTool # --------------------------------------------------------------------------- diff --git a/tests/utils/test_file_edit_events.py b/tests/utils/test_file_edit_events.py index dae8dec59..2a12697b7 100644 --- a/tests/utils/test_file_edit_events.py +++ b/tests/utils/test_file_edit_events.py @@ -9,7 +9,6 @@ from nanobot.utils.file_edit_events import ( build_file_edit_start_event, build_unified_diff_payload, line_diff_stats, - prepare_file_edit_tracker, prepare_file_edit_trackers, read_file_snapshot, ) @@ -44,15 +43,14 @@ def test_write_file_start_tracks_snapshot_and_end_emits_exact_diff(tmp_path: Pat target = tmp_path / "notes.txt" target.write_text("old\nkeep\n", encoding="utf-8") params = {"path": "notes.txt", "content": "new\nkeep\nextra\n"} - tracker = prepare_file_edit_tracker( + trackers = prepare_file_edit_trackers( call_id="call-write", tool_name="write_file", tool=_write_tool(tmp_path), workspace=tmp_path, params=params, ) - - assert tracker is not None + [tracker] = trackers start = build_file_edit_start_event(tracker) assert start == { "version": 1, @@ -103,15 +101,14 @@ def test_unified_diff_payload_truncates_large_diffs() -> None: def test_binary_file_is_reported_but_not_counted(tmp_path: Path) -> None: target = tmp_path / "data.bin" target.write_bytes(b"\x00\x01before") - tracker = prepare_file_edit_tracker( + trackers = prepare_file_edit_trackers( call_id="call-bin", tool_name="edit_file", tool=_edit_tool(tmp_path), workspace=tmp_path, params={"path": "data.bin", "old_text": "before", "new_text": "after"}, ) - - assert tracker is not None + [tracker] = trackers assert not read_file_snapshot(target).countable target.write_bytes(b"\x00\x01after") event = build_file_edit_end_event(tracker) @@ -123,15 +120,14 @@ def test_binary_file_is_reported_but_not_counted(tmp_path: Path) -> None: def test_binary_before_file_is_reported_but_not_counted(tmp_path: Path) -> None: target = tmp_path / "data.bin" target.write_bytes(b"\x00\x01before") - tracker = prepare_file_edit_tracker( + trackers = prepare_file_edit_trackers( call_id="call-bin", tool_name="write_file", tool=_write_tool(tmp_path), workspace=tmp_path, params={"path": "data.bin", "content": "after\n"}, ) - - assert tracker is not None + [tracker] = trackers target.write_text("after\n", encoding="utf-8") event = build_file_edit_end_event(tracker) assert event["binary"] is True @@ -215,15 +211,14 @@ def test_apply_patch_dry_run_does_not_prepare_file_edit_trackers(tmp_path: Path) def test_oversized_file_is_reported_but_not_counted(tmp_path: Path) -> None: target = tmp_path / "large.txt" params = {"path": "large.txt", "content": "x"} - tracker = prepare_file_edit_tracker( + trackers = prepare_file_edit_trackers( call_id="call-large", tool_name="write_file", tool=_write_tool(tmp_path), workspace=tmp_path, params=params, ) - - assert tracker is not None + [tracker] = trackers target.write_text("x" * (2 * 1024 * 1024 + 1), encoding="utf-8") event = build_file_edit_end_event(tracker) assert event["binary"] is True @@ -232,11 +227,11 @@ def test_oversized_file_is_reported_but_not_counted(tmp_path: Path) -> None: assert "diff" not in event -def test_untracked_tools_do_not_prepare_file_edit_tracker(tmp_path: Path) -> None: - assert prepare_file_edit_tracker( +def test_untracked_tools_do_not_prepare_file_edit_trackers(tmp_path: Path) -> None: + assert prepare_file_edit_trackers( call_id="call-exec", tool_name="exec", tool=None, workspace=tmp_path, params={"path": "created-by-shell.txt"}, - ) is None + ) == [] diff --git a/tests/utils/test_gitstore.py b/tests/utils/test_gitstore.py index 14a2d7fd6..04fe452a0 100644 --- a/tests/utils/test_gitstore.py +++ b/tests/utils/test_gitstore.py @@ -1,13 +1,12 @@ -"""Tests for GitStore — line_ages() and core git operations.""" +"""Tests for GitStore core operations.""" import subprocess -from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import patch import pytest -from nanobot.utils.gitstore import GitStore, GitStoreError +from nanobot.utils.gitstore import GitStore @pytest.fixture @@ -18,89 +17,6 @@ def git(tmp_path): return g -class TestLineAges: - def test_returns_empty_when_not_initialized(self, tmp_path): - """line_ages should return [] if the git repo is not initialized.""" - git = GitStore(tmp_path, tracked_files=["MEMORY.md"]) - assert git.line_ages("MEMORY.md") == [] - - def test_returns_empty_for_missing_file(self, git): - """line_ages should return [] for a file that doesn't exist.""" - assert git.line_ages("SOUL.md") == [] - - def test_returns_empty_for_empty_file(self, git, tmp_path): - """line_ages should return [] for an empty tracked file.""" - (tmp_path / "SOUL.md").write_text("", encoding="utf-8") - git.auto_commit("empty soul") - assert git.line_ages("SOUL.md") == [] - - def test_one_age_per_line(self, git, tmp_path): - """line_ages should return one entry per line in the file.""" - content = "# Memory\n\n## Section A\n- item 1\n" - (tmp_path / "MEMORY.md").write_text(content, encoding="utf-8") - git.auto_commit("initial") - ages = git.line_ages("MEMORY.md") - assert len(ages) == len(content.splitlines()) - - def test_fresh_lines_have_age_zero(self, git, tmp_path): - """Lines committed today should have age_days=0.""" - (tmp_path / "MEMORY.md").write_text("## A\n- x\n", encoding="utf-8") - git.auto_commit("initial") - ages = git.line_ages("MEMORY.md") - assert all(a.age_days == 0 for a in ages) - - def test_age_differentiates_across_days(self, git, tmp_path): - """Lines committed today should show correct age when 'now' is mocked forward.""" - (tmp_path / "MEMORY.md").write_text("## A\n- x\n", encoding="utf-8") - git.auto_commit("initial") - - future_now = datetime.now(tz=timezone.utc) + timedelta(days=30) - with patch("nanobot.utils.gitstore.datetime") as mock_dt: - mock_dt.now.return_value = future_now - mock_dt.fromtimestamp = datetime.fromtimestamp - ages = git.line_ages("MEMORY.md") - - assert len(ages) == 2 - assert all(a.age_days == 30 for a in ages) - - def test_annotate_failure_is_explicit(self, git, tmp_path): - (tmp_path / "MEMORY.md").write_text("important\n", encoding="utf-8") - git.auto_commit("initial") - - with patch("dulwich.porcelain.annotate", side_effect=OSError("broken repo")): - with pytest.raises(GitStoreError, match="annotation failed"): - git.line_ages("MEMORY.md") - - def test_partial_edit_only_updates_changed_lines(self, git, tmp_path): - """Only modified lines should reflect the new commit's timestamp.""" - now = datetime(2026, 5, 1, tzinfo=timezone.utc) - old = now - timedelta(days=30) - - (tmp_path / "MEMORY.md").write_text( - "# Memory\n\n## A\n- old\n\n## B\n- keep\n", encoding="utf-8" - ) - with patch("dulwich.worktree.time.time", return_value=old.timestamp()): - git.auto_commit("commit1") - - # Only modify section A - (tmp_path / "MEMORY.md").write_text( - "# Memory\n\n## A\n- new\n\n## B\n- keep\n", encoding="utf-8" - ) - with patch("dulwich.worktree.time.time", return_value=now.timestamp()): - git.auto_commit("commit2") - - with patch("nanobot.utils.gitstore.datetime") as mock_dt: - mock_dt.now.return_value = now - mock_dt.fromtimestamp = datetime.fromtimestamp - ages = git.line_ages("MEMORY.md") - - lines = (tmp_path / "MEMORY.md").read_text(encoding="utf-8").splitlines() - assert len(ages) == len(lines) - age_by_line = {line: age.age_days for line, age in zip(lines, ages, strict=True)} - assert age_by_line["- new"] == 0 - assert age_by_line["- keep"] == 30 - - class TestSummarizeWorkingTree: """Ground-truth diff summary used to keep Dream audit records honest.""" diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index a526d17c6..c43d760c4 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -1,14 +1,11 @@ from pathlib import Path -from zoneinfo import ZoneInfoNotFoundError -import pytest import tiktoken from nanobot.utils import helpers from nanobot.utils.helpers import ( _write_text_atomic, content_with_media_breadcrumbs, - current_time_str, split_message, truncate_text_to_tokens, ) @@ -51,11 +48,6 @@ def test_truncate_text_to_tokens_non_positive_budget_returns_text(): assert truncate_text_to_tokens(text, 0) == text -def test_current_time_str_rejects_unknown_timezone(): - with pytest.raises(ZoneInfoNotFoundError): - current_time_str("Not/AZone") - - def test_content_with_media_breadcrumbs_preserves_valid_paths(): assert content_with_media_breadcrumbs( "user", diff --git a/webui/bun.lock b/webui/bun.lock index bc46bcff9..d84a59437 100644 --- a/webui/bun.lock +++ b/webui/bun.lock @@ -9,7 +9,6 @@ "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", "class-variance-authority": "^0.7.1", @@ -250,8 +249,6 @@ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], @@ -1332,8 +1329,6 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], diff --git a/webui/package.json b/webui/package.json index dd6eb8e34..dc086c3cd 100644 --- a/webui/package.json +++ b/webui/package.json @@ -16,7 +16,6 @@ "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", "class-variance-authority": "^0.7.1", diff --git a/webui/public/brand/nanobot_icon.png b/webui/public/brand/nanobot_icon.png deleted file mode 100644 index 046086efe03efcabd99ac034b181eb11adc67052..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7365 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%0D8<)2z+h}5?sr&AG+fOx!zS!1MYwY(V zUW5PywSzZnCfGiA87mJyh4n|z!*8`=nL*a{dB#1dXE-H7a0C7Rw zD#1g9{*Fw^L#jbm1+hF+Y2-bkK@O34W)nm&fVq%mnHbClY!mn%u<`UdEk#IrLYIRD!@wl$pJ$5@+P|LTd0i=VPI|&$w(vK zbY9InI8k^oOcmGj5`;<$0oqlm6xgHHH+xn~d0Lk${1=8RZW34x{J~02IHcUsv3mA zjq_{RJiCId?VuVI&%F0W3>c%*NK*K8>=po+-^-JxkDnyq419gxk2UNTTHQ>=E0lud z$%%n&)KMF%qc+q)GH8L6qjV86o=p-Y12t@2-a?WjVR>G_iA8;|Ua(}^LnJH76Ys*V zNIY_hHR|jpAutiV0>%(X8JY7SW*8GmX*`d3kI92G7@inKyV*u(tBtJP1H1>R#$F|p zLXrBNqX2;8drXzQ5@!SeOs{o>5(3q0NCxXj1{<)$14srF*far=LexVfr17HNqzsVY zrWYN;>N9I-wt6rOIPY=mOOK&3T!)K7-Bs1Hs;;5f!=wtYiZr!2aCjPzJ#-GNHBxLX31ML472F&JIH|h3h(G?H+nNZM2&`YV|y{Cno@a7RyqPcI#8; zK6uxs_l?@0&W!P#vZ1(}UVPBGbI+l?w*5Zc{?Wk!4v?@hOV7a#0NydN`GhP0DM@K1b40 zaFJ1i1V$ha zr~||Sk|}&9@I4Rd3VbF|M^I1XI;xbpRGXLfg=7}WG#j%2A{GH;4?Q-!; zK;fN>9wY@>&96z2G8|&7h?JlzU}Lbkx`XwzmqKq(%DyX62us5px+ki_#+pr+-VET= z^H?r{J5T@F`bJ}<=154Oszh<(E=tTHScu_z8PcgyjLuDkny&Dayr?cqmC)Y@loK$A zNmpeC5u>@f0%>i6*+N9X1oWCM^wwJ-Yrw{YNHzw;GgBeci37w#C4>4542eXx(rk{> z3UO#+3{MUsYjv=FVI9UMfO6=NnKXkp@YG zI8d(o5g0^(^A3&u;~2kX4q~GC6)}K7u`UbX3NO%P0Cw~ss4)sQ2|y;y(c3yY{3O1) zbU$dP4YC$cpxyCkwK}kdQDa5s9d7*_cVX!8ZScJ=C=X5VA?V}Y5gVY-IoX0J%Jc%POxgEaOEixOJK{kaMnL>U4G4S9FyvtRopoZoGVjhFy zJ;o1=W8%hXKz-k=a}+i`fq13mHdi}AQ+nl@5K85&^#b^v?|t08{x45G#L)Ti1Ou59 zClvN3a~{J7CoysJ{xCg)0H8TQ=Bght1qPl@pf-0qC}}`k2B8T>w|fWZ%^OnKMvb03W$; z?d(fVO#j|cVhPMIJ9Nuz(l|KgGbLiSlvmir4HOcuhfspr{uAKZV92?BwSSfL#2sfF zM*D6!0v;H~GY@_RlgDnskN?b_aIRCT1w?&9C{;T$s(>-D17mRQCg8diW?yRFn&(3C zs5NSSWPI4{v>jdc>FWlk03-#KJf07KXud1Gxv=T$Y062 zUG!9akl)K*WcVWoaf)>RahT!hs1EWjPEY-vR}eIUK0rungLvl4U&82Mih9k$XPww& zC@0dQ8eSN^P?RJj5Z`H{eg07x=>f*}Y1xs33Mpu4aDXml^~Wx4`M2NxkN?;I2u~xx zoa!5*-@}uNM-$EjAV0k{Zw~zO$IqU8#b3XFIMpVzdFfEy4D>P|OrR9mlt7(_oxT}% zbS_M@5%qUnX|J-M~4 z(Y%4wnw}Y;%YNv?k9Lw@y5k+6`l>E~@k-FI{_fsW6W?|Apz2i*-+%Xyjoa3}lMVlB z$(qP|nFonc?_ej6!p__rz_i*~iIQvH5q`G3UXeJM;kGy6*Dk>@Kq}>cmFlyb54$=F zxf(%4aNDbBUwi_V03{X!p++O6)vWQ*mJYr1mS6kC9|GZ_<^1WYtIm%;mq(b z*{vA9qSDKhpz7m{fyN4v(ObW$y-SbFP|ccct!a}7|K+!2?DU;)`|lgMLPGT2AC0}H z=gM?pfpL0KiJ*^uDqh8#z%jiXO1BA>_GvmC?5i)e(81hlLpk+I1J;$JRxdPF-)W{zm?@n#g#(1ZEwP_Uj$P**HoPgr(K0}IZZ-PsJE=f zG#c1$>;K(W^N$X^?sqK(uF;eMOONLIJ@&K^@r4Ro?-O6v(2k4mF+EM{D%39S1 z`4tAfVhZ2hgkQT@C=Qyp5~+NgtTJ>P8m1CNm0F`_IvJWxsonG7`iuU>oo{(d=X$;8 z`)Z`>Gq<$3C{KOu{_(MF``r!WekEZC*X=r0Sn3;&&}vhx6w}zi?W{xBR^l|>t^u&> zt6UZl@u^|MjREX*`OLOU-#+)cd;T8~zBc64b$ZYD-AEnpMSBxRkZWkZ03!V|zunl@6??wQSPO@HgZT{!ms zx4!kQnJz6E%%A$6yytl{Qq;K#%gaosPWi3Rd}xY>+rL>S`DI%ppqIsA(TZVU%G%Eu z$o3{=^|HHKJ`603?qZZQ{0CWMM4y0atI~z=`Ek@=D>LXZr zf_hbPst(d+Zr|{l_dJu&p2)%i)8hBndw$@@($bQ7KEBQC2ldq-KhUqtP*sZ4bl=l) r8o=qrMg3v_ww)J_7mgQ>Yd-!z2=8C_>cpUa00000NkvXXu0mjf^JV`z diff --git a/webui/public/brand/nanobot_logo.png b/webui/public/brand/nanobot_logo.png deleted file mode 100644 index f519cbcf0ef6481a79735b49d26f4eb6621f9486..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28453 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 Y01}s&WTX_uq5uE@07*qoM6N<$g7#$gIRF3v diff --git a/webui/public/brand/nanobot_logo.webp b/webui/public/brand/nanobot_logo.webp deleted file mode 100644 index cb39dc449b8b33c1776cfe212c2318492a8fc90f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11938 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 maxLength ? `${text.slice(0, maxLength - 3)}...` : text; } -export function jumpToPrompt(scrollEl: HTMLElement | null, promptId: string | undefined): void { - if (!scrollEl || !promptId) return; - const target = findPromptElement(scrollEl, promptId); - if (!target) return; - scrollEl.scrollTo({ - top: Math.max(0, promptTop(scrollEl, target) - 16), - behavior: "smooth", - }); -} - export function findPromptElement(scrollEl: HTMLElement, promptId: string): HTMLElement | null { const candidates = scrollEl.querySelectorAll("[data-user-prompt-id]"); return Array.from(candidates).find( diff --git a/webui/src/components/ui/dropdown-menu.tsx b/webui/src/components/ui/dropdown-menu.tsx index 40087d90b..f52b879d2 100644 --- a/webui/src/components/ui/dropdown-menu.tsx +++ b/webui/src/components/ui/dropdown-menu.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { Check, ChevronRight, Circle } from "lucide-react"; +import { Circle } from "lucide-react"; import { floatingItemClassName, @@ -12,52 +12,11 @@ import { cn } from "@/lib/utils"; const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; -const DropdownMenuGroup = DropdownMenuPrimitive.Group; -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; -const DropdownMenuSub = DropdownMenuPrimitive.Sub; const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const menuItemClassName = `${floatingItemClassName} ${floatingItemFocusClassName} cursor-default data-[disabled]:pointer-events-none data-[disabled]:opacity-50`; -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)); -DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; - interface DropdownMenuContentProps extends React.ComponentPropsWithoutRef { portalContainer?: HTMLElement | null; @@ -103,31 +62,6 @@ const DropdownMenuItem = React.forwardRef< )); DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; - const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -183,17 +117,11 @@ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; export { DropdownMenu, - DropdownMenuCheckboxItem, DropdownMenuContent, - DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, DropdownMenuTrigger, }; diff --git a/webui/src/components/ui/separator.tsx b/webui/src/components/ui/separator.tsx deleted file mode 100644 index 4407ae5f1..000000000 --- a/webui/src/components/ui/separator.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; - -import { cn } from "@/lib/utils"; - -const Separator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref, - ) => ( - - ), -); -Separator.displayName = SeparatorPrimitive.Root.displayName; - -export { Separator }; diff --git a/webui/src/globals.css b/webui/src/globals.css index 140909a42..2a9652e60 100644 --- a/webui/src/globals.css +++ b/webui/src/globals.css @@ -235,10 +235,6 @@ height: 0; } - .shadow-inner-right { - box-shadow: inset -9px 0 6px -1px rgb(0 0 0 / 0.02); - } - /* Keep the outer document rhythm clean at message boundaries. */ .markdown-content > :first-child { @apply mt-0; @@ -338,7 +334,6 @@ animation: none; content: ""; } - .markdown-content-streaming > :last-child::after, .streaming-text-fallback::after { animation: none; } @@ -565,52 +560,6 @@ } } - @keyframes cli-app-linked-sheen { - 0% { - transform: translateX(-140%) skewX(-14deg); - opacity: 0; - } - 18% { - opacity: 0.7; - } - 72%, - 100% { - transform: translateX(140%) skewX(-14deg); - opacity: 0; - } - } - .cli-app-linked-chip::after { - content: ""; - position: absolute; - inset: -1px; - pointer-events: none; - background: linear-gradient( - 90deg, - transparent 0%, - hsl(var(--foreground) / 0.14) 46%, - hsl(var(--background) / 0.7) 50%, - hsl(var(--foreground) / 0.12) 54%, - transparent 100% - ); - animation: cli-app-linked-sheen 1.25s ease-out 1; - } - .dark .cli-app-linked-chip::after { - background: linear-gradient( - 90deg, - transparent 0%, - hsl(var(--foreground) / 0.12) 46%, - hsl(var(--background) / 0.5) 50%, - hsl(var(--foreground) / 0.1) 54%, - transparent 100% - ); - } - @media (prefers-reduced-motion: reduce) { - .cli-app-linked-chip::after { - animation: none; - content: none; - } - } - /* Subtle scrollbar that doesn't fight the dark background. */ .scrollbar-thin { scrollbar-width: thin; diff --git a/webui/src/hooks/useAttachedImages.ts b/webui/src/hooks/useAttachedImages.ts index 48d497d8f..a76b9a09c 100644 --- a/webui/src/hooks/useAttachedImages.ts +++ b/webui/src/hooks/useAttachedImages.ts @@ -55,7 +55,6 @@ export type AttachmentError = | "io"; // file read failed at the browser layer export const MAX_ATTACHMENTS_PER_MESSAGE = 4; -export const MAX_IMAGES_PER_MESSAGE = MAX_ATTACHMENTS_PER_MESSAGE; export const MAX_ATTACHMENT_BYTES = 6 * 1024 * 1024; export const MAX_TOTAL_ATTACHMENT_BYTES = 24 * 1024 * 1024;