synapse-cli-agent 0.1.16__tar.gz → 0.1.18__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 (343) hide show
  1. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.gitignore +2 -1
  2. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/AGENTS.md +52 -22
  3. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/CHANGELOG.md +37 -0
  4. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/PKG-INFO +2 -2
  5. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/README.md +1 -1
  6. synapse_cli_agent-0.1.18/decode_screenshot.py +26 -0
  7. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/pyproject.toml +1 -1
  8. synapse_cli_agent-0.1.18/release_body.md +19 -0
  9. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/Cargo.lock +1 -1
  10. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/Cargo.toml +1 -1
  11. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/pyproject.toml +1 -1
  12. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/src/lib.rs +6 -3
  13. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/app/agent.py +28 -1
  14. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/mcp.py +8 -9
  15. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/content/prompts.py +3 -3
  16. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/mcp_client.py +59 -7
  17. synapse_cli_agent-0.1.18/src/synapse/observability/debug_inspector.html +19 -0
  18. synapse_cli_agent-0.1.18/src/synapse/observability/debug_server.py +430 -0
  19. synapse_cli_agent-0.1.18/src/synapse/observability/llm_debug.py +295 -0
  20. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/backends.py +13 -2
  21. synapse_cli_agent-0.1.18/src/synapse/runtime/debug_capture_middleware.py +80 -0
  22. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/harness.py +14 -13
  23. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/middleware.py +11 -0
  24. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/tool_ignore.py +13 -6
  25. synapse_cli_agent-0.1.18/src/synapse/tools/__init__.py +13 -0
  26. synapse_cli_agent-0.1.18/src/synapse/tools/describe_image.py +92 -0
  27. synapse_cli_agent-0.1.18/src/synapse/tools/filesystem_search.py +162 -0
  28. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/transcript_blocks.py +0 -28
  29. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/tui.py +52 -7
  30. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/turn_rail_widgets.py +7 -1
  31. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_agent_factory.py +36 -2
  32. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_capabilities.py +7 -0
  33. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_compact_tool_descriptions.py +25 -8
  34. synapse_cli_agent-0.1.18/tests/test_debug_inspector.py +115 -0
  35. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_describe_image.py +45 -0
  36. synapse_cli_agent-0.1.18/tests/test_filesystem_search_tools.py +122 -0
  37. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_slash_and_mcp.py +54 -0
  38. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tool_ignore.py +7 -2
  39. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_turn_rail_minimap.py +18 -0
  40. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/uv.lock +6 -6
  41. synapse_cli_agent-0.1.16/release_body.md +0 -14
  42. synapse_cli_agent-0.1.16/src/synapse/tools/__init__.py +0 -5
  43. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.github/workflows/ci.yml +0 -0
  44. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.github/workflows/docs.yml +0 -0
  45. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.github/workflows/native-compression-wheels.yml +0 -0
  46. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.github/workflows/native-search-wheels.yml +0 -0
  47. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.github/workflows/release.yml +0 -0
  48. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.python-version +0 -0
  49. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/.readthedocs.yaml +0 -0
  50. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/agent_upstream.txt +0 -0
  51. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/cli_upstream.txt +0 -0
  52. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/agent-development-guide.md +0 -0
  53. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/architecture-flow.md +0 -0
  54. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/architecture.md +0 -0
  55. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/cli.md +0 -0
  56. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/codex-session-discovery-phase-1.md +0 -0
  57. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/codex-session-import-phase-2.md +0 -0
  58. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/config.md +0 -0
  59. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/design.md +0 -0
  60. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/index.md +0 -0
  61. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/install.md +0 -0
  62. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/mcp.md +0 -0
  63. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/models.md +0 -0
  64. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/native-compression-core.md +0 -0
  65. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/native-search-core.md +0 -0
  66. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/permissions.md +0 -0
  67. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/quickstart.md +0 -0
  68. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/sessions.md +0 -0
  69. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/skills.md +0 -0
  70. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/smith_output.md +0 -0
  71. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/tool-output-transform.md +0 -0
  72. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/tui-cursor-refactor.md +0 -0
  73. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/tutorial.md +0 -0
  74. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/docs/x-agent-development-guide.md +0 -0
  75. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/examples/mcp.example.json +0 -0
  76. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/examples/models.example.json +0 -0
  77. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/examples/settings.example.json +0 -0
  78. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/middleware_upstream.txt +0 -0
  79. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/mkdocs.yml +0 -0
  80. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/LICENSE +0 -0
  81. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/NOTICE +0 -0
  82. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/README.md +0 -0
  83. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-search-core/python/synapse_search_core/__init__.py +0 -0
  84. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/Cargo.lock +0 -0
  85. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/Cargo.toml +0 -0
  86. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/LICENSE +0 -0
  87. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/NOTICE +0 -0
  88. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/README.md +0 -0
  89. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/pyproject.toml +0 -0
  90. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/python/synapse_tool_compress_core/__init__.py +0 -0
  91. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/ccr.rs +0 -0
  92. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/ccr_in_memory.rs +0 -0
  93. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/mod.rs +0 -0
  94. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/relevance/base.rs +0 -0
  95. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/relevance/bm25.rs +0 -0
  96. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/signals/keyword_detector.rs +0 -0
  97. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/signals/line_importance.rs +0 -0
  98. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/adaptive_sizer.rs +0 -0
  99. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/anchor_selector.rs +0 -0
  100. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/code_compressor.rs +0 -0
  101. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/diff_compressor.rs +0 -0
  102. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/log_compressor.rs +0 -0
  103. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/search_compressor.rs +0 -0
  104. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/analyzer.rs +0 -0
  105. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/anchors.rs +0 -0
  106. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/builder.rs +0 -0
  107. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/classifier.rs +0 -0
  108. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/compaction/classifier.rs +0 -0
  109. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/compaction/compactor.rs +0 -0
  110. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/compaction/formatter.rs +0 -0
  111. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/compaction/ir.rs +0 -0
  112. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/compaction/mod.rs +0 -0
  113. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/compaction/walker.rs +0 -0
  114. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/config.rs +0 -0
  115. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/constraints.rs +0 -0
  116. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/crusher.rs +0 -0
  117. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/crushers.rs +0 -0
  118. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/error_keywords.rs +0 -0
  119. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/field_detect.rs +0 -0
  120. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/hashing.rs +0 -0
  121. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/mod.rs +0 -0
  122. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/observer.rs +0 -0
  123. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/orchestration.rs +0 -0
  124. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/outliers.rs +0 -0
  125. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/planning.rs +0 -0
  126. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/statistics.rs +0 -0
  127. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/stats_math.rs +0 -0
  128. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/traits.rs +0 -0
  129. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/headroom_port/transforms/smart_crusher/types.rs +0 -0
  130. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/rust/synapse-tool-compress-core/src/lib.rs +0 -0
  131. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/scripts/install.ps1 +0 -0
  132. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/scripts/release.ps1 +0 -0
  133. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/slash_cmds_upstream.txt +0 -0
  134. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/__init__.py +0 -0
  135. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/__main__.py +0 -0
  136. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/app/__init__.py +0 -0
  137. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/app/agent_md.py +0 -0
  138. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/cli.py +0 -0
  139. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/__init__.py +0 -0
  140. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/compression.py +0 -0
  141. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/helpers.py +0 -0
  142. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/model.py +0 -0
  143. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/result.py +0 -0
  144. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/sessions.py +0 -0
  145. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/slash_cmds.py +0 -0
  146. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/slash_complete.py +0 -0
  147. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/commands/theme.py +0 -0
  148. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/config.py +0 -0
  149. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/content/__init__.py +0 -0
  150. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/content/input_history.py +0 -0
  151. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/content/multimodal.py +0 -0
  152. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/content/skills_catalog.py +0 -0
  153. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/__init__.py +0 -0
  154. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/checkpoint_seed.py +0 -0
  155. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/codex_history.py +0 -0
  156. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/codex_import.py +0 -0
  157. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/codex_sessions.py +0 -0
  158. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/describe_image.py +0 -0
  159. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/http_clients.py +0 -0
  160. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/llm_openai_compat.py +0 -0
  161. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/llm_openai_websocket.py +0 -0
  162. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/openai_oauth.py +0 -0
  163. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/openai_oauth_middleware.py +0 -0
  164. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/integrations/vision_middleware.py +0 -0
  165. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/memory/__init__.py +0 -0
  166. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/memory/auto_recorder.py +0 -0
  167. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/memory/embedder.py +0 -0
  168. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/memory/long_term.py +0 -0
  169. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/models/__init__.py +0 -0
  170. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/models/config.py +0 -0
  171. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/models/helpers.py +0 -0
  172. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/models/profile.py +0 -0
  173. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/models/registry.py +0 -0
  174. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/models_registry.py +0 -0
  175. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/observability/__init__.py +0 -0
  176. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/observability/startup_trace.py +0 -0
  177. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/parallel_subagents.py +0 -0
  178. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/planner/__init__.py +0 -0
  179. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/planner/task_planner.py +0 -0
  180. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/rag/__init__.py +0 -0
  181. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/rag/knowledge_base.py +0 -0
  182. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/__init__.py +0 -0
  183. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/async_runtime.py +0 -0
  184. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/context_compact.py +0 -0
  185. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/execute_capture.py +0 -0
  186. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/fs_permissions.py +0 -0
  187. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/hitl.py +0 -0
  188. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/interaction_ledger.py +0 -0
  189. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/model_request_compression_middleware.py +0 -0
  190. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/pathing.py +0 -0
  191. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/safety.py +0 -0
  192. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/steer.py +0 -0
  193. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/subagents.py +0 -0
  194. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/tool_output_eval.py +0 -0
  195. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/tool_output_middleware.py +0 -0
  196. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/runtime/tool_output_usage_middleware.py +0 -0
  197. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/sessions/__init__.py +0 -0
  198. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/sessions/cancel_repair.py +0 -0
  199. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/sessions/session_recap.py +0 -0
  200. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/sessions/store.py +0 -0
  201. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/sessions/transcript.py +0 -0
  202. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/settings/__init__.py +0 -0
  203. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/settings/config_paths.py +0 -0
  204. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/settings/schema.py +0 -0
  205. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/subagent_monitor.py +0 -0
  206. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tool_output/__init__.py +0 -0
  207. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tool_output/detection.py +0 -0
  208. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tool_output/metrics.py +0 -0
  209. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tool_output/models.py +0 -0
  210. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tool_output/pipeline.py +0 -0
  211. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tool_output/repository.py +0 -0
  212. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tool_output/transformers.py +0 -0
  213. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/tools/session_tools.py +0 -0
  214. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/__init__.py +0 -0
  215. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/answer_divider.py +0 -0
  216. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/__init__.py +0 -0
  217. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/components/__init__.py +0 -0
  218. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/components/key_hints.py +0 -0
  219. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/components/mcp.py +0 -0
  220. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/components/mode.py +0 -0
  221. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/components/model.py +0 -0
  222. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/components/thread.py +0 -0
  223. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/context.py +0 -0
  224. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/bottombar/core.py +0 -0
  225. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/clipboard.py +0 -0
  226. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/__init__.py +0 -0
  227. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/base.py +0 -0
  228. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/codex_session_list.py +0 -0
  229. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/compression_diagnostics.py +0 -0
  230. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/git_explore.py +0 -0
  231. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/mcp_panel.py +0 -0
  232. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/model_picker.py +0 -0
  233. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/safety_panel.py +0 -0
  234. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/session_list.py +0 -0
  235. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/subagent_monitor.py +0 -0
  236. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/theme_designer.py +0 -0
  237. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/dialogs/theme_picker.py +0 -0
  238. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/formatters.py +0 -0
  239. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/git_explore/__init__.py +0 -0
  240. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/git_explore/engine.py +0 -0
  241. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/git_explore/provider.py +0 -0
  242. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/git_explore/unified.py +0 -0
  243. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/rendering.py +0 -0
  244. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/selectable_static.py +0 -0
  245. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/selectable_text.py +0 -0
  246. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/sink.py +0 -0
  247. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/steer_widget.py +0 -0
  248. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/stream.py +0 -0
  249. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/stream_events.py +0 -0
  250. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/stream_runtime.py +0 -0
  251. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/textual_stream_sink.py +0 -0
  252. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/theme.py +0 -0
  253. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/timeline.py +0 -0
  254. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/tool_blocks.py +0 -0
  255. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/__init__.py +0 -0
  256. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/components/__init__.py +0 -0
  257. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/components/branch.py +0 -0
  258. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/components/title.py +0 -0
  259. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/components/tool_output.py +0 -0
  260. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/components/usage.py +0 -0
  261. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/components/workspace.py +0 -0
  262. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/context.py +0 -0
  263. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/core.py +0 -0
  264. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/git_changes_popover.py +0 -0
  265. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/git_chrome.py +0 -0
  266. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/tool_output_popover.py +0 -0
  267. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/topbar/widget.py +0 -0
  268. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/turn_rail.py +0 -0
  269. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/user_turn.py +0 -0
  270. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/user_turn_block.py +0 -0
  271. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/src/synapse/ui/welcome.py +0 -0
  272. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/synapse.cmd +0 -0
  273. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/codex_rollouts/duplicate_response_item.jsonl +0 -0
  274. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/codex_rollouts/incomplete_and_aborted.jsonl +0 -0
  275. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/codex_rollouts/legacy_compaction.jsonl +0 -0
  276. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/codex_rollouts/normal_completed.jsonl +0 -0
  277. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/codex_rollouts/replacement_history.jsonl +0 -0
  278. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/codex_rollouts/rollback.jsonl +0 -0
  279. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/codex_rollouts/unsupported_replacement.jsonl +0 -0
  280. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/sample_repo/calculator.py +0 -0
  281. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/sample_repo/test_calculator.py +0 -0
  282. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/fixtures/tool_output_eval.json +0 -0
  283. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_async_checkpointer.py +0 -0
  284. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_backends.py +0 -0
  285. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_bottombar_registry.py +0 -0
  286. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_cancel_repair.py +0 -0
  287. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_checkpoint_seed.py +0 -0
  288. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_cli.py +0 -0
  289. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_cli_codex_import.py +0 -0
  290. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_codex_history.py +0 -0
  291. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_codex_import.py +0 -0
  292. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_codex_sessions.py +0 -0
  293. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_codex_sessions_windows.py +0 -0
  294. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_config.py +0 -0
  295. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_context_compact.py +0 -0
  296. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_context_window.py +0 -0
  297. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_dag_parallel.py +0 -0
  298. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_dialogs.py +0 -0
  299. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_git_changes_popover.py +0 -0
  300. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_git_chrome.py +0 -0
  301. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_git_explore.py +0 -0
  302. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_http_clients.py +0 -0
  303. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_input_history.py +0 -0
  304. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_iteration_abc.py +0 -0
  305. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_layered_config.py +0 -0
  306. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_mcp_schema.py +0 -0
  307. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_mermaid_render.py +0 -0
  308. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_middleware_async.py +0 -0
  309. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_model_request_compression_middleware.py +0 -0
  310. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_model_retry.py +0 -0
  311. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_multimodal.py +0 -0
  312. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_openai_compat_reasoning.py +0 -0
  313. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_openai_oauth.py +0 -0
  314. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_openai_oauth_middleware.py +0 -0
  315. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_openai_websocket.py +0 -0
  316. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_pathing.py +0 -0
  317. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_safety.py +0 -0
  318. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_session_recap.py +0 -0
  319. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_slash_complete.py +0 -0
  320. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_startup_defer.py +0 -0
  321. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_steer.py +0 -0
  322. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_stream_cancel.py +0 -0
  323. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_stream_tool_items.py +0 -0
  324. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_stream_ui.py +0 -0
  325. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_subagent_monitor_dialog.py +0 -0
  326. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_subagent_status.py +0 -0
  327. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_theme.py +0 -0
  328. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_timeline.py +0 -0
  329. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tool_intent.py +0 -0
  330. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tool_output.py +0 -0
  331. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tool_output_eval.py +0 -0
  332. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tool_output_slash.py +0 -0
  333. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tool_output_usage_middleware.py +0 -0
  334. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_topbar_context_ratio.py +0 -0
  335. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_topbar_registry.py +0 -0
  336. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_transcript.py +0 -0
  337. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tui_codex_import.py +0 -0
  338. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tui_selection.py +0 -0
  339. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_tui_sink.py +0 -0
  340. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_usage_restore.py +0 -0
  341. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_user_turn_style.py +0 -0
  342. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/test_welcome.py +0 -0
  343. {synapse_cli_agent-0.1.16 → synapse_cli_agent-0.1.18}/tests/tool_output_plugin_fixture.py +0 -0
