tsugite-cli 0.12.0__tar.gz → 0.12.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/PKG-INFO +1 -1
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/pyproject.toml +1 -1
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_agent.py +83 -6
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/daemon/test_http_adapter.py +87 -0
- tsugite_cli-0.12.2/tests/e2e/test_history_code_rendering.py +233 -0
- tsugite_cli-0.12.2/tests/e2e/test_markdown_rendering.py +205 -0
- tsugite_cli-0.12.2/tests/e2e/test_message_actions.py +260 -0
- tsugite_cli-0.12.2/tests/e2e/test_scroll_behavior.py +143 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_history_integration.py +77 -2
- tsugite_cli-0.12.2/tests/test_md_agents_json.py +99 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_skill_discovery.py +24 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_runner/history_integration.py +14 -9
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/agent.py +8 -13
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/adapters/http.py +18 -17
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/css/styles.css +64 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/index.html +29 -2
- tsugite_cli-0.12.2/tsugite/daemon/web/js/utils.js +100 -0
- tsugite_cli-0.12.2/tsugite/daemon/web/js/vendor/marked.LICENSE.md +44 -0
- tsugite_cli-0.12.2/tsugite/daemon/web/js/vendor/marked.esm.min.js +9 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/conversation/history.js +30 -14
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/conversation/streaming.js +4 -4
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/conversations.js +27 -3
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/md_agents.py +20 -45
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/skill_discovery.py +11 -2
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/uv.lock +2 -2
- tsugite_cli-0.12.0/tsugite/daemon/web/js/utils.js +0 -102
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/.github/copilot-instructions.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/.github/workflows/ci.yml +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/.github/workflows/docker-publish.yml +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/.github/workflows/pypi-publish.yml +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/.gitignore +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/AGENTS.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/CLAUDE.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/LICENSE +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/README.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/docs/agents.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/docs/plugin-hooks.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/docs/secrets.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/docs/test_agents/prefresh_readme.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/docs/test_agents/template_test.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/docs/test_agents/test_agent.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/docs/test_agents/test_steps.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/mise.toml +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/scripts/backfill_usage.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/scripts/regenerate_schema.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/scripts/update_model_registry.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/README.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/conftest.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_agent_context_tokens.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_agent_ui_events.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_content_blocks.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_executor.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_memory.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_proxy.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_sandbox.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_subprocess_executor.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/core/test_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/daemon/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/daemon/test_session_metadata_api.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/conftest.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_auth.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_chat.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_context_bar.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_draft_persistence.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_history.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_page_load.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_prompt_inspector.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_sessions.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_sidebar_redesign.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/e2e/test_sse_metadata.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/events/test_event_consolidation.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/smoke_test.sh +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_agent_file_hot_loading.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_agent_inheritance.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_agent_parser.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_agent_sessions.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_agent_skills.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_agent_utils.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_agents_tool.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_attachment_deduplication.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_attachments.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_auto_context_handler.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_auto_discovery.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_background_task_status.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_background_tasks.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_builtin_agent_paths.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_builtin_agents.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_cache_control.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_chat_cli.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_chat_error_handling.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_claude_code_attachments.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_claude_code_provider.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_cli.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_cli_arguments.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_cli_rendering.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_cli_subcommands.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_completion_callbacks.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_config.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_continuation.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_custom_shell_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_custom_ui.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_auth.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_compaction_scheduler.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_config.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_history_persistence.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_memory.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_push.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_scheduler.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_session_isolation.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_daemon_unified_sessions.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_discord_progress.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_error_display.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_file_references.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_file_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_history.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_history_models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_history_performance.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_history_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_hooks.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_http_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_interaction_backends.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_interactive_context.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_interactive_tool.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_jsonl_ui.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_list_agents_tool.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_mcp_client.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_mcp_server.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_model_registry.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_multi_agent.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_multistep_agents.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_orchestrator_heartbeat.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_plugins.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_prompt_snapshot.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_provider_registry.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_reasoning_models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_renderer.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_rendering_scenarios.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_repl_commands.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_repl_completer.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_repl_handler.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_retry_system.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_run_if.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_schedule_model_override.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_scheduler_history_injection.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_schema.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_secret_access_event.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_secrets.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_security_phase1.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_send_message.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_session_metadata.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_session_orchestrator_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_skill_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_stdin.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_subagent_subprocess.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_tmux_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_tool_directives.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_tool_registry.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_workspace_auto_continue.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_workspace_cwd.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tests/test_workspace_discovery.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_runner/runner.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/attachments/youtube.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cache.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/mcp.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/serve.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/config.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/console.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/constants.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/claude_code.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/subprocess_executor.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/adapters/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/adapters/base.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/adapters/discord.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/adapters/scheduler_adapter.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/auth.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/commands.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/compaction_scheduler.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/config.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/gateway.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/memory.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/push.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/scheduler.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/session_runner.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/session_store.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/css/responsive.css +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/css/theme.css +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/icons/icon-192.png +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/icons/icon-512.png +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/api.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/app.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/conversation/attachments.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/conversation/input.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/conversation/sessions.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/file-editor.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/schedules.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/usage.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/webhooks.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/js/views/workspace.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/manifest.json +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/web/sw.js +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/daemon/webhook_store.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/history/reconstruction.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/history/storage.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/mcp_client.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/mcp_config.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/mcp_server.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/options.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/anthropic.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/claude_code.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/model_registry.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/openai_compat.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/http.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/scratchpad.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/sessions.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tools/tmux.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/utils.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/workspace/context.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.12.0 → tsugite_cli-0.12.2}/tsugite/workspace/templates.py +0 -0
|
@@ -207,6 +207,35 @@ async def test_agent_format_error_loop_bails_early():
|
|
|
207
207
|
assert len(agent.memory.steps) <= 3
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_agent_format_error_preserves_content_blocks_on_step():
|
|
212
|
+
"""When the LLM emits content blocks but no code fence, the step should still carry them."""
|
|
213
|
+
|
|
214
|
+
agent = TsugiteAgent(
|
|
215
|
+
model_string="openai:gpt-4o-mini",
|
|
216
|
+
tools=[],
|
|
217
|
+
instructions="",
|
|
218
|
+
max_turns=5,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
format_error = (
|
|
222
|
+
"Thought: writing without code\n\n" '<content name="reply">Stopped before creating anything.</content>'
|
|
223
|
+
)
|
|
224
|
+
_patch_provider(
|
|
225
|
+
agent,
|
|
226
|
+
side_effect=[
|
|
227
|
+
_mock_response(format_error),
|
|
228
|
+
_mock_response("```python\nfinal_answer('done')\n```"),
|
|
229
|
+
],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
await agent.run("Some task")
|
|
233
|
+
|
|
234
|
+
format_error_step = agent.memory.steps[0]
|
|
235
|
+
assert format_error_step.code == ""
|
|
236
|
+
assert format_error_step.content_blocks == {"reply": "Stopped before creating anything."}
|
|
237
|
+
|
|
238
|
+
|
|
210
239
|
@pytest.mark.asyncio
|
|
211
240
|
async def test_agent_format_error_resets_on_valid_code():
|
|
212
241
|
"""Test format error counter resets when model produces valid code."""
|
|
@@ -428,6 +457,54 @@ final_answer(42)
|
|
|
428
457
|
assert "final_answer(42)" in parsed.code
|
|
429
458
|
|
|
430
459
|
|
|
460
|
+
@pytest.mark.asyncio
|
|
461
|
+
async def test_agent_parse_response_prose_only():
|
|
462
|
+
"""Prose without a Thought: prefix or code block should be captured as thought."""
|
|
463
|
+
agent = TsugiteAgent(
|
|
464
|
+
model_string="openai:gpt-4o-mini",
|
|
465
|
+
tools=[],
|
|
466
|
+
instructions="",
|
|
467
|
+
max_turns=5,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
parsed = agent._parse_response_from_text("You're welcome!")
|
|
471
|
+
assert parsed.thought == "You're welcome!"
|
|
472
|
+
assert parsed.code == ""
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@pytest.mark.asyncio
|
|
476
|
+
async def test_agent_parse_response_prose_with_code_no_prefix():
|
|
477
|
+
"""Prose preceding a code block (no Thought: prefix) should still be captured."""
|
|
478
|
+
agent = TsugiteAgent(
|
|
479
|
+
model_string="openai:gpt-4o-mini",
|
|
480
|
+
tools=[],
|
|
481
|
+
instructions="",
|
|
482
|
+
max_turns=5,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
parsed = agent._parse_response_from_text(
|
|
486
|
+
"Sure thing.\n\n```python\nx = 1\nfinal_answer(x)\n```"
|
|
487
|
+
)
|
|
488
|
+
assert parsed.thought == "Sure thing."
|
|
489
|
+
assert "x = 1" in parsed.code
|
|
490
|
+
assert "final_answer(x)" in parsed.code
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@pytest.mark.asyncio
|
|
494
|
+
async def test_agent_parse_response_code_only():
|
|
495
|
+
"""A code-only response (no prose) should still yield empty thought."""
|
|
496
|
+
agent = TsugiteAgent(
|
|
497
|
+
model_string="openai:gpt-4o-mini",
|
|
498
|
+
tools=[],
|
|
499
|
+
instructions="",
|
|
500
|
+
max_turns=5,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
parsed = agent._parse_response_from_text("```python\nfoo()\n```")
|
|
504
|
+
assert parsed.thought == ""
|
|
505
|
+
assert parsed.code == "foo()"
|
|
506
|
+
|
|
507
|
+
|
|
431
508
|
@pytest.mark.asyncio
|
|
432
509
|
async def test_agent_model_kwargs():
|
|
433
510
|
"""Test that model_kwargs are correctly filtered for reasoning models."""
|
|
@@ -675,9 +752,9 @@ def test_tool_execution_no_task_warnings():
|
|
|
675
752
|
)
|
|
676
753
|
|
|
677
754
|
assert "Task pending" not in filtered_stderr, f"Unexpected Task pending warning:\n{stderr_output}"
|
|
678
|
-
assert
|
|
679
|
-
|
|
680
|
-
)
|
|
755
|
+
assert (
|
|
756
|
+
"Task exception was never retrieved" not in filtered_stderr
|
|
757
|
+
), f"Unexpected Task exception warning:\n{stderr_output}"
|
|
681
758
|
|
|
682
759
|
|
|
683
760
|
def test_tool_exception_propagation_from_async():
|
|
@@ -727,6 +804,6 @@ def test_tool_exception_propagation_from_async():
|
|
|
727
804
|
line for line in stderr_output.split("\n") if "failing_tool" in line or "never retrieved" in line
|
|
728
805
|
)
|
|
729
806
|
|
|
730
|
-
assert
|
|
731
|
-
|
|
732
|
-
)
|
|
807
|
+
assert (
|
|
808
|
+
"exception was never retrieved" not in filtered_stderr.lower()
|
|
809
|
+
), f"Exception handling broken:\n{stderr_output}"
|
|
@@ -408,6 +408,93 @@ class TestHistoryEndpoint:
|
|
|
408
408
|
assert len(turn_data) == 1
|
|
409
409
|
assert "reactions" not in turn_data[0]
|
|
410
410
|
|
|
411
|
+
def test_history_detail_attaches_content_blocks_per_message(self, client, test_token, mock_adapter, tmp_path):
|
|
412
|
+
"""When detail=true, each assistant message carries its own content_blocks dict."""
|
|
413
|
+
from tsugite.history.storage import SessionStorage
|
|
414
|
+
|
|
415
|
+
session = mock_adapter.session_store.get_or_create_interactive("web-anonymous", "test-agent")
|
|
416
|
+
session_id = session.id
|
|
417
|
+
history_dir = tmp_path / "history"
|
|
418
|
+
history_dir.mkdir()
|
|
419
|
+
session_path = history_dir / f"{session_id}.jsonl"
|
|
420
|
+
|
|
421
|
+
storage = SessionStorage.create("test-agent", model="test", session_path=session_path)
|
|
422
|
+
storage.record_turn(
|
|
423
|
+
messages=[
|
|
424
|
+
{"role": "user", "content": "go"},
|
|
425
|
+
{"role": "assistant", "content": "```python\ninspect()\n```"},
|
|
426
|
+
{
|
|
427
|
+
"role": "user",
|
|
428
|
+
"content": '<tsugite_execution_result status="success" duration_ms="3"><output>ok</output></tsugite_execution_result>',
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
"role": "assistant",
|
|
432
|
+
"content": (
|
|
433
|
+
'```python\nfinal_answer(result="done")\n```\n\n'
|
|
434
|
+
'<content name="reply">Stopped before creating anything.</content>'
|
|
435
|
+
),
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
final_answer="done",
|
|
439
|
+
)
|
|
440
|
+
session_path.rename(history_dir / f"{session_id}.jsonl")
|
|
441
|
+
|
|
442
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
443
|
+
resp = client.get(
|
|
444
|
+
"/api/agents/test-agent/history?user_id=web-anonymous&detail=true",
|
|
445
|
+
headers={"Authorization": f"Bearer {test_token}"},
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
assert resp.status_code == 200
|
|
449
|
+
data = resp.json()
|
|
450
|
+
turn = next(t for t in data["turns"] if t.get("user"))
|
|
451
|
+
|
|
452
|
+
# Turn-level aggregate still available for backward compat
|
|
453
|
+
assert turn.get("content_blocks") == {"reply": "Stopped before creating anything."}
|
|
454
|
+
|
|
455
|
+
# Per-message attachment is the new source of truth for rendering
|
|
456
|
+
assistant_msgs = [m for m in turn["messages"] if m.get("role") == "assistant"]
|
|
457
|
+
assert assistant_msgs[0].get("content_blocks", {}) == {}
|
|
458
|
+
assert assistant_msgs[1]["content_blocks"] == {"reply": "Stopped before creating anything."}
|
|
459
|
+
|
|
460
|
+
def test_history_detail_preserves_execution_result_with_attributes(
|
|
461
|
+
self, client, test_token, mock_adapter, tmp_path
|
|
462
|
+
):
|
|
463
|
+
"""Execution-result tags with attributes must survive the payload unmodified."""
|
|
464
|
+
from tsugite.history.storage import SessionStorage
|
|
465
|
+
|
|
466
|
+
session = mock_adapter.session_store.get_or_create_interactive("web-anonymous", "test-agent")
|
|
467
|
+
session_id = session.id
|
|
468
|
+
history_dir = tmp_path / "history"
|
|
469
|
+
history_dir.mkdir()
|
|
470
|
+
session_path = history_dir / f"{session_id}.jsonl"
|
|
471
|
+
|
|
472
|
+
observation = (
|
|
473
|
+
'<tsugite_execution_result status="success" duration_ms="12">'
|
|
474
|
+
"<output>ran</output></tsugite_execution_result>"
|
|
475
|
+
)
|
|
476
|
+
storage = SessionStorage.create("test-agent", model="test", session_path=session_path)
|
|
477
|
+
storage.record_turn(
|
|
478
|
+
messages=[
|
|
479
|
+
{"role": "user", "content": "go"},
|
|
480
|
+
{"role": "assistant", "content": "```python\nprint('hi')\n```"},
|
|
481
|
+
{"role": "user", "content": observation},
|
|
482
|
+
],
|
|
483
|
+
final_answer="done",
|
|
484
|
+
)
|
|
485
|
+
session_path.rename(history_dir / f"{session_id}.jsonl")
|
|
486
|
+
|
|
487
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
488
|
+
resp = client.get(
|
|
489
|
+
"/api/agents/test-agent/history?user_id=web-anonymous&detail=true",
|
|
490
|
+
headers={"Authorization": f"Bearer {test_token}"},
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
assert resp.status_code == 200
|
|
494
|
+
turn = next(t for t in resp.json()["turns"] if t.get("user"))
|
|
495
|
+
user_msgs = [m for m in turn["messages"] if m.get("role") == "user"]
|
|
496
|
+
assert any(m.get("content") == observation for m in user_msgs)
|
|
497
|
+
|
|
411
498
|
|
|
412
499
|
class TestWebUI:
|
|
413
500
|
def test_serve_ui(self, client):
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""E2E tests for step-trace rendering: code truncation, content blocks, and tool results."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from tsugite.history.storage import SessionStorage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _seed_isolated_turn(page, e2e_adapter, e2e_tmp, label, messages, final_answer="done"):
|
|
9
|
+
"""Seed a fresh session (unique user) with one crafted turn and return (history_dir, user_id, session_id)."""
|
|
10
|
+
unique_user = f"web-user-{label}"
|
|
11
|
+
session = e2e_adapter.session_store.get_or_create_interactive(unique_user, "test-agent")
|
|
12
|
+
history_dir = e2e_tmp / f"history-{label}"
|
|
13
|
+
history_dir.mkdir(exist_ok=True)
|
|
14
|
+
session_path = history_dir / f"{session.id}.jsonl"
|
|
15
|
+
|
|
16
|
+
storage = SessionStorage.create("test-agent", model="test", session_path=session_path)
|
|
17
|
+
storage.record_turn(messages=messages, final_answer=final_answer)
|
|
18
|
+
return history_dir, unique_user, session.id
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _open_progress_trace(page, user_id, session_id):
|
|
22
|
+
"""Reload as `user_id`, navigate to the session, and expand the progress-done summary."""
|
|
23
|
+
page.evaluate(f"localStorage.setItem('tsugite_user_id', {user_id!r})")
|
|
24
|
+
page.goto(page.url.split("#")[0] + f"#conversations?session={session_id}")
|
|
25
|
+
page.reload()
|
|
26
|
+
page.wait_for_function("!Alpine.store('app').authRequired", timeout=5000)
|
|
27
|
+
page.wait_for_function(f"Alpine.store('app').userId === {user_id!r}", timeout=3000)
|
|
28
|
+
page.wait_for_selector(".msg.user", timeout=5000)
|
|
29
|
+
page.wait_for_selector(".msg.progress", timeout=5000)
|
|
30
|
+
summary = page.locator(".msg.progress .tool-summary").first
|
|
31
|
+
summary.click()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_history_code_block_not_truncated(authenticated_page, e2e_adapter, e2e_tmp):
|
|
35
|
+
"""Long code blocks must render in full on history reload, no trailing ellipsis."""
|
|
36
|
+
page = authenticated_page
|
|
37
|
+
|
|
38
|
+
long_code = "\n".join([f"line_{i} = 'x' * 80" for i in range(40)])
|
|
39
|
+
assistant_msg = f"```python\n{long_code}\n```"
|
|
40
|
+
history_dir, user_id, session_id = _seed_isolated_turn(
|
|
41
|
+
page,
|
|
42
|
+
e2e_adapter,
|
|
43
|
+
e2e_tmp,
|
|
44
|
+
"truncate",
|
|
45
|
+
messages=[
|
|
46
|
+
{"role": "user", "content": "go"},
|
|
47
|
+
{"role": "assistant", "content": assistant_msg},
|
|
48
|
+
],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
52
|
+
_open_progress_trace(page, user_id, session_id)
|
|
53
|
+
code_details = page.locator(".msg.progress .tool-steps li details").first
|
|
54
|
+
code_details.click()
|
|
55
|
+
code_text = page.locator(".msg.progress .tool-steps li details pre code").first.text_content()
|
|
56
|
+
|
|
57
|
+
assert "line_39 = 'x'" in code_text
|
|
58
|
+
assert not code_text.endswith("...")
|
|
59
|
+
assert len(code_text) >= len(long_code)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_history_content_block_renders_next_to_its_code(authenticated_page, e2e_adapter, e2e_tmp):
|
|
63
|
+
"""Content blocks should appear inline with the assistant message that declared them."""
|
|
64
|
+
page = authenticated_page
|
|
65
|
+
|
|
66
|
+
history_dir, user_id, session_id = _seed_isolated_turn(
|
|
67
|
+
page,
|
|
68
|
+
e2e_adapter,
|
|
69
|
+
e2e_tmp,
|
|
70
|
+
"inline",
|
|
71
|
+
messages=[
|
|
72
|
+
{"role": "user", "content": "go"},
|
|
73
|
+
{"role": "assistant", "content": "```python\ninvestigate()\n```"},
|
|
74
|
+
{
|
|
75
|
+
"role": "user",
|
|
76
|
+
"content": '<tsugite_execution_result status="success"><output>ok</output></tsugite_execution_result>',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"role": "assistant",
|
|
80
|
+
"content": (
|
|
81
|
+
'```python\nfinal_answer(result="done")\n```\n\n'
|
|
82
|
+
'<content name="reply">Stopped before creating.</content>'
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
89
|
+
_open_progress_trace(page, user_id, session_id)
|
|
90
|
+
|
|
91
|
+
steps = page.locator(".msg.progress .tool-steps > li")
|
|
92
|
+
step_count = steps.count()
|
|
93
|
+
# Find the index of the content block and the 2nd code step
|
|
94
|
+
cb_index = None
|
|
95
|
+
code_indices = []
|
|
96
|
+
for i in range(step_count):
|
|
97
|
+
li = steps.nth(i)
|
|
98
|
+
if li.locator("details.content-block").count() > 0:
|
|
99
|
+
cb_index = i
|
|
100
|
+
elif "code" in (li.text_content() or ""):
|
|
101
|
+
code_indices.append(i)
|
|
102
|
+
|
|
103
|
+
assert cb_index is not None, "content block should be rendered"
|
|
104
|
+
assert len(code_indices) >= 2, "both code steps should render"
|
|
105
|
+
# content block came from the 2nd code step, so it must appear immediately after it,
|
|
106
|
+
# not pushed to the end of the trace
|
|
107
|
+
assert cb_index == code_indices[1] + 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_history_tool_result_visible_between_code_steps(authenticated_page, e2e_adapter, e2e_tmp):
|
|
111
|
+
"""tsugite_execution_result with attributes must render a tool_result step."""
|
|
112
|
+
page = authenticated_page
|
|
113
|
+
|
|
114
|
+
observation = (
|
|
115
|
+
'<tsugite_execution_result status="success" duration_ms="7">'
|
|
116
|
+
"<output>hello world</output></tsugite_execution_result>"
|
|
117
|
+
)
|
|
118
|
+
history_dir, user_id, session_id = _seed_isolated_turn(
|
|
119
|
+
page,
|
|
120
|
+
e2e_adapter,
|
|
121
|
+
e2e_tmp,
|
|
122
|
+
"toolresult",
|
|
123
|
+
messages=[
|
|
124
|
+
{"role": "user", "content": "go"},
|
|
125
|
+
{"role": "assistant", "content": "```python\nprint('hi')\n```"},
|
|
126
|
+
{"role": "user", "content": observation},
|
|
127
|
+
{"role": "assistant", "content": "```python\nfinal_answer('done')\n```"},
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
132
|
+
_open_progress_trace(page, user_id, session_id)
|
|
133
|
+
|
|
134
|
+
summaries = page.locator(".msg.progress .tool-steps > li details > summary")
|
|
135
|
+
labels = [summaries.nth(i).text_content() or "" for i in range(summaries.count())]
|
|
136
|
+
|
|
137
|
+
# Two code steps plus a result step in between
|
|
138
|
+
code_hits = [i for i, s in enumerate(labels) if "code" in s]
|
|
139
|
+
result_hits = [i for i, s in enumerate(labels) if "result" in s]
|
|
140
|
+
assert len(code_hits) == 2
|
|
141
|
+
assert len(result_hits) == 1
|
|
142
|
+
assert code_hits[0] < result_hits[0] < code_hits[1]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_history_content_block_survives_no_code_turn(authenticated_page, e2e_adapter, e2e_tmp):
|
|
146
|
+
"""A sub-turn that only emitted content blocks (no code) must still render them after reload."""
|
|
147
|
+
page = authenticated_page
|
|
148
|
+
|
|
149
|
+
history_dir, user_id, session_id = _seed_isolated_turn(
|
|
150
|
+
page,
|
|
151
|
+
e2e_adapter,
|
|
152
|
+
e2e_tmp,
|
|
153
|
+
"nocode",
|
|
154
|
+
messages=[
|
|
155
|
+
{"role": "user", "content": "go"},
|
|
156
|
+
{
|
|
157
|
+
"role": "assistant",
|
|
158
|
+
"content": '<content name="reply">Just a content block, no code.</content>',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"role": "user",
|
|
162
|
+
"content": '<tsugite_execution_result status="error"><error>no code</error></tsugite_execution_result>',
|
|
163
|
+
},
|
|
164
|
+
{"role": "assistant", "content": "```python\nfinal_answer('done')\n```"},
|
|
165
|
+
],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
169
|
+
_open_progress_trace(page, user_id, session_id)
|
|
170
|
+
content_blocks = page.locator(".msg.progress .tool-steps details.content-block")
|
|
171
|
+
assert content_blocks.count() == 1
|
|
172
|
+
content_blocks.first.click()
|
|
173
|
+
pre_text = page.locator(".msg.progress .tool-steps details.content-block pre code").first.text_content()
|
|
174
|
+
assert "Just a content block, no code." in pre_text
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_history_prose_only_assistant_message_renders(authenticated_page, e2e_adapter, e2e_tmp):
|
|
178
|
+
"""An assistant message that is prose only (no code block) must still render on reload.
|
|
179
|
+
|
|
180
|
+
Mirrors the real stored shape from _build_turn_messages: a prose-only step (thought as
|
|
181
|
+
assistant message), then the format-error observation, then the corrected final_answer
|
|
182
|
+
code step, its observation, and finally the plain-text result assistant message.
|
|
183
|
+
"""
|
|
184
|
+
page = authenticated_page
|
|
185
|
+
|
|
186
|
+
history_dir, user_id, session_id = _seed_isolated_turn(
|
|
187
|
+
page,
|
|
188
|
+
e2e_adapter,
|
|
189
|
+
e2e_tmp,
|
|
190
|
+
"prose",
|
|
191
|
+
messages=[
|
|
192
|
+
{"role": "user", "content": "thanks"},
|
|
193
|
+
{"role": "assistant", "content": "You're welcome!"},
|
|
194
|
+
{
|
|
195
|
+
"role": "user",
|
|
196
|
+
"content": (
|
|
197
|
+
'<tsugite_execution_result status="error">'
|
|
198
|
+
"<error>Format Error: You must respond with a Python code block.</error>"
|
|
199
|
+
"</tsugite_execution_result>"
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
{"role": "assistant", "content": '```python\nfinal_answer("You\'re welcome!")\n```'},
|
|
203
|
+
{
|
|
204
|
+
"role": "user",
|
|
205
|
+
"content": (
|
|
206
|
+
'<tsugite_execution_result status="success">'
|
|
207
|
+
"<output></output></tsugite_execution_result>"
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
{"role": "assistant", "content": "You're welcome!"},
|
|
211
|
+
],
|
|
212
|
+
final_answer="You're welcome!",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
216
|
+
_open_progress_trace(page, user_id, session_id)
|
|
217
|
+
|
|
218
|
+
summaries = page.locator(".msg.progress .tool-steps > li details > summary")
|
|
219
|
+
labels = [summaries.nth(i).text_content() or "" for i in range(summaries.count())]
|
|
220
|
+
thought_hits = [i for i, s in enumerate(labels) if "thought" in s.lower()]
|
|
221
|
+
assert len(thought_hits) == 1, f"expected exactly one 'thought' step, got summaries: {labels}"
|
|
222
|
+
|
|
223
|
+
thought_idx = thought_hits[0]
|
|
224
|
+
result_hits = [i for i, s in enumerate(labels) if "result" in s.lower()]
|
|
225
|
+
assert result_hits, f"expected a result step, got summaries: {labels}"
|
|
226
|
+
assert thought_idx < result_hits[0], (
|
|
227
|
+
f"thought must appear before the format-error result, got: {labels}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
thought_details = page.locator(".msg.progress .tool-steps > li details").nth(thought_idx)
|
|
231
|
+
thought_details.click()
|
|
232
|
+
thought_text = thought_details.locator("pre code").first.text_content()
|
|
233
|
+
assert "You're welcome!" in thought_text
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""E2E tests for markdown rendering in `.msg.agent` bubbles.
|
|
2
|
+
|
|
3
|
+
Written before swapping the hand-rolled renderer in `utils.js` for the `marked`
|
|
4
|
+
library. Regression cases pin pre-existing behavior; the table/alignment/style
|
|
5
|
+
cases drive the new behavior.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
from tsugite.history.storage import SessionStorage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _seed_agent_turn(e2e_adapter, e2e_tmp, label, final_answer):
|
|
14
|
+
"""Seed a fresh session whose single turn has the given markdown final_answer."""
|
|
15
|
+
unique_user = f"md-user-{label}"
|
|
16
|
+
session = e2e_adapter.session_store.get_or_create_interactive(unique_user, "test-agent")
|
|
17
|
+
history_dir = e2e_tmp / f"history-{label}"
|
|
18
|
+
history_dir.mkdir(exist_ok=True)
|
|
19
|
+
session_path = history_dir / f"{session.id}.jsonl"
|
|
20
|
+
|
|
21
|
+
storage = SessionStorage.create("test-agent", model="test", session_path=session_path)
|
|
22
|
+
storage.record_turn(
|
|
23
|
+
messages=[{"role": "user", "content": "show"}],
|
|
24
|
+
final_answer=final_answer,
|
|
25
|
+
)
|
|
26
|
+
return history_dir, unique_user, session.id
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _open_session(page, user_id, session_id):
|
|
30
|
+
page.evaluate(f"localStorage.setItem('tsugite_user_id', {user_id!r})")
|
|
31
|
+
page.goto(page.url.split("#")[0] + f"#conversations?session={session_id}")
|
|
32
|
+
page.reload()
|
|
33
|
+
page.wait_for_function("!Alpine.store('app').authRequired", timeout=5000)
|
|
34
|
+
page.wait_for_function(f"Alpine.store('app').userId === {user_id!r}", timeout=3000)
|
|
35
|
+
page.wait_for_selector(".msg.agent", timeout=5000)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_markdown_regression_basic_formatting(authenticated_page, e2e_adapter, e2e_tmp):
|
|
39
|
+
"""All pre-existing markdown features still render after the parser swap."""
|
|
40
|
+
page = authenticated_page
|
|
41
|
+
|
|
42
|
+
md = (
|
|
43
|
+
"# H1\n"
|
|
44
|
+
"## H2\n"
|
|
45
|
+
"### H3\n"
|
|
46
|
+
"#### H4\n\n"
|
|
47
|
+
"A paragraph with **bold**, *italic*, and `inline`.\n\n"
|
|
48
|
+
"- ul item 1\n"
|
|
49
|
+
"- ul item 2\n\n"
|
|
50
|
+
"1. ol item 1\n"
|
|
51
|
+
"2. ol item 2\n\n"
|
|
52
|
+
"> a blockquote\n\n"
|
|
53
|
+
"---\n\n"
|
|
54
|
+
"A [link](https://example.com).\n\n"
|
|
55
|
+
"```python\nprint('hello')\n```\n"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
history_dir, user_id, session_id = _seed_agent_turn(e2e_adapter, e2e_tmp, "regression", md)
|
|
59
|
+
|
|
60
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
61
|
+
_open_session(page, user_id, session_id)
|
|
62
|
+
agent = page.locator(".msg.agent").last
|
|
63
|
+
|
|
64
|
+
assert agent.locator("h1").count() >= 1
|
|
65
|
+
assert agent.locator("h2").count() >= 1
|
|
66
|
+
assert agent.locator("h3").count() >= 1
|
|
67
|
+
assert agent.locator("h4").count() >= 1
|
|
68
|
+
|
|
69
|
+
assert agent.locator("strong").first.text_content() == "bold"
|
|
70
|
+
assert agent.locator("em").first.text_content() == "italic"
|
|
71
|
+
assert agent.locator("code").first.text_content() == "inline"
|
|
72
|
+
|
|
73
|
+
assert agent.locator("ul > li").count() == 2
|
|
74
|
+
assert agent.locator("ol > li").count() == 2
|
|
75
|
+
|
|
76
|
+
assert agent.locator("blockquote").count() == 1
|
|
77
|
+
assert agent.locator("hr").count() == 1
|
|
78
|
+
|
|
79
|
+
link = agent.locator("a").first
|
|
80
|
+
assert link.get_attribute("href") == "https://example.com"
|
|
81
|
+
|
|
82
|
+
pre_code = agent.locator("pre code")
|
|
83
|
+
assert pre_code.count() == 1
|
|
84
|
+
assert "print('hello')" in (pre_code.first.text_content() or "")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_markdown_gfm_table_renders(authenticated_page, e2e_adapter, e2e_tmp):
|
|
88
|
+
"""Simple GFM table produces <table><thead><th>...<tbody><tr><td>."""
|
|
89
|
+
page = authenticated_page
|
|
90
|
+
|
|
91
|
+
md = (
|
|
92
|
+
"| Name | Score |\n"
|
|
93
|
+
"| ---- | ----- |\n"
|
|
94
|
+
"| Alice | 10 |\n"
|
|
95
|
+
"| Bob | 20 |\n"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
history_dir, user_id, session_id = _seed_agent_turn(e2e_adapter, e2e_tmp, "table", md)
|
|
99
|
+
|
|
100
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
101
|
+
_open_session(page, user_id, session_id)
|
|
102
|
+
agent = page.locator(".msg.agent").last
|
|
103
|
+
|
|
104
|
+
assert agent.locator("table").count() == 1
|
|
105
|
+
assert agent.locator("table thead th").count() == 2
|
|
106
|
+
assert agent.locator("table tbody tr").count() == 2
|
|
107
|
+
assert (agent.locator("table thead th").first.text_content() or "").strip() == "Name"
|
|
108
|
+
last_cell = agent.locator("table tbody tr").nth(1).locator("td").last
|
|
109
|
+
assert (last_cell.text_content() or "").strip() == "20"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_markdown_gfm_table_alignment(authenticated_page, e2e_adapter, e2e_tmp):
|
|
113
|
+
"""GFM alignment syntax yields text-align on th/td via inline style."""
|
|
114
|
+
page = authenticated_page
|
|
115
|
+
|
|
116
|
+
md = (
|
|
117
|
+
"| L | C | R |\n"
|
|
118
|
+
"| :--- | :---: | ---: |\n"
|
|
119
|
+
"| a | b | c |\n"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
history_dir, user_id, session_id = _seed_agent_turn(e2e_adapter, e2e_tmp, "align", md)
|
|
123
|
+
|
|
124
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
125
|
+
_open_session(page, user_id, session_id)
|
|
126
|
+
agent = page.locator(".msg.agent").last
|
|
127
|
+
|
|
128
|
+
ths = agent.locator("table thead th")
|
|
129
|
+
tds = agent.locator("table tbody tr").first.locator("td")
|
|
130
|
+
|
|
131
|
+
def _align(loc, i):
|
|
132
|
+
return loc.nth(i).evaluate("el => getComputedStyle(el).textAlign")
|
|
133
|
+
|
|
134
|
+
assert _align(ths, 0) in ("left", "start", "-webkit-left")
|
|
135
|
+
assert _align(ths, 1) in ("center", "-webkit-center")
|
|
136
|
+
assert _align(ths, 2) in ("right", "end", "-webkit-right")
|
|
137
|
+
assert _align(tds, 0) in ("left", "start", "-webkit-left")
|
|
138
|
+
assert _align(tds, 1) in ("center", "-webkit-center")
|
|
139
|
+
assert _align(tds, 2) in ("right", "end", "-webkit-right")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_markdown_table_styling(authenticated_page, e2e_adapter, e2e_tmp):
|
|
143
|
+
"""Computed CSS matches the design: no uppercase header, last row no border."""
|
|
144
|
+
page = authenticated_page
|
|
145
|
+
|
|
146
|
+
md = (
|
|
147
|
+
"| h1 | h2 |\n"
|
|
148
|
+
"| --- | --- |\n"
|
|
149
|
+
"| a | b |\n"
|
|
150
|
+
"| c | d |\n"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
history_dir, user_id, session_id = _seed_agent_turn(e2e_adapter, e2e_tmp, "style", md)
|
|
154
|
+
|
|
155
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
156
|
+
_open_session(page, user_id, session_id)
|
|
157
|
+
agent = page.locator(".msg.agent").last
|
|
158
|
+
|
|
159
|
+
th = agent.locator("table th").first
|
|
160
|
+
assert th.evaluate("el => getComputedStyle(el).textTransform") == "none"
|
|
161
|
+
weight = th.evaluate("el => getComputedStyle(el).fontWeight")
|
|
162
|
+
assert weight in ("600", "700", "bold"), f"unexpected th font-weight: {weight}"
|
|
163
|
+
|
|
164
|
+
last_td = agent.locator("table tbody tr").last.locator("td").first
|
|
165
|
+
border = last_td.evaluate(
|
|
166
|
+
"el => ({ style: getComputedStyle(el).borderBottomStyle, "
|
|
167
|
+
"width: getComputedStyle(el).borderBottomWidth })"
|
|
168
|
+
)
|
|
169
|
+
assert border["style"] == "none" or border["width"] == "0px", (
|
|
170
|
+
f"last row should have no bottom border; got {border}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_markdown_wide_table_scrolls_inside_bubble(authenticated_page, e2e_adapter, e2e_tmp):
|
|
175
|
+
"""A wide table scrolls inside the bubble; the page itself does not overflow."""
|
|
176
|
+
page = authenticated_page
|
|
177
|
+
|
|
178
|
+
cols = 8
|
|
179
|
+
header = "| " + " | ".join(f"col{i}" for i in range(cols)) + " |\n"
|
|
180
|
+
sep = "| " + " | ".join("---" for _ in range(cols)) + " |\n"
|
|
181
|
+
row = "| " + " | ".join("very long cell content " * 3 for _ in range(cols)) + " |\n"
|
|
182
|
+
md = header + sep + row + row
|
|
183
|
+
|
|
184
|
+
history_dir, user_id, session_id = _seed_agent_turn(e2e_adapter, e2e_tmp, "wide-table", md)
|
|
185
|
+
|
|
186
|
+
page.set_viewport_size({"width": 400, "height": 800})
|
|
187
|
+
|
|
188
|
+
with patch("tsugite.daemon.adapters.http.get_history_dir", return_value=history_dir):
|
|
189
|
+
_open_session(page, user_id, session_id)
|
|
190
|
+
table = page.locator(".msg.agent table").last
|
|
191
|
+
|
|
192
|
+
dims = table.evaluate(
|
|
193
|
+
"el => ({ scrollWidth: el.scrollWidth, clientWidth: el.clientWidth })"
|
|
194
|
+
)
|
|
195
|
+
assert dims["scrollWidth"] > dims["clientWidth"], (
|
|
196
|
+
f"expected table to be horizontally scrollable; got {dims}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
page_dims = page.evaluate(
|
|
200
|
+
"() => ({ scrollWidth: document.documentElement.scrollWidth, "
|
|
201
|
+
"clientWidth: document.documentElement.clientWidth })"
|
|
202
|
+
)
|
|
203
|
+
assert page_dims["scrollWidth"] <= page_dims["clientWidth"] + 1, (
|
|
204
|
+
f"page itself should not horizontally scroll; got {page_dims}"
|
|
205
|
+
)
|