tsugite-cli 0.8.0__tar.gz → 0.9.1__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.8.0 → tsugite_cli-0.9.1}/PKG-INFO +1 -1
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/pyproject.toml +1 -1
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cache_control.py +54 -23
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_continuation.py +3 -2
- tsugite_cli-0.9.1/tests/test_tmux_tools.py +397 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/history_integration.py +56 -1
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/models.py +1 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/runner.py +64 -14
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/default.md +18 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/agent.py +104 -25
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/claude_code.py +29 -6
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/base.py +28 -3
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/discord.py +61 -1
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/http.py +71 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/scheduler_adapter.py +1 -0
- tsugite_cli-0.9.1/tsugite/daemon/commands.py +168 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/config.py +1 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/memory.py +50 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/session_runner.py +17 -3
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/session_store.py +4 -1
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/styles.css +28 -5
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/index.html +105 -33
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/conversations.js +227 -38
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/reconstruction.py +14 -6
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/hooks.py +11 -2
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/renderer.py +11 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/__init__.py +5 -1
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/sessions.py +16 -0
- tsugite_cli-0.9.1/tsugite/tools/tmux.py +311 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/uv.lock +13 -13
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/copilot-instructions.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/workflows/ci.yml +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/workflows/docker-publish.yml +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/workflows/pypi-publish.yml +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.gitignore +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/AGENTS.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/CLAUDE.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/LICENSE +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/README.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/scripts/regenerate_schema.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/README.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/conftest.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_agent.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_agent_ui_events.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_content_blocks.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_executor.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_memory.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_proxy.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_sandbox.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_subprocess_executor.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/daemon/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/daemon/test_http_adapter.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/events/test_event_consolidation.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/smoke_test.sh +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_file_hot_loading.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_inheritance.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_parser.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_sessions.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_skills.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_utils.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agents_tool.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_attachment_deduplication.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_attachments.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_auto_context_handler.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_auto_discovery.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_background_task_status.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_background_tasks.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_builtin_agent_paths.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_builtin_agents.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_chat_cli.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_chat_error_handling.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_claude_code_attachments.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_claude_code_provider.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli_arguments.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli_rendering.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli_subcommands.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_config.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_custom_shell_tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_custom_ui.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_compaction_scheduler.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_config.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_history_persistence.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_memory.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_push.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_scheduler.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_session_isolation.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_unified_sessions.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_discord_progress.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_error_display.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_file_references.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_file_tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_integration.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_models.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_performance.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_hooks.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_http_tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_interaction_backends.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_interactive_context.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_interactive_tool.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_jsonl_ui.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_kvstore.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_list_agents_tool.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_mcp_client.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_mcp_server.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_models.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_multi_agent.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_multistep_agents.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_orchestrator_heartbeat.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_plugins.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_reasoning_models.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_renderer.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_rendering_scenarios.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_repl_commands.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_repl_completer.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_repl_handler.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_retry_system.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_run_if.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_schedule_model_override.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_scheduler_history_injection.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_schema.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_security_phase1.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_send_message.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_skill_discovery.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_skill_tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_stdin.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_subagent_subprocess.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_tool_directives.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_tool_registry.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_workspace_auto_continue.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_workspace_cwd.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_workspace_discovery.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/helpers.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/youtube.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/codebase_exploration.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/python_math.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/response_patterns.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/scheduling.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/skill_authoring.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_agent_basics.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_jinja_reference.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_skill_basics.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cache.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/mcp.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/serve.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/config.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/console.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/constants.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/executor.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/subprocess_executor.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/compaction_scheduler.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/gateway.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/push.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/scheduler.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/responsive.css +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/theme.css +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-192.png +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512.png +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/api.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/app.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/utils.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/dashboard.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/file-editor.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/kvstore.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/schedules.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/webhooks.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/workspace.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/manifest.json +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/sw.js +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/webhook_store.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/storage.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/kvstore/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/kvstore/backend.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/kvstore/sqlite.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/mcp_client.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/mcp_config.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/mcp_server.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/md_agents.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/models.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/options.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/schemas/agent.schema.json +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/agents.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/fs.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/http.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/kv.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/schedule.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/skills.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/utils.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/context.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/session.py +0 -0
- {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/templates.py +0 -0
|
@@ -28,20 +28,20 @@ class TestApplyCacheControlToMessages:
|
|
|
28
28
|
assert result == []
|
|
29
29
|
|
|
30
30
|
def test_single_turn_caching(self):
|
|
31
|
-
"""Test caching a single turn -
|
|
31
|
+
"""Test caching a single turn - both messages cached (within default limit)."""
|
|
32
32
|
messages = [
|
|
33
33
|
{"role": "user", "content": "Hello"},
|
|
34
34
|
{"role": "assistant", "content": "Hi"},
|
|
35
35
|
]
|
|
36
36
|
result = apply_cache_control_to_messages(messages)
|
|
37
37
|
|
|
38
|
-
# Both messages should have cache_control
|
|
38
|
+
# Both messages should have cache_control (2 <= default max_markers of 2)
|
|
39
39
|
assert len(result) == 2
|
|
40
40
|
assert result[0]["cache_control"] == {"type": "ephemeral"}
|
|
41
41
|
assert result[1]["cache_control"] == {"type": "ephemeral"}
|
|
42
42
|
|
|
43
|
-
def
|
|
44
|
-
"""Test that
|
|
43
|
+
def test_multiple_turns_only_last_cached(self):
|
|
44
|
+
"""Test that only the last max_markers messages get cache_control."""
|
|
45
45
|
messages = [
|
|
46
46
|
{"role": "user", "content": "Turn 1 user"},
|
|
47
47
|
{"role": "assistant", "content": "Turn 1 assistant"},
|
|
@@ -52,13 +52,15 @@ class TestApplyCacheControlToMessages:
|
|
|
52
52
|
]
|
|
53
53
|
result = apply_cache_control_to_messages(messages)
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# Only last 2 messages should have cache_control (default max_markers=2)
|
|
56
56
|
assert len(result) == 6
|
|
57
|
-
for
|
|
58
|
-
assert
|
|
57
|
+
for msg in result[:4]:
|
|
58
|
+
assert "cache_control" not in msg
|
|
59
|
+
assert result[4]["cache_control"] == {"type": "ephemeral"}
|
|
60
|
+
assert result[5]["cache_control"] == {"type": "ephemeral"}
|
|
59
61
|
|
|
60
|
-
def
|
|
61
|
-
"""Test that
|
|
62
|
+
def test_many_turns_respects_limit(self):
|
|
63
|
+
"""Test that many turns only cache the last max_markers messages."""
|
|
62
64
|
messages = []
|
|
63
65
|
for i in range(10):
|
|
64
66
|
messages.append({"role": "user", "content": f"Turn {i}"})
|
|
@@ -66,11 +68,30 @@ class TestApplyCacheControlToMessages:
|
|
|
66
68
|
|
|
67
69
|
result = apply_cache_control_to_messages(messages)
|
|
68
70
|
|
|
69
|
-
#
|
|
71
|
+
# Only last 2 should be cached (default max_markers=2)
|
|
70
72
|
assert len(result) == 20
|
|
71
|
-
for msg in result
|
|
73
|
+
cached_count = sum(1 for msg in result if "cache_control" in msg)
|
|
74
|
+
assert cached_count == 2
|
|
75
|
+
for msg in result[:18]:
|
|
76
|
+
assert "cache_control" not in msg
|
|
77
|
+
for msg in result[18:]:
|
|
72
78
|
assert msg["cache_control"] == {"type": "ephemeral"}
|
|
73
79
|
|
|
80
|
+
def test_custom_max_markers(self):
|
|
81
|
+
"""Test with a custom max_markers value."""
|
|
82
|
+
messages = [
|
|
83
|
+
{"role": "user", "content": f"Turn {i}"}
|
|
84
|
+
for i in range(10)
|
|
85
|
+
]
|
|
86
|
+
result = apply_cache_control_to_messages(messages, max_markers=3)
|
|
87
|
+
|
|
88
|
+
cached_count = sum(1 for msg in result if "cache_control" in msg)
|
|
89
|
+
assert cached_count == 3
|
|
90
|
+
assert "cache_control" not in result[6]
|
|
91
|
+
assert result[7]["cache_control"] == {"type": "ephemeral"}
|
|
92
|
+
assert result[8]["cache_control"] == {"type": "ephemeral"}
|
|
93
|
+
assert result[9]["cache_control"] == {"type": "ephemeral"}
|
|
94
|
+
|
|
74
95
|
def test_preserves_existing_fields(self):
|
|
75
96
|
"""Test that existing message fields are preserved."""
|
|
76
97
|
messages = [
|
|
@@ -90,12 +111,25 @@ class TestApplyCacheControlToMessages:
|
|
|
90
111
|
assert result[1]["function_call"] == {"name": "test"}
|
|
91
112
|
assert result[1]["cache_control"] == {"type": "ephemeral"}
|
|
92
113
|
|
|
114
|
+
def test_anthropic_limit_with_context_turn(self):
|
|
115
|
+
"""Test that history markers + context turn (2) stay within Anthropic's 4 block limit."""
|
|
116
|
+
messages = []
|
|
117
|
+
for i in range(100):
|
|
118
|
+
messages.append({"role": "user", "content": f"Turn {i}"})
|
|
119
|
+
messages.append({"role": "assistant", "content": f"Response {i}"})
|
|
120
|
+
|
|
121
|
+
result = apply_cache_control_to_messages(messages)
|
|
122
|
+
|
|
123
|
+
cached_count = sum(1 for msg in result if "cache_control" in msg)
|
|
124
|
+
# Default 2 + 2 from context turn = 4 total (Anthropic limit)
|
|
125
|
+
assert cached_count == 2
|
|
126
|
+
|
|
93
127
|
|
|
94
128
|
class TestCacheControlIntegration:
|
|
95
129
|
"""Integration tests for cache control with conversation history."""
|
|
96
130
|
|
|
97
131
|
def test_load_and_cache_conversation(self, temp_history_dir):
|
|
98
|
-
"""Test loading conversation and applying cache control
|
|
132
|
+
"""Test loading conversation and applying cache control."""
|
|
99
133
|
from tsugite.history import reconstruct_messages
|
|
100
134
|
|
|
101
135
|
storage = SessionStorage.create(agent_name="test_agent", model="test:model")
|
|
@@ -114,18 +148,17 @@ class TestCacheControlIntegration:
|
|
|
114
148
|
|
|
115
149
|
# Load messages
|
|
116
150
|
messages = reconstruct_messages(storage.session_path)
|
|
117
|
-
assert len(messages) == 6 # 3 turns
|
|
151
|
+
assert len(messages) == 6 # 3 turns x 2 messages
|
|
118
152
|
|
|
119
|
-
# Apply cache control
|
|
153
|
+
# Apply cache control - only last 2 messages should be cached
|
|
120
154
|
cached = apply_cache_control_to_messages(messages)
|
|
121
155
|
|
|
122
|
-
# All messages should be cached
|
|
123
156
|
assert len(cached) == 6
|
|
124
|
-
for msg in cached
|
|
125
|
-
|
|
157
|
+
cached_count = sum(1 for msg in cached if "cache_control" in msg)
|
|
158
|
+
assert cached_count == 2
|
|
126
159
|
|
|
127
|
-
def
|
|
128
|
-
"""Test that
|
|
160
|
+
def test_long_conversation_respects_limit(self, temp_history_dir):
|
|
161
|
+
"""Test that long conversations respect the cache_control block limit."""
|
|
129
162
|
from tsugite.history import reconstruct_messages
|
|
130
163
|
|
|
131
164
|
storage = SessionStorage.create(agent_name="test_agent", model="test:model")
|
|
@@ -143,11 +176,9 @@ class TestCacheControlIntegration:
|
|
|
143
176
|
)
|
|
144
177
|
|
|
145
178
|
messages = reconstruct_messages(storage.session_path)
|
|
146
|
-
assert len(messages) == 20 # 10 turns
|
|
179
|
+
assert len(messages) == 20 # 10 turns x 2 messages
|
|
147
180
|
|
|
148
|
-
# Apply cache control - should cache all messages
|
|
149
181
|
cached = apply_cache_control_to_messages(messages)
|
|
150
182
|
|
|
151
|
-
# All 20 messages should be cached
|
|
152
183
|
cached_count = sum(1 for msg in cached if "cache_control" in msg)
|
|
153
|
-
assert cached_count ==
|
|
184
|
+
assert cached_count == 2
|
|
@@ -448,5 +448,6 @@ class TestToolCallHistory:
|
|
|
448
448
|
cached = apply_cache_control_to_messages(reconstructed)
|
|
449
449
|
|
|
450
450
|
assert len(cached) == 4
|
|
451
|
-
|
|
452
|
-
|
|
451
|
+
# Last 2 messages get cache_control (default max_markers=2, context turn uses the other 2)
|
|
452
|
+
cached_count = sum(1 for msg in cached if "cache_control" in msg)
|
|
453
|
+
assert cached_count == 2
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Tests for tmux session management tools."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from unittest.mock import MagicMock, call, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from tsugite.tools.tmux import (
|
|
10
|
+
_get_session_status,
|
|
11
|
+
_list_managed_sessions,
|
|
12
|
+
_strip_ansi,
|
|
13
|
+
_validate_name,
|
|
14
|
+
get_tmux_sessions,
|
|
15
|
+
tmux_create,
|
|
16
|
+
tmux_kill,
|
|
17
|
+
tmux_list,
|
|
18
|
+
tmux_read,
|
|
19
|
+
tmux_send,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_metadata(tmp_path, monkeypatch):
|
|
25
|
+
"""Redirect metadata and log paths to tmp_path."""
|
|
26
|
+
meta_dir = tmp_path / "tmux"
|
|
27
|
+
log_dir = tmp_path / "tmux-logs"
|
|
28
|
+
meta_dir.mkdir()
|
|
29
|
+
log_dir.mkdir()
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr("tsugite.tools.tmux._get_metadata_path", lambda: meta_dir / "sessions.json")
|
|
32
|
+
monkeypatch.setattr("tsugite.tools.tmux._get_log_dir", lambda: log_dir)
|
|
33
|
+
return tmp_path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _make_run_result(returncode=0, stdout="", stderr=""):
|
|
37
|
+
result = MagicMock(spec=subprocess.CompletedProcess)
|
|
38
|
+
result.returncode = returncode
|
|
39
|
+
result.stdout = stdout
|
|
40
|
+
result.stderr = stderr
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestStripAnsi:
|
|
45
|
+
def test_strips_sgr_sequences(self):
|
|
46
|
+
assert _strip_ansi("\x1b[31mred\x1b[0m") == "red"
|
|
47
|
+
|
|
48
|
+
def test_strips_bold_and_color(self):
|
|
49
|
+
assert _strip_ansi("\x1b[1;32mgreen bold\x1b[0m") == "green bold"
|
|
50
|
+
|
|
51
|
+
def test_strips_osc_sequences(self):
|
|
52
|
+
assert _strip_ansi("\x1b]0;title\x07text") == "text"
|
|
53
|
+
|
|
54
|
+
def test_strips_charset_designator(self):
|
|
55
|
+
assert _strip_ansi("\x1b(Btext") == "text"
|
|
56
|
+
|
|
57
|
+
def test_passthrough_clean_text(self):
|
|
58
|
+
assert _strip_ansi("hello world") == "hello world"
|
|
59
|
+
|
|
60
|
+
def test_mixed_ansi(self):
|
|
61
|
+
text = "\x1b[1m\x1b[32mOK\x1b[0m: \x1b[34mtest\x1b[0m passed"
|
|
62
|
+
assert _strip_ansi(text) == "OK: test passed"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestValidateName:
|
|
66
|
+
def test_valid_names(self):
|
|
67
|
+
for name in ["test", "my-session", "project_1", "A-b_C-3"]:
|
|
68
|
+
_validate_name(name)
|
|
69
|
+
|
|
70
|
+
def test_invalid_names(self):
|
|
71
|
+
for name in ["has space", "semi;colon", "pipe|char", "slash/path", ""]:
|
|
72
|
+
with pytest.raises(ValueError, match="Invalid session name"):
|
|
73
|
+
_validate_name(name)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestGetSessionStatus:
|
|
77
|
+
@patch("tsugite.tools.tmux._get_pane_command")
|
|
78
|
+
def test_idle_when_shell(self, mock_cmd):
|
|
79
|
+
for shell in ["bash", "zsh", "sh", "fish"]:
|
|
80
|
+
mock_cmd.return_value = shell
|
|
81
|
+
assert _get_session_status("tsu-test") == "idle"
|
|
82
|
+
|
|
83
|
+
@patch("tsugite.tools.tmux._get_pane_command")
|
|
84
|
+
def test_active_when_process(self, mock_cmd):
|
|
85
|
+
mock_cmd.return_value = "python3"
|
|
86
|
+
assert _get_session_status("tsu-test") == "active: python3"
|
|
87
|
+
|
|
88
|
+
@patch("tsugite.tools.tmux._get_pane_command")
|
|
89
|
+
def test_idle_when_empty(self, mock_cmd):
|
|
90
|
+
mock_cmd.return_value = ""
|
|
91
|
+
assert _get_session_status("tsu-test") == "idle"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestTmuxCreate:
|
|
95
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
96
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
97
|
+
def test_create_session(self, mock_run, mock_exists, mock_metadata):
|
|
98
|
+
mock_run.return_value = _make_run_result()
|
|
99
|
+
|
|
100
|
+
result = tmux_create("test")
|
|
101
|
+
|
|
102
|
+
assert result["name"] == "test"
|
|
103
|
+
assert result["tmux_session"] == "tsu-test"
|
|
104
|
+
assert result["status"] == "created"
|
|
105
|
+
assert "log_file" in result
|
|
106
|
+
|
|
107
|
+
calls = mock_run.call_args_list
|
|
108
|
+
assert calls[0] == call(
|
|
109
|
+
["tmux", "new-session", "-d", "-s", "tsu-test", "-x", "200", "-y", "50"],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
)
|
|
113
|
+
assert calls[1].args[0][:4] == ["tmux", "pipe-pane", "-t", "tsu-test"]
|
|
114
|
+
|
|
115
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
116
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
117
|
+
def test_create_with_command(self, mock_run, mock_exists, mock_metadata):
|
|
118
|
+
mock_run.return_value = _make_run_result()
|
|
119
|
+
|
|
120
|
+
tmux_create("test", command="htop")
|
|
121
|
+
|
|
122
|
+
new_session_call = mock_run.call_args_list[0]
|
|
123
|
+
assert "htop" in new_session_call.args[0]
|
|
124
|
+
|
|
125
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=True)
|
|
126
|
+
def test_create_already_exists(self, mock_exists):
|
|
127
|
+
with pytest.raises(RuntimeError, match="already exists"):
|
|
128
|
+
tmux_create("test")
|
|
129
|
+
|
|
130
|
+
def test_create_invalid_name(self):
|
|
131
|
+
with pytest.raises(ValueError, match="Invalid session name"):
|
|
132
|
+
tmux_create("bad name")
|
|
133
|
+
|
|
134
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
135
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
136
|
+
def test_create_saves_metadata(self, mock_run, mock_exists, mock_metadata):
|
|
137
|
+
mock_run.return_value = _make_run_result()
|
|
138
|
+
|
|
139
|
+
tmux_create("myproject", command="python3")
|
|
140
|
+
|
|
141
|
+
meta_path = mock_metadata / "tmux" / "sessions.json"
|
|
142
|
+
assert meta_path.exists()
|
|
143
|
+
data = json.loads(meta_path.read_text())
|
|
144
|
+
assert "myproject" in data
|
|
145
|
+
assert data["myproject"]["command"] == "python3"
|
|
146
|
+
assert data["myproject"]["prefixed_name"] == "tsu-myproject"
|
|
147
|
+
|
|
148
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
149
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
150
|
+
def test_create_cleans_up_on_pipe_failure(self, mock_run, mock_exists, mock_metadata):
|
|
151
|
+
mock_run.side_effect = [
|
|
152
|
+
_make_run_result(), # new-session succeeds
|
|
153
|
+
_make_run_result(returncode=1, stderr="pipe error"), # pipe-pane fails
|
|
154
|
+
_make_run_result(), # kill-session cleanup
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
with pytest.raises(RuntimeError, match="Failed to set up logging"):
|
|
158
|
+
tmux_create("test")
|
|
159
|
+
|
|
160
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
161
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
162
|
+
def test_create_pipe_pane_uses_shlex_quote(self, mock_run, mock_exists, mock_metadata):
|
|
163
|
+
mock_run.return_value = _make_run_result()
|
|
164
|
+
|
|
165
|
+
tmux_create("test")
|
|
166
|
+
|
|
167
|
+
pipe_call = mock_run.call_args_list[1]
|
|
168
|
+
pipe_arg = pipe_call.args[0][5] # The -o argument value
|
|
169
|
+
assert pipe_arg.startswith("cat >> ")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestTmuxRead:
|
|
173
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=True)
|
|
174
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
175
|
+
def test_read_pane(self, mock_run, mock_exists):
|
|
176
|
+
mock_run.return_value = _make_run_result(stdout="\x1b[32mhello\x1b[0m world\n")
|
|
177
|
+
|
|
178
|
+
result = tmux_read("test", lines=10)
|
|
179
|
+
|
|
180
|
+
assert result == "hello world\n"
|
|
181
|
+
mock_run.assert_called_once_with(
|
|
182
|
+
["tmux", "capture-pane", "-t", "tsu-test", "-p", "-S", "-10"],
|
|
183
|
+
capture_output=True,
|
|
184
|
+
text=True,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def test_read_log(self, mock_metadata):
|
|
188
|
+
log_dir = mock_metadata / "tmux-logs"
|
|
189
|
+
log_file = log_dir / "test.log"
|
|
190
|
+
log_file.write_text("line1\nline2\nline3\n\x1b[31mline4\x1b[0m\n")
|
|
191
|
+
|
|
192
|
+
result = tmux_read("test", lines=2, source="log")
|
|
193
|
+
|
|
194
|
+
assert result == "line3\nline4\n"
|
|
195
|
+
|
|
196
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
197
|
+
def test_read_nonexistent_pane(self, mock_exists):
|
|
198
|
+
with pytest.raises(RuntimeError, match="not found"):
|
|
199
|
+
tmux_read("nonexistent")
|
|
200
|
+
|
|
201
|
+
def test_read_nonexistent_log(self, mock_metadata):
|
|
202
|
+
with pytest.raises(RuntimeError, match="No log file"):
|
|
203
|
+
tmux_read("nonexistent", source="log")
|
|
204
|
+
|
|
205
|
+
def test_read_invalid_source(self):
|
|
206
|
+
with pytest.raises(ValueError, match="Invalid source"):
|
|
207
|
+
tmux_read("test", source="invalid")
|
|
208
|
+
|
|
209
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=True)
|
|
210
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
211
|
+
def test_read_clamps_lines(self, mock_run, mock_exists):
|
|
212
|
+
mock_run.return_value = _make_run_result(stdout="text\n")
|
|
213
|
+
|
|
214
|
+
tmux_read("test", lines=99999)
|
|
215
|
+
|
|
216
|
+
args = mock_run.call_args.args[0]
|
|
217
|
+
assert "-5000" in args
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestTmuxSend:
|
|
221
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=True)
|
|
222
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
223
|
+
def test_send_with_enter(self, mock_run, mock_exists):
|
|
224
|
+
mock_run.return_value = _make_run_result()
|
|
225
|
+
|
|
226
|
+
result = tmux_send("test", "ls -la")
|
|
227
|
+
|
|
228
|
+
mock_run.assert_called_once_with(
|
|
229
|
+
["tmux", "send-keys", "-t", "tsu-test", "ls -la", "Enter"],
|
|
230
|
+
capture_output=True,
|
|
231
|
+
text=True,
|
|
232
|
+
)
|
|
233
|
+
assert "command" in result
|
|
234
|
+
|
|
235
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=True)
|
|
236
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
237
|
+
def test_send_without_enter(self, mock_run, mock_exists):
|
|
238
|
+
mock_run.return_value = _make_run_result()
|
|
239
|
+
|
|
240
|
+
result = tmux_send("test", "q", enter=False)
|
|
241
|
+
|
|
242
|
+
mock_run.assert_called_once_with(
|
|
243
|
+
["tmux", "send-keys", "-t", "tsu-test", "q"],
|
|
244
|
+
capture_output=True,
|
|
245
|
+
text=True,
|
|
246
|
+
)
|
|
247
|
+
assert "keys" in result
|
|
248
|
+
|
|
249
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
250
|
+
def test_send_nonexistent(self, mock_exists):
|
|
251
|
+
with pytest.raises(RuntimeError, match="not found"):
|
|
252
|
+
tmux_send("nonexistent", "hello")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestListManagedSessions:
|
|
256
|
+
"""Tests for the shared _list_managed_sessions helper used by tmux_list and get_tmux_sessions."""
|
|
257
|
+
|
|
258
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
259
|
+
def test_filters_by_prefix(self, mock_run, mock_metadata):
|
|
260
|
+
mock_run.return_value = _make_run_result(
|
|
261
|
+
stdout="tsu-project1\tbash\nuser-session\tvim\ntsu-project2\tpython3\n"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
meta_path = mock_metadata / "tmux" / "sessions.json"
|
|
265
|
+
meta_path.write_text(
|
|
266
|
+
json.dumps(
|
|
267
|
+
{
|
|
268
|
+
"project1": {"command": "htop", "created_at": "2026-01-01T00:00:00", "log_file": "/tmp/p1.log"},
|
|
269
|
+
"project2": {"command": None, "created_at": "2026-01-02T00:00:00", "log_file": "/tmp/p2.log"},
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
result = _list_managed_sessions()
|
|
275
|
+
|
|
276
|
+
assert len(result) == 2
|
|
277
|
+
names = [s["name"] for s in result]
|
|
278
|
+
assert "project1" in names
|
|
279
|
+
assert "project2" in names
|
|
280
|
+
|
|
281
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
282
|
+
def test_idle_status_for_shell(self, mock_run, mock_metadata):
|
|
283
|
+
mock_run.return_value = _make_run_result(stdout="tsu-test\tbash\n")
|
|
284
|
+
|
|
285
|
+
result = _list_managed_sessions()
|
|
286
|
+
|
|
287
|
+
assert result[0]["status"] == "idle"
|
|
288
|
+
|
|
289
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
290
|
+
def test_active_status_for_process(self, mock_run, mock_metadata):
|
|
291
|
+
mock_run.return_value = _make_run_result(stdout="tsu-test\tpython3\n")
|
|
292
|
+
|
|
293
|
+
result = _list_managed_sessions()
|
|
294
|
+
|
|
295
|
+
assert result[0]["status"] == "active: python3"
|
|
296
|
+
|
|
297
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
298
|
+
def test_no_server(self, mock_run):
|
|
299
|
+
mock_run.return_value = _make_run_result(returncode=1, stderr="no server running")
|
|
300
|
+
|
|
301
|
+
assert _list_managed_sessions() == []
|
|
302
|
+
|
|
303
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
304
|
+
def test_no_managed_sessions(self, mock_run, mock_metadata):
|
|
305
|
+
mock_run.return_value = _make_run_result(stdout="user-session\tbash\n")
|
|
306
|
+
|
|
307
|
+
assert _list_managed_sessions() == []
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TestTmuxList:
|
|
311
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
312
|
+
def test_list_delegates_to_shared_helper(self, mock_run, mock_metadata):
|
|
313
|
+
mock_run.return_value = _make_run_result(
|
|
314
|
+
stdout="tsu-project1\tbash\ntsu-project2\thtop\n"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
result = tmux_list()
|
|
318
|
+
|
|
319
|
+
assert len(result) == 2
|
|
320
|
+
assert result[0]["status"] == "idle"
|
|
321
|
+
assert result[1]["status"] == "active: htop"
|
|
322
|
+
|
|
323
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
324
|
+
def test_list_no_server(self, mock_run):
|
|
325
|
+
mock_run.return_value = _make_run_result(returncode=1)
|
|
326
|
+
|
|
327
|
+
assert tmux_list() == []
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class TestTmuxKill:
|
|
331
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=True)
|
|
332
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
333
|
+
def test_kill_session(self, mock_run, mock_exists, mock_metadata):
|
|
334
|
+
mock_run.return_value = _make_run_result()
|
|
335
|
+
|
|
336
|
+
meta_path = mock_metadata / "tmux" / "sessions.json"
|
|
337
|
+
meta_path.write_text(json.dumps({"test": {"command": "htop"}}))
|
|
338
|
+
|
|
339
|
+
result = tmux_kill("test")
|
|
340
|
+
|
|
341
|
+
mock_run.assert_called_once_with(
|
|
342
|
+
["tmux", "kill-session", "-t", "tsu-test"],
|
|
343
|
+
capture_output=True,
|
|
344
|
+
text=True,
|
|
345
|
+
)
|
|
346
|
+
assert "terminated" in result
|
|
347
|
+
|
|
348
|
+
data = json.loads(meta_path.read_text())
|
|
349
|
+
assert "test" not in data
|
|
350
|
+
|
|
351
|
+
@patch("tsugite.tools.tmux._session_exists", return_value=False)
|
|
352
|
+
def test_kill_nonexistent(self, mock_exists):
|
|
353
|
+
with pytest.raises(RuntimeError, match="not found"):
|
|
354
|
+
tmux_kill("nonexistent")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestGetTmuxSessions:
|
|
358
|
+
@patch("tsugite.tools.tmux.shutil.which", return_value=None)
|
|
359
|
+
def test_no_tmux_installed(self, mock_which):
|
|
360
|
+
assert get_tmux_sessions() == []
|
|
361
|
+
|
|
362
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
363
|
+
@patch("tsugite.tools.tmux.shutil.which", return_value="/usr/bin/tmux")
|
|
364
|
+
def test_filters_prefix(self, mock_which, mock_run, mock_metadata):
|
|
365
|
+
mock_run.return_value = _make_run_result(stdout="tsu-myproject\tbash\nother-session\tvim\n")
|
|
366
|
+
|
|
367
|
+
result = get_tmux_sessions()
|
|
368
|
+
|
|
369
|
+
assert len(result) == 1
|
|
370
|
+
assert result[0]["name"] == "myproject"
|
|
371
|
+
assert "created_at" not in result[0]
|
|
372
|
+
assert "log_file" not in result[0]
|
|
373
|
+
|
|
374
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
375
|
+
@patch("tsugite.tools.tmux.shutil.which", return_value="/usr/bin/tmux")
|
|
376
|
+
def test_idle_status(self, mock_which, mock_run, mock_metadata):
|
|
377
|
+
mock_run.return_value = _make_run_result(stdout="tsu-test\tzsh\n")
|
|
378
|
+
|
|
379
|
+
result = get_tmux_sessions()
|
|
380
|
+
|
|
381
|
+
assert result[0]["status"] == "idle"
|
|
382
|
+
|
|
383
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
384
|
+
@patch("tsugite.tools.tmux.shutil.which", return_value="/usr/bin/tmux")
|
|
385
|
+
def test_active_status(self, mock_which, mock_run, mock_metadata):
|
|
386
|
+
mock_run.return_value = _make_run_result(stdout="tsu-test\thtop\n")
|
|
387
|
+
|
|
388
|
+
result = get_tmux_sessions()
|
|
389
|
+
|
|
390
|
+
assert result[0]["status"] == "active: htop"
|
|
391
|
+
|
|
392
|
+
@patch("tsugite.tools.tmux.subprocess.run")
|
|
393
|
+
@patch("tsugite.tools.tmux.shutil.which", return_value="/usr/bin/tmux")
|
|
394
|
+
def test_no_server_running(self, mock_which, mock_run):
|
|
395
|
+
mock_run.return_value = _make_run_result(returncode=1)
|
|
396
|
+
|
|
397
|
+
assert get_tmux_sessions() == []
|
|
@@ -6,6 +6,7 @@ the session storage system.
|
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Dict, List, Optional
|
|
11
12
|
|
|
@@ -76,6 +77,7 @@ def save_run_to_history(
|
|
|
76
77
|
channel_metadata: Optional[dict] = None,
|
|
77
78
|
duration_ms: Optional[int] = None,
|
|
78
79
|
claude_code_session_id: Optional[str] = None,
|
|
80
|
+
claude_code_compacted: bool = False,
|
|
79
81
|
) -> Optional[str]:
|
|
80
82
|
"""Save a single agent run to history.
|
|
81
83
|
|
|
@@ -143,10 +145,12 @@ def save_run_to_history(
|
|
|
143
145
|
|
|
144
146
|
functions_called = _extract_functions_called(execution_steps) if execution_steps else []
|
|
145
147
|
|
|
146
|
-
# Merge
|
|
148
|
+
# Merge claude_code session info into metadata if present
|
|
147
149
|
metadata = dict(channel_metadata) if channel_metadata else {}
|
|
148
150
|
if claude_code_session_id:
|
|
149
151
|
metadata["claude_code_session_id"] = claude_code_session_id
|
|
152
|
+
if claude_code_compacted:
|
|
153
|
+
metadata["claude_code_compacted"] = True
|
|
150
154
|
|
|
151
155
|
# Write accumulated hook execution records before the turn
|
|
152
156
|
from tsugite.hooks import drain_all_executions
|
|
@@ -212,6 +216,57 @@ def _extract_functions_called(execution_steps: list) -> List[str]:
|
|
|
212
216
|
return sorted(list(functions))
|
|
213
217
|
|
|
214
218
|
|
|
219
|
+
@dataclass
|
|
220
|
+
class ClaudeCodeSessionInfo:
|
|
221
|
+
"""Info about the Claude Code session for a conversation."""
|
|
222
|
+
|
|
223
|
+
session_id: str
|
|
224
|
+
compacted: bool = False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_claude_code_session_info(conversation_id: str) -> Optional["ClaudeCodeSessionInfo"]:
|
|
228
|
+
"""Get the Claude Code session ID and compaction state from conversation history.
|
|
229
|
+
|
|
230
|
+
Returns None if the session was compacted by Tsugite (Claude Code session would
|
|
231
|
+
be stale) or if no session ID is found (e.g. non-claude_code model was used).
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
from tsugite.history import SessionStorage, Turn
|
|
235
|
+
from tsugite.history.models import CompactionSummary
|
|
236
|
+
|
|
237
|
+
session_path = get_history_dir() / f"{conversation_id}.jsonl"
|
|
238
|
+
if not session_path.exists():
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
storage = SessionStorage(session_path)
|
|
242
|
+
records = storage.load_records()
|
|
243
|
+
|
|
244
|
+
# Find the last CompactionSummary index (if any).
|
|
245
|
+
# Session IDs from retained turns (carried over from pre-compaction)
|
|
246
|
+
# are stale, but session IDs from turns AFTER the compaction are valid.
|
|
247
|
+
compaction_idx = -1
|
|
248
|
+
for i, record in enumerate(records):
|
|
249
|
+
if isinstance(record, CompactionSummary):
|
|
250
|
+
compaction_idx = i
|
|
251
|
+
|
|
252
|
+
for record in reversed(records):
|
|
253
|
+
if isinstance(record, Turn) and record.metadata:
|
|
254
|
+
session_id = record.metadata.get("claude_code_session_id")
|
|
255
|
+
if not session_id:
|
|
256
|
+
continue
|
|
257
|
+
# If this turn is a retained turn from before compaction
|
|
258
|
+
# (carried over with stale session ID), skip it
|
|
259
|
+
if compaction_idx >= 0:
|
|
260
|
+
turn_idx = records.index(record)
|
|
261
|
+
if turn_idx <= compaction_idx:
|
|
262
|
+
continue
|
|
263
|
+
compacted = record.metadata.get("claude_code_compacted", False)
|
|
264
|
+
return ClaudeCodeSessionInfo(session_id, compacted)
|
|
265
|
+
return None
|
|
266
|
+
except Exception:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
215
270
|
def get_latest_conversation() -> Optional[str]:
|
|
216
271
|
"""Get the most recent conversation/session ID.
|
|
217
272
|
|
|
@@ -40,6 +40,7 @@ class AgentExecutionResult(BaseModel):
|
|
|
40
40
|
default_factory=list
|
|
41
41
|
) # List of Attachment objects (using Any for Pydantic compatibility)
|
|
42
42
|
claude_code_session_id: Optional[str] = None
|
|
43
|
+
claude_code_compacted: bool = False
|
|
43
44
|
context_window: Optional[int] = None
|
|
44
45
|
|
|
45
46
|
def __str__(self) -> str:
|