@@ -18,6 +18,7 @@ wheels/
18
18
  .env
19
19
  .coding-agent/
20
20
  .cmd-agent/
21
+ .tmp/
21
22
  .synapse/
22
23
  .pytest_cache/
23
24
  .ruff_cache/
@@ -40,4 +41,4 @@ site/
40
41
 
41
42
  # OS
42
43
  Thumbs.db
43
- .DS_Store
44
+ .DS_Store.tmp/
@@ -1,27 +1,5 @@
1
1
  # Synapse 项目协作指南
2
2
 
3
- ## 用户信息
4
- - 姓名:肖前程
5
- - 年龄:35 岁
6
- - 职业:Java 程序员
7
- - 当前学习方向:Agent 应用开发
8
- - 主语言背景:Java → 正在学习 Python/LangChain Agent 生态
9
-
10
- ## 用户信息
11
- - 姓名:肖前程
12
- - 年龄:35 岁
13
- - 职业:Java 程序员
14
- - 当前学习方向:Agent 应用开发
15
- - 主语言背景:Java → 正在学习 Python/LangChain Agent 生态
16
-
17
- ## 约定
18
- - Python 依赖使用 `uv` 管理。
19
- - 优先小步、可测试的改动。
20
- - CLI 输出简洁,不使用 emoji。
21
- - 默认关闭人工审批(自动放行);除非用户要求,不要自行加审批门槛。
22
- - Backend 仅使用 `LocalShellBackend`,无远程 sandbox。
23
- - 对用户回复与模型思考过程使用中文;代码标识符/路径/命令可保留原文。
24
-
25
3
  ## 当前仓库结构
26
4
 
27
5
  | 路径 | 主要职责 |
@@ -153,3 +131,55 @@ powershell -ExecutionPolicy Bypass -File scripts/release.ps1
153
131
  ```
154
132
 
155
133
  8. 脚本会创建并推送 `v{version}` tag;`.github/workflows/release.yml` 会从 `CHANGELOG.md` 提取同名段落、执行 `uv build` 并创建 GitHub Release。
134
+
135
+ ## 10. cdp_take_screenshot 工具使用说明
136
+
137
+ ### 10.1 已知限制
138
+ - `filePath` 参数无法写入工作区目录(浏览器沙箱限制),所有指定 `filePath` 的尝试均会报 `Access denied`。
139
+ - 不指定 `filePath` 时,截图以文本格式(含 base64)自动存入 `/large_tool_results/call_xx_xxx`。
140
+
141
+ ### 10.2 标准流程:截图 → 解码 → 识图
142
+
143
+ 执行完 `cdp_take_screenshot`(不指定 `filePath`)后,按以下步骤操作:
144
+
145
+ #### Step 1 — 编写/复用解码脚本
146
+ 如果工作区不存在 `/decode_screenshot.py`,创建它:
147
+ ```python
148
+ import base64, re, sys, glob, os
149
+
150
+ files = glob.glob('large_tool_results/call_*')
151
+ if not files:
152
+ print("No screenshot files found")
153
+ sys.exit(1)
154
+
155
+ latest = max(files, key=os.path.getctime)
156
+ print(f"Processing: {latest}")
157
+
158
+ content = open(latest, 'r').read()
159
+ m = re.search(r"data='([^']+)'", content)
160
+ if not m:
161
+ print("No base64 data found")
162
+ sys.exit(1)
163
+
164
+ os.makedirs('.tmp', exist_ok=True)
165
+ out = f'.tmp/screenshot_{os.path.getmtime(latest)}.png'
166
+ open(out, 'wb').write(base64.b64decode(m.group(1)))
167
+ print(f"OK -> {out}")
168
+ ```
169
+
170
+ #### Step 2 — 执行解码
171
+ ```bash
172
+ python decode_screenshot.py
173
+ # 输出示例: OK -> .tmp/screenshot_1712345678.1234567.png
174
+ ```
175
+
176
+ #### Step 3 — 调用识图
177
+ ```bash
178
+ # 使用 Step 2 返回的文件名
179
+ describe_image(image_path="/.tmp/screenshot_1712345678.1234567.png", ...)
180
+ ```
181
+
182
+ ### 10.3 注意事项
183
+ - 每次截图都会在 `/large_tool_results/` 生成一个新文件,解码脚本自动选最新的。
184
+ - 输出到 `/.tmp/` 目录,该目录需加入 `.gitignore`。
185
+ - 不要在对话中手动执行多步解码,应直接调用解码脚本。
@@ -7,6 +7,43 @@ The release workflow automatically extracts the matching section as release note
7
7
 
8
8
  ---
9
9
 
10
+ ## v0.1.18
11
+
12
+ ### 新功能
13
+
14
+ - 新增 LLM Debug Inspector(`F11`),实时监控模型通信、工具调用和 token 消耗。
15
+ - Inspector 支持采集开关、跟随最新、按类型筛选(异常/工具/慢调用)、回合折叠和调用详情查看。
16
+ - Inspector 概览栏显示失败率(基于工具级错误检测),工具标签页展示失败工具及原因。
17
+
18
+ ### 修复
19
+
20
+ - TUI:`F10` 恢复删除 session 弹框入口;修复鼠标选中与点击复制的冲突,拖选后自动复制。
21
+
22
+ ### 工程改进
23
+
24
+ - `DebugCaptureRecord` 增加工具级错误检测(LangChain `ToolMessage.status` + 内容模式)。
25
+ - `_tool_pairs` 返回 `error` 字段,区分 "等待中"(result null)与 "真失败"(有错误内容)。
26
+ - Inspector 前端:失败率仅统计真正失败的工具,"待响应"不计入。
27
+
28
+ ---
29
+
30
+ ## v0.1.17
31
+
32
+ ### 新功能
33
+
34
+ - `find_files` / `search_files` 工具新增 `context_lines`、`case_insensitive`、`head_limit`、`offset` 参数。
35
+ - `search_files` 支持忽略大小写(`case_insensitive`,由 `synapse-search-core` 原生引擎实现)。
36
+ - 支持分页查询(`head_limit` + `offset`),Agent 可按需翻页而非一次性获取全部结果。
37
+
38
+ ### 工程改进
39
+
40
+ - 工具的 Pydantic schema 不再定义 `intent` 字段,改由 `build_intent_schema_middleware` 中间件统一管理。
41
+ - 新增 Synapse 自有文件搜索工具 `find_files` / `search_files`,排除 deepagents 内置 `ls`/`glob`/`grep` 工具。
42
+ - 系统提示词中的 `glob`/`grep` 工具名修正为 `find_files` / `search_files`。
43
+ - `synapse-search-core` 升级至 0.1.1(新增 `case_insensitive` 参数)。
44
+
45
+ ---
46
+
10
47
  ## v0.1.16
11
48
 
12
49
  ### 新功能
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synapse-cli-agent
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: Local coding agent built on LangChain Deep Agents (LocalShell, no sandbox)
5
5
  Project-URL: Homepage, https://github.com/alex8224/synapse-agent
6
6
  Project-URL: Repository, https://github.com/alex8224/synapse-agent
@@ -287,7 +287,7 @@ OpenAI Responses API 同时支持 HTTP/SSE 与普通 LLM WebSocket。profile 中
287
287
  }
288
288
  ```
289
289
 
290
- `vision_model.model`、`base_url`、`api_key_env` 均可更换;`api_key` 也支持,但建议使用环境变量。`think` 是识图模型独立的思考开关,开启后发送 `thinking: {"type": "enabled"}`;它不影响主模型。默认只处理本地/内存图片;将 `allow_remote_urls` 设为 `true` 才会把消息中的 HTTP(S) 图片 URL 转发给识图服务。该服务只用于把图片转换成文字,配置后图片内容会发送到指定服务。多模态主模型会跳过该 middleware,不调用识图服务。
290
+ `vision_model.model`、`base_url`、`api_key_env` 均可更换;`api_key` 也支持,但建议使用环境变量。`think` 是识图模型独立的思考开关,开启后发送 `thinking: {"type": "enabled"}`;它不影响主模型。默认只处理本地/内存图片;将 `allow_remote_urls` 设为 `true` 才会把消息中的 HTTP(S) 图片 URL 转发给识图服务。该服务只用于把图片转换成文字,配置后图片内容会发送到指定服务。主模型 profile 显式配置 `image_input=false` 时,Synapse 还会自动注入 `describe_image` 工具,让 Agent 主动读取并识别工作区内的图片;未配置 `vision_model` 时工具会明确返回不可用。多模态主模型会跳过 middleware,且不会注入该工具。
291
291
 
