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.
Files changed (281) hide show
  1. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/PKG-INFO +1 -1
  2. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/pyproject.toml +1 -1
  3. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cache_control.py +54 -23
  4. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_continuation.py +3 -2
  5. tsugite_cli-0.9.1/tests/test_tmux_tools.py +397 -0
  6. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/history_integration.py +56 -1
  7. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/models.py +1 -0
  8. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/runner.py +64 -14
  9. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/default.md +18 -0
  10. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/agent.py +104 -25
  11. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/claude_code.py +29 -6
  12. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/base.py +28 -3
  13. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/discord.py +61 -1
  14. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/http.py +71 -0
  15. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/scheduler_adapter.py +1 -0
  16. tsugite_cli-0.9.1/tsugite/daemon/commands.py +168 -0
  17. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/config.py +1 -0
  18. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/memory.py +50 -0
  19. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/session_runner.py +17 -3
  20. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/session_store.py +4 -1
  21. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/styles.css +28 -5
  22. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/index.html +105 -33
  23. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/conversations.js +227 -38
  24. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/reconstruction.py +14 -6
  25. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/hooks.py +11 -2
  26. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/renderer.py +11 -0
  27. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/__init__.py +5 -1
  28. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/sessions.py +16 -0
  29. tsugite_cli-0.9.1/tsugite/tools/tmux.py +311 -0
  30. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/uv.lock +13 -13
  31. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/copilot-instructions.md +0 -0
  32. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/workflows/ci.yml +0 -0
  33. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/workflows/docker-publish.yml +0 -0
  34. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.github/workflows/pypi-publish.yml +0 -0
  35. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/.gitignore +0 -0
  36. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/AGENTS.md +0 -0
  37. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/CLAUDE.md +0 -0
  38. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/LICENSE +0 -0
  39. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/README.md +0 -0
  40. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/scripts/regenerate_schema.py +0 -0
  41. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/README.md +0 -0
  42. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/__init__.py +0 -0
  43. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/conftest.py +0 -0
  44. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/__init__.py +0 -0
  45. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_agent.py +0 -0
  46. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_agent_ui_events.py +0 -0
  47. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_content_blocks.py +0 -0
  48. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_executor.py +0 -0
  49. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_memory.py +0 -0
  50. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_proxy.py +0 -0
  51. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_sandbox.py +0 -0
  52. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_subprocess_executor.py +0 -0
  53. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/core/test_tools.py +0 -0
  54. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/daemon/__init__.py +0 -0
  55. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/daemon/test_http_adapter.py +0 -0
  56. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/events/test_event_consolidation.py +0 -0
  57. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/smoke_test.sh +0 -0
  58. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_file_hot_loading.py +0 -0
  59. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_inheritance.py +0 -0
  60. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_parser.py +0 -0
  61. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_sessions.py +0 -0
  62. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_skills.py +0 -0
  63. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agent_utils.py +0 -0
  64. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_agents_tool.py +0 -0
  65. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_attachment_deduplication.py +0 -0
  66. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_attachments.py +0 -0
  67. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_auto_context_handler.py +0 -0
  68. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_auto_discovery.py +0 -0
  69. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_background_task_status.py +0 -0
  70. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_background_tasks.py +0 -0
  71. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_builtin_agent_paths.py +0 -0
  72. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_builtin_agents.py +0 -0
  73. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_chat_cli.py +0 -0
  74. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_chat_error_handling.py +0 -0
  75. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_claude_code_attachments.py +0 -0
  76. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_claude_code_provider.py +0 -0
  77. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli.py +0 -0
  78. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli_arguments.py +0 -0
  79. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli_rendering.py +0 -0
  80. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_cli_subcommands.py +0 -0
  81. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_config.py +0 -0
  82. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_custom_shell_tools.py +0 -0
  83. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_custom_ui.py +0 -0
  84. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_compaction_scheduler.py +0 -0
  85. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_config.py +0 -0
  86. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_history_persistence.py +0 -0
  87. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_memory.py +0 -0
  88. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_push.py +0 -0
  89. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_scheduler.py +0 -0
  90. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_session_isolation.py +0 -0
  91. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_daemon_unified_sessions.py +0 -0
  92. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_discord_progress.py +0 -0
  93. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_error_display.py +0 -0
  94. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_file_references.py +0 -0
  95. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_file_tools.py +0 -0
  96. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history.py +0 -0
  97. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_integration.py +0 -0
  98. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_models.py +0 -0
  99. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_performance.py +0 -0
  100. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_history_tools.py +0 -0
  101. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_hooks.py +0 -0
  102. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_http_tools.py +0 -0
  103. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_interaction_backends.py +0 -0
  104. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_interactive_context.py +0 -0
  105. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_interactive_tool.py +0 -0
  106. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_jsonl_ui.py +0 -0
  107. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_kvstore.py +0 -0
  108. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_list_agents_tool.py +0 -0
  109. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_mcp_client.py +0 -0
  110. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_mcp_server.py +0 -0
  111. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_models.py +0 -0
  112. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_multi_agent.py +0 -0
  113. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_multistep_agents.py +0 -0
  114. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_orchestrator_heartbeat.py +0 -0
  115. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_plugins.py +0 -0
  116. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_reasoning_models.py +0 -0
  117. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_renderer.py +0 -0
  118. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_rendering_scenarios.py +0 -0
  119. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_repl_commands.py +0 -0
  120. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_repl_completer.py +0 -0
  121. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_repl_handler.py +0 -0
  122. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_retry_system.py +0 -0
  123. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_run_if.py +0 -0
  124. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_schedule_model_override.py +0 -0
  125. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_scheduler_history_injection.py +0 -0
  126. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_schema.py +0 -0
  127. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_security_phase1.py +0 -0
  128. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_send_message.py +0 -0
  129. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_skill_discovery.py +0 -0
  130. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_skill_tools.py +0 -0
  131. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_stdin.py +0 -0
  132. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_subagent_subprocess.py +0 -0
  133. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_tool_directives.py +0 -0
  134. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_tool_registry.py +0 -0
  135. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_workspace_auto_continue.py +0 -0
  136. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_workspace_cwd.py +0 -0
  137. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tests/test_workspace_discovery.py +0 -0
  138. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/__init__.py +0 -0
  139. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_inheritance.py +0 -0
  140. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_preparation.py +0 -0
  141. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/__init__.py +0 -0
  142. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/helpers.py +0 -0
  143. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/metrics.py +0 -0
  144. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_runner/validation.py +0 -0
  145. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/agent_utils.py +0 -0
  146. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/__init__.py +0 -0
  147. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/auto_context.py +0 -0
  148. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/base.py +0 -0
  149. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/file.py +0 -0
  150. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/inline.py +0 -0
  151. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/storage.py +0 -0
  152. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/url.py +0 -0
  153. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/attachments/youtube.py +0 -0
  154. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/.gitkeep +0 -0
  155. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/code_searcher.md +0 -0
  156. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/file_searcher.md +0 -0
  157. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_agents/onboard.md +0 -0
  158. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/.gitkeep +0 -0
  159. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/codebase_exploration.md +0 -0
  160. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/python_math.md +0 -0
  161. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/response_patterns.md +0 -0
  162. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/scheduling.md +0 -0
  163. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/skill_authoring.md +0 -0
  164. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_agent_basics.md +0 -0
  165. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_jinja_reference.md +0 -0
  166. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/builtin_skills/tsugite_skill_basics.md +0 -0
  167. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cache.py +0 -0
  168. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/__init__.py +0 -0
  169. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/agents.py +0 -0
  170. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/attachments.py +0 -0
  171. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/cache.py +0 -0
  172. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/chat.py +0 -0
  173. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/config.py +0 -0
  174. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/daemon.py +0 -0
  175. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/helpers.py +0 -0
  176. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/history.py +0 -0
  177. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/init.py +0 -0
  178. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/mcp.py +0 -0
  179. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/plugins.py +0 -0
  180. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/render.py +0 -0
  181. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/run.py +0 -0
  182. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/serve.py +0 -0
  183. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/tools.py +0 -0
  184. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/validate.py +0 -0
  185. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/cli/workspace.py +0 -0
  186. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/config.py +0 -0
  187. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/console.py +0 -0
  188. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/constants.py +0 -0
  189. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/__init__.py +0 -0
  190. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/content_blocks.py +0 -0
  191. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/executor.py +0 -0
  192. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/memory.py +0 -0
  193. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/proxy.py +0 -0
  194. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/sandbox.py +0 -0
  195. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/subprocess_executor.py +0 -0
  196. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/core/tools.py +0 -0
  197. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/__init__.py +0 -0
  198. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/adapters/__init__.py +0 -0
  199. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/compaction_scheduler.py +0 -0
  200. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/gateway.py +0 -0
  201. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/push.py +0 -0
  202. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/scheduler.py +0 -0
  203. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/responsive.css +0 -0
  204. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/css/theme.css +0 -0
  205. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-192.png +0 -0
  206. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
  207. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/icon-512.png +0 -0
  208. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
  209. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
  210. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/api.js +0 -0
  211. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/app.js +0 -0
  212. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/utils.js +0 -0
  213. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/dashboard.js +0 -0
  214. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/file-editor.js +0 -0
  215. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/kvstore.js +0 -0
  216. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/schedules.js +0 -0
  217. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/webhooks.js +0 -0
  218. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/js/views/workspace.js +0 -0
  219. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/manifest.json +0 -0
  220. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/web/sw.js +0 -0
  221. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/daemon/webhook_store.py +0 -0
  222. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/__init__.py +0 -0
  223. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/base.py +0 -0
  224. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/bus.py +0 -0
  225. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/events.py +0 -0
  226. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/events/helpers.py +0 -0
  227. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/exceptions.py +0 -0
  228. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/__init__.py +0 -0
  229. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/models.py +0 -0
  230. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/history/storage.py +0 -0
  231. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/interaction.py +0 -0
  232. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/kvstore/__init__.py +0 -0
  233. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/kvstore/backend.py +0 -0
  234. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/kvstore/sqlite.py +0 -0
  235. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/mcp_client.py +0 -0
  236. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/mcp_config.py +0 -0
  237. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/mcp_server.py +0 -0
  238. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/md_agents.py +0 -0
  239. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/models.py +0 -0
  240. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/options.py +0 -0
  241. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/plugins.py +0 -0
  242. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/schemas/__init__.py +0 -0
  243. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/schemas/agent.schema.json +0 -0
  244. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/shell_tool_config.py +0 -0
  245. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/skill_discovery.py +0 -0
  246. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/AGENTS.md +0 -0
  247. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/IDENTITY.md +0 -0
  248. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/MEMORY.md +0 -0
  249. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/USER.md +0 -0
  250. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/personas/casual-technical.md +0 -0
  251. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/personas/marvin.md +0 -0
  252. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/templates/personas/minimal.md +0 -0
  253. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/agents.py +0 -0
  254. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/fs.py +0 -0
  255. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/history.py +0 -0
  256. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/http.py +0 -0
  257. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/interactive.py +0 -0
  258. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/kv.py +0 -0
  259. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/notify.py +0 -0
  260. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/schedule.py +0 -0
  261. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/shell.py +0 -0
  262. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/shell_tools.py +0 -0
  263. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tools/skills.py +0 -0
  264. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/tsugite.py +0 -0
  265. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/__init__.py +0 -0
  266. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/base.py +0 -0
  267. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/chat.py +0 -0
  268. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/helpers.py +0 -0
  269. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/jsonl.py +0 -0
  270. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/plain.py +0 -0
  271. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_chat.py +0 -0
  272. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_commands.py +0 -0
  273. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_completer.py +0 -0
  274. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui/repl_handler.py +0 -0
  275. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/ui_context.py +0 -0
  276. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/utils.py +0 -0
  277. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/__init__.py +0 -0
  278. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/context.py +0 -0
  279. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/models.py +0 -0
  280. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/session.py +0 -0
  281. {tsugite_cli-0.8.0 → tsugite_cli-0.9.1}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.8.0