292
292
  ### MCP 服务器配置
293
293
 
@@ -251,7 +251,7 @@ OpenAI Responses API 同时支持 HTTP/SSE 与普通 LLM WebSocket。profile 中
251
251
  }
252
252
  ```
253
253
 
254
- `vision_model.model`、`base_url`、`api_key_env` 均可更换;`api_key` 也支持,但建议使用环境变量。`think` 是识图模型独立的思考开关,开启后发送 `thinking: {"type": "enabled"}`;它不影响主模型。默认只处理本地/内存图片;将 `allow_remote_urls` 设为 `true` 才会把消息中的 HTTP(S) 图片 URL 转发给识图服务。该服务只用于把图片转换成文字,配置后图片内容会发送到指定服务。多模态主模型会跳过该 middleware,不调用识图服务。
254
+ `vision_model.model`、`base_url`、`api_key_env` 均可更换;`api_key` 也支持,但建议使用环境变量。`think` 是识图模型独立的思考开关,开启后发送 `thinking: {"type": "enabled"}`;它不影响主模型。默认只处理本地/内存图片;将 `allow_remote_urls` 设为 `true` 才会把消息中的 HTTP(S) 图片 URL 转发给识图服务。该服务只用于把图片转换成文字,配置后图片内容会发送到指定服务。主模型 profile 显式配置 `image_input=false` 时,Synapse 还会自动注入 `describe_image` 工具,让 Agent 主动读取并识别工作区内的图片;未配置 `vision_model` 时工具会明确返回不可用。多模态主模型会跳过 middleware,且不会注入该工具。
255
255
 
256
256
  ### MCP 服务器配置
257
257
 
@@ -0,0 +1,26 @@
1
+ import base64
2
+ import glob
3
+ import os
4
+ import re
5
+ import sys
6
+
7
+ # 找到最新的 large_tool_results 文件
8
+ files = glob.glob('large_tool_results/call_*')
9
+ if not files:
10
+ print("No screenshot files found")
11
+ sys.exit(1)
12
+
13
+ latest = max(files, key=os.path.getctime)
14
+ print(f"Processing: {latest}")
15
+
16
+ content = open(latest).read()
17
+ m = re.search(r"data='([^']+)'", content)
18
+ if not m:
19
+ print("No base64 data found")
20
+ sys.exit(1)
21
+
22
+ os.makedirs('.tmp', exist_ok=True)
23
+ ts = str(os.path.getmtime(latest))
24
+ out = f'.tmp/screenshot_{ts}.png'
25
+ open(out, 'wb').write(base64.b64decode(m.group(1)))
26
+ print(f"OK -> {out}")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "synapse-cli-agent"
3
- version = "0.1.16"
3
+ version = "0.1.18"
4
4
  description = "Local coding agent built on LangChain Deep Agents (LocalShell, no sandbox)"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,19 @@
1
+ ## v0.1.18
2
+
3
+ ### 新功能
4
+
5
+ - 新增 LLM Debug Inspector(`F11`),实时监控模型通信、工具调用和 token 消耗。
6
+ - Inspector 支持采集开关、跟随最新、按类型筛选(异常/工具/慢调用)、回合折叠和调用详情查看。
7
+ - Inspector 概览栏显示失败率(基于工具级错误检测),工具标签页展示失败工具及原因。
8
+
9
+ ### 修复
10
+
11
+ - TUI:`F10` 恢复删除 session 弹框入口;修复鼠标选中与点击复制的冲突,拖选后自动复制。
12
+
13
+ ### 工程改进
14
+
15
+ - `DebugCaptureRecord` 增加工具级错误检测(LangChain `ToolMessage.status` + 内容模式)。
16
+ - `_tool_pairs` 返回 `error` 字段,区分 "等待中"(result null)与 "真失败"(有错误内容)。
17
+ - Inspector 前端:失败率仅统计真正失败的工具,"待响应"不计入。
18
+
19
+ ---
@@ -391,7 +391,7 @@ dependencies = [
391
391
 
392
392
  [[package]]
393
393
  name = "synapse-search-core"
394
- version = "0.1.0"
394
+ version = "0.1.1"
395
395
  dependencies = [
396
396
  "globset",
397
397
  "grep-regex",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "synapse-search-core"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  rust-version = "1.80"
6
6
  license = "Apache-2.0"
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "synapse-search-core"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Native workspace search algorithms for Synapse"
9
9
  requires-python = ">=3.12"
10
10
  license = { text = "Apache-2.0" }
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
3
3
  use std::time::UNIX_EPOCH;
4
4
 
5
5
  use globset::{Glob, GlobSet, GlobSetBuilder};
6
- use grep_regex::RegexMatcher;
6
+ use grep_regex::{RegexMatcher, RegexMatcherBuilder};
7
7
  use grep_searcher::{BinaryDetection, Searcher, SearcherBuilder, Sink, SinkContext, SinkMatch};
8
8
  use ignore::WalkBuilder;
9
9
  use pyo3::exceptions::PyValueError;
@@ -121,7 +121,7 @@ fn search_file(
121
121
  Ok(sink.result)
122
122
  }
123
123
 
124
- #[pyfunction(signature = (base_path, pattern, include_glob=None, max_results=1000, context_lines=0))]
124
+ #[pyfunction(signature = (base_path, pattern, include_glob=None, max_results=1000, context_lines=0, case_insensitive=false))]
125
125
  fn grep(
126
126
  py: Python<'_>,
127
127
  base_path: &str,
@@ -129,6 +129,7 @@ fn grep(
129
129
  include_glob: Option<&str>,
130
130
  max_results: usize,
131
131
  context_lines: usize,
132
+ case_insensitive: bool,
132
133
  ) -> PyResult<Py<PyDict>> {
133
134
  if pattern.is_empty() {
134
135
  return Err(PyValueError::new_err("pattern must not be empty"));
@@ -143,7 +144,9 @@ fn grep(
143
144
  base_path.display()
144
145
  )));
145
146
  }
146
- let matcher = RegexMatcher::new(pattern)
147
+ let matcher = RegexMatcherBuilder::new()
148
+ .case_insensitive(case_insensitive)
149
+ .build(pattern)
147
150
  .map_err(|error| PyValueError::new_err(format!("invalid regex pattern: {error}")))?;
148
151
  let include = include_glob
149
152
  .map(compile_glob)
@@ -35,6 +35,7 @@ from synapse.runtime.middleware import (
35
35
  build_strip_redundant_prompt_blocks,
36
36
  build_task_namespace_middleware,
37
37
  build_tool_error_recovery_middleware,
38
+ build_tool_exclusion_middleware,
38
39
  )
39
40
  from synapse.runtime.model_request_compression_middleware import (
40
41
  build_model_request_compression_middleware,
@@ -48,7 +49,11 @@ from synapse.settings import Settings
48
49
  from synapse.tool_output.pipeline import ToolOutputTransformPipeline
49
50
  from synapse.tool_output.repository import ToolOutputRepository
50
51
  from synapse.tool_output.transformers import load_transformer_plugins
51
- from synapse.tools import build_session_tools
52
+ from synapse.tools import (
53
+ build_describe_image_tools,
54
+ build_filesystem_search_tools,
55
+ build_session_tools,
56
+ )
52
57
 
53
58
 
54
59
  def _build_checkpointer(settings: Settings):
@@ -228,6 +233,12 @@ def build_coding_agent(
228
233
  readonly=settings.readonly,
229
234
  excluded_tools=settings.excluded_tools,
230
235
  )
236
+ # Keep deepagents' built-in ``ls``, ``glob``, and ``grep`` out of model
237
+ # requests. Synapse registers the non-conflicting ``find_files`` and
238
+ # ``search_files`` tools explicitly below.
239
+ model_request_excluded_tools = set(settings.excluded_tools) | {"ls", "glob", "grep"}
240
+ if settings.readonly:
241
+ model_request_excluded_tools.update({"execute", "write_file", "edit_file"})
231
242
 
232
243
  interrupt_on = build_interrupt_on(require_approval=settings.require_approval)
233
244
  with span("checkpointer"):
@@ -279,6 +290,7 @@ def build_coding_agent(
279
290
  tools: list[Any] = []
280
291
  if extra_tools:
281
292
  tools.extend(extra_tools)
293
+ tools.extend(build_filesystem_search_tools(backend))
282
294
  # 跨会话查阅工具
283
295
  try:
284
296
  session_tools = build_session_tools(
@@ -382,6 +394,13 @@ def build_coding_agent(
382
394
  getattr(selected_profile, "base_url", None) or getattr(settings, "openai_base_url", None),
383
395
  )
384
396
  vision_config = VisionModelConfig.from_registry(registry, settings)
397
+ tools.extend(
398
+ build_describe_image_tools(
399
+ image_input=getattr(selected_profile, "image_input", None),
400
+ backend=backend,
401
+ config=vision_config,
402
+ )
403
+ )
385
404
 
386
405
  output_repository = ToolOutputRepository(settings.resolved_tool_output_db_path())
387
406
  middleware: list[Any] = [
@@ -392,6 +411,7 @@ def build_coding_agent(
392
411
  ),
393
412
  build_model_retry_middleware(),
394
413
  build_task_namespace_middleware(),
414
+ build_tool_exclusion_middleware(model_request_excluded_tools),
395
415
  ]
396
416
  transform_enabled = bool(getattr(settings, "enable_tool_output_transform", True))
397
417
  try:
@@ -449,6 +469,13 @@ def build_coding_agent(
449
469
 
450
470
  middleware.append(build_openai_oauth_compat_middleware())
451
471
 
472
+ # Debug capture middleware — innermost, records final provider-ready
473
+ # request/response pairs for the Debug Inspector (F11 / /debug).
474
+ from synapse.observability.llm_debug import get_debug_store
475
+ from synapse.runtime.debug_capture_middleware import build_debug_capture_middleware
476
+
477
+ middleware.append(build_debug_capture_middleware(get_debug_store()))
478
+
452
479
  if progress is not None:
453
480
  progress("compiling agent graph")
454
481
  with span("create_deep_agent"):
@@ -1,4 +1,5 @@
1
1
  """MCP slash-command handler and presentation helpers."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import json
@@ -102,6 +103,7 @@ def _md_tool_list(names: list[str], warnings: list[str] | None = None) -> str:
102
103
  lines.append(f"\nwarn: {w}")
103
104
  return "\n".join(lines)
104
105
 
106
+
105
107
  def handle_mcp(
106
108
  args: list[str],
107
109
  *,
@@ -216,8 +218,7 @@ def handle_mcp(
216
218
  md_lines.append("|---|---|")
217
219
  for transport, names in sorted(by_transport.items()):
218
220
  md_lines.append(
219
- f"| {markdown_escape(transport)} | "
220
- f"{markdown_escape(', '.join(names))} |"
221
+ f"| {markdown_escape(transport)} | {markdown_escape(', '.join(names))} |"
221
222
  )
222
223
  if result.tools:
223
224
  md_lines.append("")
@@ -291,9 +292,7 @@ def handle_mcp(
291
292
  loaded = getattr(build_coding_agent, "last_mcp_servers", []) or []
292
293
  warnings_list = getattr(build_coding_agent, "last_mcp_warnings", []) or []
293
294
  tools = getattr(build_coding_agent, "last_mcp_tool_names", []) or []
294
- notice = (
295
- f"mcp '{target}' {'enabled' if changed.enabled else 'disabled'} 路 tools={len(tools)}"
296
- )
295
+ notice = f"mcp '{target}' {'enabled' if changed.enabled else 'disabled'} tools={len(tools)}"
297
296
  lines = [
298
297
  f"mcp server '{target}' {'enabled' if changed.enabled else 'disabled'}; agent rebuilt",
299
298
  f"loaded servers: {', '.join(loaded) or '(none)'}",
@@ -325,7 +324,7 @@ def handle_mcp(
325
324
  return SlashResult(
326
325
  handled=True,
327
326
  lines=["mcp enabled (run /mcp reload to rebuild agent)"],
328
- notice="mcp enabled 路 reload pending",
327
+ notice="mcp enabled reload pending",
329
328
  settings_changed=True,
330
329
  )
331
330
 
@@ -348,7 +347,7 @@ def handle_mcp(
348
347
  return SlashResult(
349
348
  handled=True,
350
349
  lines=["mcp disabled; agent rebuilt without MCP tools"],
351
- notice="mcp disabled 路 tools=0",
350
+ notice="mcp disabled tools=0",
352
351
  agent=new_agent,
353
352
  settings_changed=True,
354
353
  )
@@ -372,9 +371,9 @@ def handle_mcp(
372
371
  warnings = getattr(build_coding_agent, "last_mcp_warnings", []) or []
373
372
  tools = getattr(build_coding_agent, "last_mcp_tool_names", []) or []
374
373
  enabled_flag = bool(getattr(settings, "enable_mcp", True))
375
- notice = f"mcp reloaded 路 servers={len(loaded)} tools={len(tools)}"
374
+ notice = f"mcp reloaded servers={len(loaded)} tools={len(tools)}"
376
375
  if not enabled_flag:
377
- notice = "mcp reloaded 路 disabled"
376
+ notice = "mcp reloaded disabled"
378
377
  lines = [
379
378
  f"agent rebuilt; mcp enabled={getattr(settings, 'enable_mcp', True)}",
380
379
  f"loaded servers: {', '.join(loaded) or '(none)'}",
@@ -46,7 +46,7 @@ Never scan the entire repository without a specific reason.
46
46
 
47
47
  ## Virtual filesystem
48
48
 
49
- File tools such as `ls`, `glob`, `grep`, `read_file`, `edit_file`, and `write_file`
49
+ File tools such as `find_files`, `search_files`, `read_file`, `edit_file`, and `write_file`
50
50
  operate on a virtual filesystem rooted at `/`.
51
51
 
52
52
  Valid paths:
@@ -109,7 +109,7 @@ Do not use generic intent values such as `run tool` or `read_file`.
109
109
  Search only when required by a clear task.
110
110
  Keep searches targeted and avoid unnecessary full-file output.
111
111
 
112
- For `glob` and `grep` tools, automatically exclude common build artifacts and caches:
112
+ For the `search_files` tool, automatically exclude common build artifacts and caches via the `glob` parameter:
113
113
  * `target/`
114
114
  * `.venv/`
115
115
  * `.node_modules/`
@@ -133,7 +133,7 @@ Run independent tool calls in parallel within the same turn.
133
133
  Parallelize when arguments are already known and results do not depend on each other, including:
134
134
 
135
135
  * Multiple file reads
136
- * `glob` and related file reads
136
+ * `find_files` and related file reads
137
137
  * `execute` and related reads
138
138
  * Edits to different files
139
139
  * Independent test or lint commands
@@ -13,11 +13,13 @@ import asyncio
13
13
  import atexit
14
14
  import json
15
15
  import logging
16
+ import os
17
+ import sys
16
18
  import threading
17
19
  from concurrent.futures import Future
18
20
  from dataclasses import dataclass, field
19
21
  from pathlib import Path
20
- from typing import Any
22
+ from typing import Any, TextIO
21
23
 
22
24
  logger = logging.getLogger(__name__)
23
25
 
@@ -71,6 +73,31 @@ def _expand_mapping(raw: dict[str, Any] | None) -> dict[str, str]:
71
73
  return out
72
74
 
73
75
 
76
+ def _valid_subprocess_stream(stream: TextIO | None) -> TextIO | None:
77
+ """Return a stream only when its OS descriptor is valid for inheritance."""
78
+ if stream is None or getattr(stream, "closed", False):
79
+ return None
80
+ try:
81
+ fd = stream.fileno()
82
+ os.fstat(fd)
83
+ except (AttributeError, OSError, ValueError):
84
+ return None
85
+ return stream
86
+
87
+
88
+ def _stdio_errlog() -> tuple[TextIO, bool]:
89
+ """Choose a valid stderr target and report whether the caller owns it.
90
+
91
+ Textual, redirected Windows launchers, and GUI executables can leave
92
+ ``sys.stderr`` present while its underlying descriptor is closed. The MCP
93
+ SDK passes this object to subprocess creation, which then raises EBADF.
94
+ """
95
+ stream = _valid_subprocess_stream(sys.stderr)
96
+ if stream is not None:
97
+ return stream, False
98
+ return open(os.devnull, "w", encoding="utf-8"), True # noqa: SIM115
99
+
100
+
74
101
  def _parse_server(raw: dict[str, Any]) -> McpServerConfig:
75
102
  name = str(raw.get("name") or "").strip()
76
103
  if not name:
@@ -337,10 +364,11 @@ class _LoopThread:
337
364
  class _LiveServer:
338
365
  config: McpServerConfig
339
366
  session: Any
340
- # Keep transport context managers open for process lifetime.
367
+ # Keep transport context managers and owned streams open for process lifetime.
341
368
  transport_cm: Any
342
369
  session_cm: Any
343
370
  streams: Any = None
371
+ errlog: TextIO | None = None
344
372
 
345
373
 
346
374
  class McpSessionPool:
@@ -385,6 +413,8 @@ class McpSessionPool:
385
413
  await live.transport_cm.__aexit__(None, None, None)
386
414
  except Exception: # noqa: BLE001
387
415
  pass
416
+ if live.errlog is not None:
417
+ live.errlog.close()
388
418
  self._servers.clear()
389
419
 
390
420
  async def _open_stdio(self, server: McpServerConfig) -> _LiveServer:
@@ -398,16 +428,38 @@ class McpSessionPool:
398
428
  args=server.args,
399
429
  env=server.env or None,
400
430
  )
401
- transport_cm = stdio_client(params)
402
- read, write = await transport_cm.__aenter__()
403
- session_cm = ClientSession(read, write)
404
- session = await session_cm.__aenter__()
405
- await session.initialize()
431
+ errlog, owns_errlog = _stdio_errlog()
432
+ transport_cm = stdio_client(params, errlog=errlog)
433
+ transport_entered = False
434
+ session_cm = None
435
+ session_entered = False
436
+ try:
437
+ read, write = await transport_cm.__aenter__()
438
+ transport_entered = True
439
+ session_cm = ClientSession(read, write)
440
+ session = await session_cm.__aenter__()
441
+ session_entered = True
442
+ await session.initialize()
443
+ except BaseException:
444
+ if session_entered and session_cm is not None:
445
+ try:
446
+ await session_cm.__aexit__(None, None, None)
447
+ except BaseException:
448
+ pass
449
+ if transport_entered:
450
+ try:
451
+ await transport_cm.__aexit__(None, None, None)
452
+ except BaseException:
453
+ pass
454
+ if owns_errlog:
455
+ errlog.close()
456
+ raise
406
457
  return _LiveServer(
407
458
  config=server,
408
459
  session=session,
409
460
  transport_cm=transport_cm,
410
461
  session_cm=session_cm,
462
+ errlog=errlog if owns_errlog else None,
411
463
  )
412
464
 
413
465
  async def _open_http(self, server: McpServerConfig) -> _LiveServer:
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>Synapse Inspector</title>
7
+ <style>
8
+ :root{color-scheme:dark;--bg:#0d131b;--panel:#121b25;--raised:#182431;--hover:#1d2b3a;--line:#29394b;--text:#e7edf4;--muted:#93a4b7;--blue:#68b5f8;--red:#ff8f8f;--amber:#efc36b;--green:#74c69d}*{box-sizing:border-box}body{margin:0;height:100vh;overflow:hidden;background:var(--bg);color:var(--text);font:13px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}button,input{font:inherit}button{border:0;color:inherit;background:transparent;cursor:pointer}button:focus-visible,input:focus-visible{outline:2px solid var(--blue);outline-offset:2px}.top{height:52px;display:flex;align-items:center;gap:16px;padding:0 16px;border-bottom:1px solid var(--line);background:#101822}.brand{font-size:15px;font-weight:700}.brand span{color:var(--blue)}.capture{display:flex;align-items:center;gap:7px;color:var(--muted)}.switch{width:31px;height:18px;padding:2px;border-radius:10px;background:#465466}.switch i{display:block;width:14px;height:14px;border-radius:50%;background:#edf3f8;transition:.15s}.switch.on{background:var(--green)}.switch.on i{transform:translateX(13px)}.actions{display:flex;align-items:center;gap:4px;margin-left:auto}.btn{min-height:28px;padding:4px 9px;border:1px solid transparent;border-radius:5px;color:var(--muted)}.btn:hover{border-color:var(--line);background:var(--raised);color:var(--text)}.danger:hover{color:var(--red)}#app{display:grid;grid-template-columns:300px minmax(0,1fr);height:calc(100vh - 52px)}#side{display:flex;min-width:0;min-height:0;flex-direction:column;border-right:1px solid var(--line);background:var(--panel)}.side-head{padding:12px;border-bottom:1px solid var(--line)}.side-title{display:flex;justify-content:space-between;color:var(--muted);font-size:11px;font-weight:700}.search{width:100%;height:32px;margin-top:9px;padding:0 9px;border:1px solid var(--line);border-radius:5px;background:#0d151e;color:var(--text)}.filters{display:flex;gap:5px;margin-top:8px}.filter{padding:3px 7px;border:1px solid var(--line);border-radius:4px;color:var(--muted);font-size:11px}.filter.active,.filter:hover{border-color:#3b6d98;background:#1b3044;color:var(--blue)}#records{flex:1;min-height:0;overflow-y:scroll;overflow-x:hidden;padding:6px 0;scrollbar-color:#40546a #111923;scrollbar-width:thin}#records::-webkit-scrollbar{width:9px}#records::-webkit-scrollbar-track{background:#111923}#records::-webkit-scrollbar-thumb{border:2px solid #111923;border-radius:8px;background:#40546a}.turn-group{margin:0}.turn{display:flex;width:100%;align-items:center;gap:7px;padding:8px 12px;color:var(--muted);font-size:10px;font-weight:700;text-align:left;text-transform:uppercase}.turn:hover{background:var(--hover);color:var(--text)}.turn-arrow{width:10px;color:var(--blue);transition:transform .15s}.turn-group.collapsed .turn-arrow{transform:rotate(-90deg)}.turn-count{margin-left:auto;font-weight:500}.turn-group.collapsed .turn-records{display:none}.record{display:block;width:100%;padding:8px 12px;border-left:3px solid transparent;text-align:left}.record:hover{background:var(--hover)}.record.selected{border-left-color:var(--blue);background:#1d3043}.r-top,.r-bottom{display:flex;align-items:center;gap:7px}.r-id{font-weight:700}.r-model{flex:1;overflow:hidden;color:var(--muted);text-overflow:ellipsis;white-space:nowrap}.r-bottom{justify-content:space-between;margin-top:3px;color:var(--muted);font:11px ui-monospace,Consolas,monospace}.bad{color:var(--red)}.warn{color:var(--amber)}.tag{padding:0 5px;border-radius:8px;background:#26384a;color:#b9c8d6}#main{min-width:0;overflow:auto;padding:20px 24px 56px}.empty{display:grid;height:100%;place-content:center;gap:5px;text-align:center;color:var(--muted)}.empty strong{color:var(--text);font-size:16px}.call-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px}.eyebrow{color:var(--blue);font-size:10px;font-weight:700;text-transform:uppercase}h1{margin:3px 0 2px;font-size:20px}.sub{color:var(--muted);font-size:12px}.metrics{display:grid;grid-template-columns:repeat(6,minmax(80px,1fr));margin:16px 0;border:1px solid var(--line);border-radius:6px;background:var(--panel)}.metric{padding:9px 11px;border-right:1px solid var(--line)}.metric:last-child{border:0}.metric label{display:block;color:var(--muted);font-size:10px}.metric strong{display:block;margin-top:1px;font:600 14px ui-monospace,Consolas,monospace}.tabs{display:flex;gap:2px;border-bottom:1px solid var(--line)}.tab{padding:9px 12px;border-bottom:2px solid transparent;color:var(--muted)}.tab.active{border-bottom-color:var(--blue);color:var(--text)}#view{padding-top:14px}.section{margin-bottom:12px}.section-head{display:flex;align-items:center;margin-bottom:7px}.section-head h2{margin:0;font-size:14px}.section-head .meta{margin-left:8px;color:var(--muted);font-size:11px}.section-head .btn{margin-left:auto}.message,.tool-row,.raw-box{border:1px solid var(--line);border-radius:6px;background:var(--panel)}.message{margin-bottom:8px}.message-head{display:flex;align-items:center;padding:7px 10px;border-bottom:1px solid var(--line)}.role{font-size:11px;font-weight:700;text-transform:uppercase}.message-head .meta{margin-left:8px;color:var(--muted);font-size:11px}.message-head .btn{margin-left:auto}.content,pre{margin:0;overflow:auto;padding:10px;white-space:pre-wrap;overflow-wrap:anywhere;color:#dce6ef;font:12px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace}.content{max-height:340px}.tool-summary{padding:9px 11px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--muted)}.tool-row{margin-top:7px;overflow:hidden}.tool-toggle{display:grid;width:100%;grid-template-columns:62px 140px minmax(0,1fr);align-items:center;gap:8px;padding:9px 11px;text-align:left}.tool-toggle:hover{background:var(--hover)}.status{font-size:11px}.status.ok{color:var(--green)}.status.missing{color:var(--amber)}.tool-name{overflow:hidden;color:var(--text);font-weight:650;text-overflow:ellipsis;white-space:nowrap}.tool-intent{overflow:hidden;color:var(--muted);text-overflow:ellipsis;white-space:nowrap}.tool-detail{display:none;grid-template-columns:1fr 1fr;gap:1px;border-top:1px solid var(--line);background:var(--line)}.tool-row.open .tool-detail{display:grid}.tool-side{min-width:0;background:#101821}.tool-label{padding:6px 10px;border-bottom:1px solid var(--line);color:var(--muted);font-size:10px;font-weight:700}.tool-side pre{max-height:300px}.raw-box pre{max-height:65vh}.notice{margin-bottom:10px;padding:9px 11px;border-left:3px solid var(--amber);background:#2a251a;color:#dbcda9}.new-records{display:none;color:var(--blue)}@media(max-width:850px){#app{grid-template-columns:250px minmax(0,1fr)}.metrics{grid-template-columns:repeat(3,1fr)}.metric:nth-child(3),.metric:nth-child(6){border-right:0}.tool-detail{grid-template-columns:1fr}}@media(max-width:650px){#app{display:block}#side{display:none}#main{padding:16px}.metrics{grid-template-columns:repeat(2,1fr)}.metric:nth-child(even){border-right:0}}
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <header class="top"><div class="brand"><span>//</span> Synapse Inspector</div><button class="capture" id="capture" title="开启或暂停模型通信采集">采集 <span class="switch"><i></i></span><strong id="state">关闭</strong></button><div class="actions"><button class="btn new-records" id="newRecords"></button><label class="btn"><input id="follow" type="checkbox" checked> 跟随最新</label><button class="btn" id="refresh">刷新</button><button class="btn danger" id="clear">清空</button></div></header>
13
+ <div id="app"><aside id="side"><div class="side-head"><div class="side-title"><span>调用记录</span><span id="count">0 条</span></div><input class="search" id="search" type="search" placeholder="筛选模型、工具、回合或错误"><div class="filters"><button class="filter active" data-filter="all">全部</button><button class="filter" data-filter="error">异常</button><button class="filter" data-filter="tool">工具</button><button class="filter" data-filter="slow">慢调用</button></div></div><div id="records"></div></aside><main id="main"><div class="empty"><strong>等待通信记录</strong><span>开启采集并运行一个回合后,在这里检查模型调用。</span></div></main></div>
14
+ <script>
15
+ const $=id=>document.getElementById(id);let records=[],selected=-1,loaded=null,filter='all',tab='overview',timer,newCount=0;const collapsedTurns=new Set;const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));const dump=v=>JSON.stringify(v,null,2);const ms=v=>v>=1000?(v/1000).toFixed(2)+'s':Math.round(v||0)+'ms';const tok=v=>Number(v||0).toLocaleString();async function api(url,options){try{const response=await fetch(url,options);return response.ok?response.json():null}catch(_){return null}}function copy(text,button){navigator.clipboard?.writeText(text).then(()=>{const old=button.textContent;button.textContent='已复制';setTimeout(()=>button.textContent=old,900)}).catch(()=>{})}function visibleRows(){const query=$('search').value.trim().toLowerCase();return records.filter(r=>(filter==='all'||filter==='error'&&r.error||filter==='tool'&&r.has_tools||filter==='slow'&&r.duration_ms>=10000)&&(!query||`${r.turn_index} ${r.model_call_index} ${r.model_name} ${r.provider} ${r.error||''} ${r.tool_names||''}`.toLowerCase().includes(query)))}function drawList(){const out=$('records'),rows=visibleRows(),groups=new Map,scrollTop=out.scrollTop;out.innerHTML='';$('count').textContent=`${rows.length} / ${records.length} 条`;for(const record of rows){if(!groups.has(record.turn_index))groups.set(record.turn_index,[]);groups.get(record.turn_index).push(record)}for(const [turn,items] of groups){const group=document.createElement('section'),collapsed=collapsedTurns.has(turn);group.className='turn-group'+(collapsed?' collapsed':'');group.innerHTML=`<button class="turn" aria-expanded="${!collapsed}"><span class="turn-arrow">▼</span><span>回合 ${turn}</span><span class="turn-count">${items.length} 次调用</span></button><div class="turn-records"></div>`;const container=group.querySelector('.turn-records');for(const r of items){const button=document.createElement('button'),tools=r.tool_count?`<span class="tag">${r.tool_count} 工具</span>`:'';button.className='record'+(r.index===selected?' selected':'');button.innerHTML=`<div class="r-top"><span class="r-id ${r.error?'bad':''}">#${r.model_call_index}</span><span class="r-model">${esc(r.model_name)}</span>${r.error?'<span class="bad">失败</span>':''}</div><div class="r-bottom"><span class="${r.duration_ms>=10000?'warn':''}">${ms(r.duration_ms)}</span><span>${tok(r.usage?.input_tokens)} → ${tok(r.usage?.output_tokens)}</span>${tools}</div>`;button.onclick=()=>select(r.index);container.appendChild(button)}group.querySelector('.turn').onclick=()=>{if(collapsedTurns.has(turn))collapsedTurns.delete(turn);else collapsedTurns.add(turn);drawList()};out.appendChild(group)}out.scrollTop=scrollTop}function messageCard(message){const text=message.content_full||message.content_preview||'',hasCalls=(message.tool_calls||[]).length;let body=text&&!looksLikeRawCalls(text)?`<div class="content">${esc(text)}</div>`:'';if(!body&&hasCalls)body=`<div class="tool-summary">模型发起 ${hasCalls} 个结构化工具调用。详细参数请在“工具”标签查看。</div>`;if(!body)body='<div class="tool-summary">此消息没有可展示的文本内容。</div>';return `<article class="message"><div class="message-head"><span class="role">${esc(message.role||'unknown')}</span><span class="meta">${message.name?' / '+esc(message.name):''} ${tok(message.estimated_tokens)} tokens${hasCalls?' · '+hasCalls+' 工具':''}</span><button class="btn copy-message" data-text="${encodeURIComponent(text)}">复制文本</button></div>${body}</article>`}function looksLikeRawCalls(text){return text.includes("'type': 'function_call'")||text.includes('"type":"function_call"')}function section(title,meta,body,raw){return `<section class="section"><div class="section-head"><h2>${title}</h2><span class="meta">${meta}</span>${raw?'<button class="btn copy-section">复制 JSON</button>':''}</div>${body}</section>`}function intentOf(pair){try{const value=typeof pair.args==='string'?JSON.parse(pair.args):pair.args;return value?.intent||value?.command||value?.file_path||value?.pattern||''}catch(_){const match=String(pair.args||'').match(/["']intent["']\s*:\s*["']([^"']+)/);return match?.[1]||''}}function toolsView(pairs){if(!pairs.length)return '<div class="empty"><strong>本次调用没有关联工具</strong><span>这里只展示本次请求新增的工具结果和本次响应发起的工具调用。</span></div>';const failed=pairs.filter(p=>p.error===true);let failHtml='';if(failed.length)failHtml=`<div class="notice bad">${failed.length} 个工具失败:${failed.map(p=>esc(p.name)+' — '+esc((p.result||'').slice(0,100))).join('<br>')}</div>`;return `${failHtml}<div class="notice">默认折叠参数和结果;缺失响应的工具会优先展开。</div>${pairs.map((pair,index)=>`<article class="tool-row ${pair.result==null?'open':''}"><button class="tool-toggle"><span class="status ${pair.error===true?'bad':pair.result==null?'missing':'ok'}">${pair.error===true?'失败':pair.result==null?'待响应':'已完成'}</span><span class="tool-name">${esc(pair.name)}</span><span class="tool-intent">${esc(intentOf(pair)||pair.id)}</span></button><div class="tool-detail"><div class="tool-side"><div class="tool-label">调用参数</div><pre>${esc(pair.args||'{}')}</pre></div><div class="tool-side"><div class="tool-label">工具响应</div><pre>${esc(pair.result??'尚未捕获对应工具响应')}</pre></div></div></article>`).join('')}`}
16
+ function renderView(record){const req=record.request_messages||[],delta=req.slice(record.request_delta_start||0),resp=record.response_messages||[],pairs=record.tool_pairs||[];let html;if(tab==='overview'){const directCalls=resp.reduce((n,m)=>n+(m.tool_calls||[]).length,0),results=delta.filter(m=>m.role==='tool').length;html=section('本次请求新增',`${delta.length} 条 · 约 ${tok(delta.reduce((n,m)=>n+(m.estimated_tokens||0),0))} tokens`,delta.length?delta.map(messageCard).join(''):'<div class="tool-summary">本次调用沿用同回合上下文,没有新增请求消息。</div>',dump(delta))+section('模型输出',`${resp.length} 条消息 · ${directCalls} 个工具调用`,resp.length?resp.map(messageCard).join(''):'<div class="tool-summary">模型没有返回消息。</div>',dump(resp))+section('执行摘要',`${results} 条工具结果输入 · ${directCalls} 个新工具调用`,`<div class="tool-summary">本次调用接收了 ${results} 条工具结果,并发起 ${directCalls} 个后续工具调用。切换到“工具”查看结构化参数和结果。</div>`)}else if(tab==='request'){html=section('请求增量',`${delta.length} 新增 / ${req.length} 总消息`,delta.map(messageCard).join('')||'<div class="tool-summary">没有新增消息。</div>',dump(delta))+section('完整请求',`${req.length} 条消息`,req.map(messageCard).join(''),dump(req))}else if(tab==='response'){html=section('响应消息',`${resp.length} 条 · ${tok(record.usage?.output_tokens)} output tokens`,resp.map(messageCard).join('')||'<div class="tool-summary">模型没有返回消息。</div>',dump(resp))}else if(tab==='tools'){html=toolsView(pairs)}else{html='<div class="raw-box"><div class="tool-summary">正在加载捕获快照…</div></div>';loadRaw(record)}$('view').innerHTML=html;bindView(record)}function bindView(record){$('view').querySelectorAll('.tool-toggle').forEach(button=>button.onclick=()=>button.parentElement.classList.toggle('open'));$('view').querySelectorAll('.copy-message').forEach(button=>button.onclick=()=>copy(decodeURIComponent(button.dataset.text),button));$('view').querySelectorAll('.copy-section').forEach(button=>button.onclick=()=>{const title=button.parentElement.querySelector('h2').textContent;let value=title.includes('完整')?record.request_messages:title.includes('响应')?record.response_messages:(record.request_messages||[]).slice(record.request_delta_start||0);copy(dump(value),button)})}async function loadRaw(record){const raw=await api('/api/records/'+selected+'/raw');if(tab!=='raw'||loaded!==record)return;const box=$('view');box.innerHTML=`<section class="section"><div class="section-head"><h2>捕获快照</h2><span class="meta">按需加载</span><button class="btn" id="copyRaw">复制 JSON</button></div><div class="raw-box"><pre>${esc(dump(raw))}</pre></div></section>`;$('copyRaw').onclick=()=>copy(dump(raw),$('copyRaw'))}function drawDetail(record){if(!record){$('main').innerHTML='<div class="empty"><strong>等待通信记录</strong><span>开启采集并运行一个回合后,在这里检查模型调用。</span></div>';return}const pairs=record.tool_pairs||[],delta=(record.request_messages||[]).slice(record.request_delta_start||0),newTokens=delta.reduce((n,m)=>n+(m.estimated_tokens||0),0),tools=pairs.length,failed=tools?pairs.filter(p=>p.error===true).length:0,failRate=tools?Math.round(failed/tools*100)+'%':'—',status=record.error?'失败':'已完成';$('main').innerHTML=`<div class="call-head"><div><div class="eyebrow">模型通信 / 回合 ${record.turn_index}</div><h1>调用 #${record.model_call_index} <span class="${record.error?'bad':''}">${status}</span></h1><div class="sub">${esc(record.provider)} · ${esc(record.model_name)} · ${new Date((record.started_at||0)*1000).toLocaleString()}</div></div><button class="btn" id="copyRawTop">复制原始记录</button></div><div class="metrics"><div class="metric"><label>耗时</label><strong>${ms(record.duration_ms)}</strong></div><div class="metric"><label>总输入</label><strong>${tok(record.usage?.input_tokens)}</strong></div><div class="metric"><label>本次新增</label><strong>+${tok(newTokens)}</strong></div><div class="metric"><label>输出</label><strong>${tok(record.usage?.output_tokens)}</strong></div><div class="metric"><label>关联工具</label><strong>${tools}</strong></div><div class="metric"><label>失败率</label><strong>${failRate}</strong></div></div>${record.error?`<div class="notice bad">${esc(record.error)}</div>`:''}<nav class="tabs">${[['overview','概览'],['request','请求'],['response','响应'],['tools',`工具 ${tools}`],['raw','原始']].map(([id,label])=>`<button class="tab ${tab===id?'active':''}" data-tab="${id}">${label}</button>`).join('')}</nav><div id="view"></div>`;$('main').querySelectorAll('.tab').forEach(button=>button.onclick=()=>{tab=button.dataset.tab;drawDetail(record)});$('copyRawTop').onclick=async()=>{const raw=await api('/api/records/'+selected+'/raw');if(raw)copy(dump(raw),$('copyRawTop'))};renderView(record)}async function select(index){selected=index;drawList();$('main').innerHTML='<div class="empty"><strong>正在加载调用详情</strong></div>';const record=await api('/api/records/'+index);if(selected===index&&record){loaded=record;drawDetail(record)}}async function refresh(forceFollow=false){const [status,next]=await Promise.all([api('/api/status'),api('/api/records')]);if(status){$('capture').querySelector('.switch').classList.toggle('on',!!status.enabled);$('state').textContent=status.enabled?'开启':'关闭'}if(!next)return;const previousCount=records.length,hadSelection=selected>=0;records=next;drawList();if(records.length>previousCount&&hadSelection&&!$('follow').checked){newCount+=records.length-previousCount;$('newRecords').style.display='block';$('newRecords').textContent=`${newCount} 条新记录`}if((!hadSelection||forceFollow||$('follow').checked&&records.length>previousCount)&&records.length)select(records.at(-1).index);else if(selected>=records.length){selected=-1;loaded=null;drawDetail(null)}}$('capture').onclick=async()=>{await api('/api/toggle',{method:'POST'});refresh()};$('refresh').onclick=()=>refresh(false);$('newRecords').onclick=()=>{newCount=0;$('newRecords').style.display='none';refresh(true)};$('clear').onclick=async()=>{if(records.length&&confirm('清空全部已采集的通信记录?')){await api('/api/clear',{method:'POST'});records=[];selected=-1;loaded=null;drawList();drawDetail(null)}};$('search').oninput=drawList;document.querySelectorAll('.filter').forEach(button=>button.onclick=()=>{filter=button.dataset.filter;document.querySelectorAll('.filter').forEach(item=>item.classList.toggle('active',item===button));drawList()});document.addEventListener('visibilitychange',()=>{clearInterval(timer);if(!document.hidden)timer=setInterval(refresh,1500)});refresh();timer=setInterval(refresh,1500);
17
+ </script>
18
+ </body>
19
+ </html>