3
+ Version: 0.9.1
4
4
  Summary: Micro-agent runner for task automation using markdown definitions
5
5
  Author: Justyn Shull
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.8.0"
3
+ version = "0.9.1"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -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 - all messages cached."""
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 test_multiple_turns_all_cached(self):
44
- """Test that all turns are cached (industry best practice)."""
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
- # All messages should have cache_control
55
+ # Only last 2 messages should have cache_control (default max_markers=2)
56
56
  assert len(result) == 6
57
- for i, msg in enumerate(result):
58
- assert msg["cache_control"] == {"type": "ephemeral"}, f"Message {i} not cached"
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 test_many_turns_all_cached(self):
61
- """Test that even many turns are all cached."""
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
- # All 20 messages should be cached
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 to all messages."""
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 × 2 messages
151
+ assert len(messages) == 6 # 3 turns x 2 messages
118
152
 
119
- # Apply cache control to all messages (industry best practice)
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
- assert msg["cache_control"] == {"type": "ephemeral"}
157
+ cached_count = sum(1 for msg in cached if "cache_control" in msg)
158
+ assert cached_count == 2
126
159
 
127
- def test_long_conversation_all_cached(self, temp_history_dir):
128
- """Test that even long conversations have all messages cached."""
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 × 2 messages
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 == 20
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
- for msg in cached:
452
- assert msg["cache_control"] == {"type": "ephemeral"}
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 claude_code_session_id into metadata if present
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: