sliceagent 0.1.2__tar.gz → 0.1.4__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 (204) hide show
  1. {sliceagent-0.1.2 → sliceagent-0.1.4}/CHANGELOG.md +27 -0
  2. {sliceagent-0.1.2 → sliceagent-0.1.4}/PKG-INFO +3 -4
  3. {sliceagent-0.1.2 → sliceagent-0.1.4}/QUICKSTART.md +6 -4
  4. {sliceagent-0.1.2 → sliceagent-0.1.4}/README.md +2 -3
  5. {sliceagent-0.1.2 → sliceagent-0.1.4}/install.sh +2 -3
  6. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/__init__.py +1 -1
  7. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/cli.py +2 -1
  8. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/llm.py +18 -1
  9. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/onboarding.py +92 -11
  10. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/tui.py +96 -0
  11. sliceagent-0.1.4/tests/test_menu_select.py +109 -0
  12. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_onboarding.py +7 -7
  13. sliceagent-0.1.4/tests/test_provider_lineup.py +95 -0
  14. {sliceagent-0.1.2 → sliceagent-0.1.4}/.dockerignore +0 -0
  15. {sliceagent-0.1.2 → sliceagent-0.1.4}/.env.example +0 -0
  16. {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/dependabot.yml +0 -0
  20. {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/workflows/ci.yml +0 -0
  21. {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/workflows/publish.yml +0 -0
  22. {sliceagent-0.1.2 → sliceagent-0.1.4}/.gitignore +0 -0
  23. {sliceagent-0.1.2 → sliceagent-0.1.4}/CODE_OF_CONDUCT.md +0 -0
  24. {sliceagent-0.1.2 → sliceagent-0.1.4}/CONTRIBUTING.md +0 -0
  25. {sliceagent-0.1.2 → sliceagent-0.1.4}/Dockerfile +0 -0
  26. {sliceagent-0.1.2 → sliceagent-0.1.4}/LICENSE +0 -0
  27. {sliceagent-0.1.2 → sliceagent-0.1.4}/NOTICE +0 -0
  28. {sliceagent-0.1.2 → sliceagent-0.1.4}/SECURITY.md +0 -0
  29. {sliceagent-0.1.2 → sliceagent-0.1.4}/examples/plugins/hello/__init__.py +0 -0
  30. {sliceagent-0.1.2 → sliceagent-0.1.4}/examples/plugins/hello/plugin.toml +0 -0
  31. {sliceagent-0.1.2 → sliceagent-0.1.4}/pyproject.toml +0 -0
  32. {sliceagent-0.1.2 → sliceagent-0.1.4}/scripts/gen_config_reference.py +0 -0
  33. {sliceagent-0.1.2 → sliceagent-0.1.4}/scripts/run_tests.sh +0 -0
  34. {sliceagent-0.1.2 → sliceagent-0.1.4}/sliceagent.toml.example +0 -0
  35. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/__main__.py +0 -0
  36. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/access.py +0 -0
  37. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/agents.py +0 -0
  38. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/background_review.py +0 -0
  39. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/binsniff.py +0 -0
  40. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/clock.py +0 -0
  41. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/code_grep.py +0 -0
  42. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/code_index.py +0 -0
  43. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/config.py +0 -0
  44. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/context_overflow.py +0 -0
  45. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/envspec.py +0 -0
  46. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/errors.py +0 -0
  47. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/events.py +0 -0
  48. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/finding_types.py +0 -0
  49. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/flags.py +0 -0
  50. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/fuzzy.py +0 -0
  51. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/guardrails.py +0 -0
  52. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/guidance.py +0 -0
  53. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/hippocampus.py +0 -0
  54. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/hooks.py +0 -0
  55. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/interfaces.py +0 -0
  56. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/loop.py +0 -0
  57. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/mcp_client.py +0 -0
  58. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/mcp_security.py +0 -0
  59. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/memory.py +0 -0
  60. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/metrics.py +0 -0
  61. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/model_catalog.py +0 -0
  62. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/monitor.py +0 -0
  63. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/neocortex.py +0 -0
  64. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/oracle.py +0 -0
  65. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/pagetable.py +0 -0
  66. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/pfc.py +0 -0
  67. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/plugins.py +0 -0
  68. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/policy.py +0 -0
  69. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/procman.py +0 -0
  70. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/prompt.py +0 -0
  71. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/records.py +0 -0
  72. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/recovery.py +0 -0
  73. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/regions.py +0 -0
  74. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/registry.py +0 -0
  75. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/retriever.py +0 -0
  76. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/safety.py +0 -0
  77. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/sandbox.py +0 -0
  78. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/scheduler.py +0 -0
  79. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/search_index.py +0 -0
  80. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/seed.py +0 -0
  81. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/sensory_cortex.py +0 -0
  82. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/session.py +0 -0
  83. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/skill_provenance.py +0 -0
  84. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/skill_usage.py +0 -0
  85. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/skills.py +0 -0
  86. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/subagent.py +0 -0
  87. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/subdir_hints.py +0 -0
  88. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/swap.py +0 -0
  89. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/taskstate.py +0 -0
  90. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/telemetry.py +0 -0
  91. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/terminal.py +0 -0
  92. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/text_utils.py +0 -0
  93. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/tool_summary.py +0 -0
  94. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/tools.py +0 -0
  95. {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/web.py +0 -0
  96. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/README.md +0 -0
  97. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_active_focus.py +0 -0
  98. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_agents.py +0 -0
  99. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_ask_echo.py +0 -0
  100. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_ask_user.py +0 -0
  101. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_background_review.py +0 -0
  102. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_binary_view.py +0 -0
  103. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bound_is_relevance.py +0 -0
  104. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_breadth_e2e.py +0 -0
  105. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_2b.py +0 -0
  106. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_breadth_wave.py +0 -0
  107. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_current_request.py +0 -0
  108. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_linenum.py +0 -0
  109. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_loop_wave.py +0 -0
  110. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_memory_wave.py +0 -0
  111. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_p0a.py +0 -0
  112. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_p0b.py +0 -0
  113. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_p0c.py +0 -0
  114. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_selfreview.py +0 -0
  115. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_selfreview_siblings.py +0 -0
  116. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_skills_h1.py +0 -0
  117. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_slice_wave.py +0 -0
  118. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_subagent_wave.py +0 -0
  119. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_tools_wave.py +0 -0
  120. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bughunt_fixes.py +0 -0
  121. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_cache_manifest.py +0 -0
  122. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_checkpoint.py +0 -0
  123. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_cli_smoke.py +0 -0
  124. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_closure.py +0 -0
  125. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_code_grep.py +0 -0
  126. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_code_index.py +0 -0
  127. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_consolidate.py +0 -0
  128. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_context_overflow.py +0 -0
  129. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_conversation.py +0 -0
  130. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_coresidency.py +0 -0
  131. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_echo_before_blocking.py +0 -0
  132. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_episode.py +0 -0
  133. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_errors_backoff.py +0 -0
  134. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_esc_sentinel.py +0 -0
  135. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_exec_env.py +0 -0
  136. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_finding_types.py +0 -0
  137. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_flags.py +0 -0
  138. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_fuzzy.py +0 -0
  139. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_ghost_index.py +0 -0
  140. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_guardrails.py +0 -0
  141. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_guidance.py +0 -0
  142. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_history.py +0 -0
  143. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_invariant_fixes.py +0 -0
  144. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_live_composer.py +0 -0
  145. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_llm_streaming.py +0 -0
  146. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_llm_usage_cache.py +0 -0
  147. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_llm_watchdog.py +0 -0
  148. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_log_redaction.py +0 -0
  149. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_loop_overflow.py +0 -0
  150. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mcp_output_cap.py +0 -0
  151. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mcp_runtime.py +0 -0
  152. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_memory.py +0 -0
  153. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_memory_persist.py +0 -0
  154. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_metrics.py +0 -0
  155. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mining.py +0 -0
  156. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mission_tier.py +0 -0
  157. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_model_catalog.py +0 -0
  158. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_monitor.py +0 -0
  159. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_pageout_compaction.py +0 -0
  160. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_permission_patterns.py +0 -0
  161. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_pinned_composer.py +0 -0
  162. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_plan_tier.py +0 -0
  163. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_procman.py +0 -0
  164. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_product_features.py +0 -0
  165. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_progress_status.py +0 -0
  166. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_prompt_ab.py +0 -0
  167. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_provenance.py +0 -0
  168. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_proxy_interrupt.py +0 -0
  169. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_reach.py +0 -0
  170. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_readonly_subagent.py +0 -0
  171. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_recall_search.py +0 -0
  172. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_records.py +0 -0
  173. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_refault.py +0 -0
  174. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_registry_validation.py +0 -0
  175. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_reliability.py +0 -0
  176. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_repo_map_bounded.py +0 -0
  177. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_requirements.py +0 -0
  178. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_review_fixes.py +0 -0
  179. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_route_lexical.py +0 -0
  180. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_safety.py +0 -0
  181. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_seal_markdown.py +0 -0
  182. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_search_index.py +0 -0
  183. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_skill_meta.py +0 -0
  184. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_skill_metadata.py +0 -0
  185. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_subdir_hints.py +0 -0
  186. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_task_agnostic.py +0 -0
  187. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_task_state_roundtrip.py +0 -0
  188. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_telemetry_convergence.py +0 -0
  189. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_terminal.py +0 -0
  190. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tool_dedup.py +0 -0
  191. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tool_result_ok.py +0 -0
  192. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tools_robust.py +0 -0
  193. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_topic_switch.py +0 -0
  194. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_trajectory.py +0 -0
  195. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui.py +0 -0
  196. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui_menus.py +0 -0
  197. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui_render.py +0 -0
  198. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui_widgets.py +0 -0
  199. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_turn_budget.py +0 -0
  200. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_verification_agent.py +0 -0
  201. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_web.py +0 -0
  202. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_workspace.py +0 -0
  203. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_world_region.py +0 -0
  204. {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_world_state.py +0 -0
@@ -5,6 +5,33 @@ this project aims for [Semantic Versioning](https://semver.org/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.4] — 2026-07-02
9
+
10
+ ### Fixed
11
+ - **Wizard menus render as a proper vertical list.** The single-line selector wrapped with six long
12
+ provider labels, and its clear-one-line redraw stacked copies of itself down the screen. New
13
+ `_menu_select`: one option per row, width-clamped labels (wrap impossible), in-place cursor-up
14
+ redraws, explicit `\r\n` in raw mode. PTY-tested, including the anti-stacking invariant.
15
+
16
+ ## [0.1.3] — 2026-07-02
17
+
18
+ The five-door provider lineup + a wizard that feels like one.
19
+
20
+ ### Added
21
+ - **Providers**: OpenRouter (hundreds of models, one key — now the first door), OpenAI,
22
+ **Anthropic/Claude** (new — via Anthropic's OpenAI-compatible endpoint), DeepSeek, Moonshot/Kimi,
23
+ plus custom endpoints. All five ride the single adapter — zero new dependencies.
24
+ - **OpenRouter quirks**: reasoning intent maps to OpenRouter's unified `reasoning` object (works
25
+ WITH tools — the raw `reasoning_effort` param never could); tool-calling requests pin routing to
26
+ hosts that honor every param (`require_parameters`) so nothing silently degrades; per-call
27
+ `usage.cost` is parsed into the cost meter.
28
+ - `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENROUTER_API_KEY` accepted as key fallbacks.
29
+
30
+ ### Changed
31
+ - **Wizard UX**: API key shows `******` as you type (no more invisible field); provider and model
32
+ are arrow-key menus (↑/↓ + Enter, Esc cancels) with per-provider model suggestions +
33
+ "type another model id…". Scripted/CI runs keep the plain typed flow.
34
+
8
35
  ## [0.1.2] — 2026-07-02
9
36
 
10
37
  Nothing may hang the user — three fixes from a live first-run reproduction.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sliceagent
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A coding agent with a new context-engineering framework: bounded, deterministic, reconstructed context (slice architecture) — built for long-horizon work.
5
5
  Author: TT-Wang
6
6
  License-Expression: MIT
@@ -145,11 +145,10 @@ Footprint is light (no torch). `pip install -e .` works for a clone too. Homebre
145
145
  ## Quickstart
146
146
 
147
147
  ```bash
148
- sliceagent init # guided setup: provider, API key, model ~/.sliceagent/config.toml (tests your key)
149
- sliceagent # start the agent
148
+ sliceagent # that's it — first run walks you through setup (provider, API key, live-tested), then you're in
150
149
  ```
151
150
 
152
- `init` writes the config so the next run needs no env vars. Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) and skip `init` — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
151
+ Setup happens once, in-process, and writes `~/.sliceagent/config.toml` (0600) so every later run just starts. Re-configure anytime with `sliceagent init` (add/switch providers). Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
153
152
 
154
153
  → Full walkthrough in **[QUICKSTART.md](QUICKSTART.md)** · **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[CHANGELOG.md](CHANGELOG.md)**
155
154
 
@@ -15,12 +15,14 @@ installed by default; everything still works without it (plain stdout).
15
15
  ## 2. Set up your provider
16
16
 
17
17
  ```bash
18
- sliceagent init
18
+ sliceagent
19
19
  ```
20
20
 
21
- `init` walks you through it: pick a provider (Moonshot/Kimi, OpenAI, DeepSeek, or a custom OpenAI-compatible
22
- endpoint), paste your API key (hidden), choose a model. It then **tests the key with one request** and writes
23
- `~/.sliceagent/config.toml` (mode `0600` — it holds your key). The next `sliceagent` just works.
21
+ Nothing to memorize the **first run walks you through setup automatically**: pick a provider
22
+ (OpenRouter hundreds of models with one key — or OpenAI, Anthropic/Claude, DeepSeek, Moonshot/Kimi, or a custom OpenAI-compatible endpoint), paste your API key (hidden),
23
+ choose a model. It **tests the key with one request**, writes `~/.sliceagent/config.toml` (mode `0600` —
24
+ it holds your key), and drops you straight into the session. Every later `sliceagent` just starts.
25
+ (Re-configure anytime — add or switch providers — with `sliceagent init`.)
24
26
 
25
27
  Prefer environment variables? Skip `init` and export them instead:
26
28
 
@@ -112,11 +112,10 @@ Footprint is light (no torch). `pip install -e .` works for a clone too. Homebre
112
112
  ## Quickstart
113
113
 
114
114
  ```bash
115
- sliceagent init # guided setup: provider, API key, model ~/.sliceagent/config.toml (tests your key)
116
- sliceagent # start the agent
115
+ sliceagent # that's it — first run walks you through setup (provider, API key, live-tested), then you're in
117
116
  ```
118
117
 
119
- `init` writes the config so the next run needs no env vars. Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) and skip `init` — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
118
+ Setup happens once, in-process, and writes `~/.sliceagent/config.toml` (0600) so every later run just starts. Re-configure anytime with `sliceagent init` (add/switch providers). Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
120
119
 
121
120
  → Full walkthrough in **[QUICKSTART.md](QUICKSTART.md)** · **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[CHANGELOG.md](CHANGELOG.md)**
122
121
 
@@ -96,9 +96,8 @@ cat <<'EOF'
96
96
 
97
97
  ✓ sliceagent installed.
98
98
 
99
- Next:
100
- sliceagent init # guided setup: provider, API key, model (tests your key)
101
- sliceagent # start the agent
99
+ Next — just one command:
100
+ sliceagent # first run walks you through setup (provider, API key), then you're chatting
102
101
 
103
102
  If 'sliceagent' isn't found, open a NEW terminal (PATH was just updated).
104
103
  Docs: https://github.com/TT-Wang/sliceagent
@@ -1,3 +1,3 @@
1
1
  """sliceagent — a memory-native coding agent (deterministic, bounded, reconstructed context)."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
@@ -171,7 +171,8 @@ def main() -> None:
171
171
 
172
172
  def _key_present() -> bool:
173
173
  return bool(os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
174
- or os.environ.get("MOONSHOT_API_KEY"))
174
+ or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("DEEPSEEK_API_KEY")
175
+ or os.environ.get("OPENROUTER_API_KEY") or os.environ.get("MOONSHOT_API_KEY"))
175
176
 
176
177
  def _first_run_setup(reason: str) -> bool:
177
178
  """First-run UX: a bare interactive `sliceagent` with nothing configured drops STRAIGHT into the
@@ -81,6 +81,9 @@ def _usage_dict(raw) -> dict | None:
81
81
  }
82
82
  if cache_read:
83
83
  usage["cached_tokens"] = cache_read # legacy key (only when present)
84
+ _cost = getattr(raw, "cost", None) # OpenRouter: authoritative $ per call
85
+ if isinstance(_cost, (int, float)) and _cost >= 0:
86
+ usage["cost_usd"] = float(_cost)
84
87
  return usage
85
88
 
86
89
 
@@ -189,7 +192,9 @@ class OpenAILLM:
189
192
  # export OPENAI_API_KEY) — the surface the user configures says "LLM", not a provider name.
190
193
  # Resolve the ENDPOINT first: the proxy choice below depends on which provider it is.
191
194
  kwargs: dict = {"api_key": api_key or os.environ.get("LLM_API_KEY")
192
- or os.environ.get("OPENAI_API_KEY") or os.environ.get("MOONSHOT_API_KEY")}
195
+ or os.environ.get("OPENAI_API_KEY") or os.environ.get("ANTHROPIC_API_KEY")
196
+ or os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENROUTER_API_KEY")
197
+ or os.environ.get("MOONSHOT_API_KEY")}
193
198
  resolved_base = base_url or os.environ.get("LLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL")
194
199
  if not resolved_base and os.environ.get("MOONSHOT_API_KEY") and not (
195
200
  os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")):
@@ -466,6 +471,12 @@ class OpenAILLM:
466
471
  from .model_catalog import capability
467
472
  r = self.reasoning
468
473
  model, base = self.model.lower(), self._base_url.lower()
474
+ if "openrouter" in base:
475
+ # OpenRouter's UNIFIED reasoning object translates effort per upstream vendor (OpenAI
476
+ # reasoning_effort / Anthropic thinking budget / Gemini thinkingLevel) and — unlike raw
477
+ # reasoning_effort on chat/completions — works WITH tools. "full" stays provider-default.
478
+ effort = {"fast": "low", "high": "high", "max": "high"}.get(r)
479
+ return {"extra_body": {"reasoning": {"effort": effort}}} if effort else {}
469
480
  if "deepseek" in model or "deepseek" in base:
470
481
  return {"extra_body": {"thinking": {"type": "disabled"}}} if r == "fast" else {}
471
482
  if not capability(self.model, self._base_url).supports_reasoning_effort:
@@ -637,6 +648,12 @@ class OpenAILLM:
637
648
  self._merge_kwargs(kwargs, self._cache_routing_kwargs()) # session-stable cache routing (0 added tokens)
638
649
  self._merge_kwargs(kwargs, self._reasoning_kwargs())
639
650
  self._merge_kwargs(kwargs, self._cache_kwargs(messages))
651
+ if "openrouter" in self._base_url.lower() and tools:
652
+ # OpenRouter routes one model slug across MANY hosts; some serve quantizations with broken
653
+ # tool calling, and unsupported params are DROPPED silently by default. require_parameters
654
+ # pins routing to hosts that honor every param we sent — a silent degrade becomes a visible
655
+ # error instead (the exact failure class behind the old reasoning-effort ceiling).
656
+ self._merge_kwargs(kwargs, {"extra_body": {"provider": {"require_parameters": True}}})
640
657
  # Provider quirk (isolated here, llm-agnostic): some reasoning models (gpt-5.5) reject
641
658
  # reasoning_effort TOGETHER with function tools on /v1/chat/completions (400 — "use /v1/responses").
642
659
  # Once seen, drop reasoning_effort whenever tools are present so we degrade to default reasoning
@@ -13,14 +13,65 @@ import os
13
13
  from .envspec import GROUPS, REGISTRY, current_value
14
14
 
15
15
  # provider presets: key → (label, base_url, default_model). 'custom' prompts for the base_url.
16
+ # The lineup: one AGGREGATOR door (OpenRouter — breadth, one key) + four first-party doors
17
+ # (OpenAI, Anthropic, DeepSeek, Moonshot). Anthropic rides its OpenAI-compatible endpoint, so all
18
+ # five share the single OpenAILLM adapter — no per-provider SDKs.
16
19
  PROVIDERS = {
17
- "1": ("moonshot", "Moonshot (Kimi)", "https://api.moonshot.cn/v1", "kimi-k2.7-code"),
20
+ "1": ("openrouter", "OpenRouter (hundreds of models, one key)", "https://openrouter.ai/api/v1", "openai/gpt-5.5"),
18
21
  "2": ("openai", "OpenAI", "", "gpt-5.5"),
19
- "3": ("deepseek", "DeepSeek", "https://api.deepseek.com/v1", "deepseek-chat"),
20
- "4": ("custom", "Custom OpenAI-compatible endpoint", "", ""),
22
+ "3": ("anthropic", "Anthropic (Claude)", "https://api.anthropic.com/v1", "claude-sonnet-5"),
23
+ "4": ("deepseek", "DeepSeek", "https://api.deepseek.com/v1", "deepseek-chat"),
24
+ "5": ("moonshot", "Moonshot (Kimi)", "https://api.moonshot.cn/v1", "kimi-k2.7-code"),
25
+ "6": ("custom", "Custom OpenAI-compatible endpoint", "", ""),
26
+ }
27
+
28
+ # wizard model MENU per provider — suggestions for the arrow picker, NOT a cap: "type another…"
29
+ # always accepts any model id (the catalog/pricing layers stay the capability source of truth).
30
+ MODEL_SUGGESTIONS = {
31
+ "openrouter": ["openai/gpt-5.5", "anthropic/claude-sonnet-5", "deepseek/deepseek-chat",
32
+ "moonshotai/kimi-k2.7"],
33
+ "openai": ["gpt-5.5", "gpt-5", "gpt-5-mini", "o3"],
34
+ "anthropic": ["claude-sonnet-5", "claude-opus-4-8", "claude-haiku-4-5"],
35
+ "deepseek": ["deepseek-chat", "deepseek-reasoner"],
36
+ "moonshot": ["kimi-k2.7-code", "kimi-k2-0905-preview"],
37
+ "custom": [],
21
38
  }
22
39
 
23
40
 
41
+ def _tty() -> bool:
42
+ import sys
43
+ try:
44
+ return sys.stdin.isatty() and sys.stdout.isatty()
45
+ except Exception: # noqa: BLE001
46
+ return False
47
+
48
+
49
+ def _masked_input(prompt_text: str, fallback):
50
+ """API-key entry with VISIBLE feedback — asterisks per keystroke (prompt_toolkit is_password)
51
+ instead of getpass's fully-invisible field, which first-run users read as 'nothing is happening'.
52
+ Falls back to getpass when the TUI stack is absent or anything goes wrong."""
53
+ try:
54
+ from prompt_toolkit import prompt as _ptk_prompt
55
+ return _ptk_prompt(prompt_text, is_password=True)
56
+ except (ImportError, OSError, EOFError):
57
+ return fallback(prompt_text)
58
+
59
+
60
+ def _pick(options: list, default: int = 0):
61
+ """Arrow-key selector for the wizard — the VERTICAL menu (_menu_select: one option per row), NOT
62
+ the single-line _arrow_select, whose one-line redraw wraps and stacks with 6 long provider labels
63
+ (live-repro'd). Returns the index; raises KeyboardInterrupt on Esc (→ the wizard's normal
64
+ 'cancelled' path); returns None when a selector can't safely run → caller falls back to typed."""
65
+ try:
66
+ from .tui import _menu_select
67
+ except ImportError:
68
+ return None
69
+ idx = _menu_select(options, default=default)
70
+ if idx == -1:
71
+ raise KeyboardInterrupt
72
+ return idx
73
+
74
+
24
75
  def _version() -> str:
25
76
  try:
26
77
  from . import __version__
@@ -182,11 +233,24 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
182
233
  if ans in ("n", "no"):
183
234
  out(" Leaving the existing config unchanged. Run `sliceagent` to start."); return 0
184
235
 
185
- out("\n Choose a provider:")
186
- for k, (_id, label, base, model) in PROVIDERS.items():
187
- out(f" {k}. {label}" + (f" ({model})" if model else ""))
236
+ # arrow-key niceties only on a REAL interactive run with the default input hooks — injected
237
+ # inp/getpw (tests, scripted runs) keep the typed flow byte-for-byte.
238
+ fancy = inp is input and getpw is __import__("getpass").getpass and _tty()
239
+ keys = sorted(PROVIDERS)
188
240
  try:
189
- choice = inp(" > ").strip() or "1"
241
+ out("\n Choose a provider (↑/↓ + Enter):" if fancy else "\n Choose a provider:")
242
+ idx = None
243
+ if fancy:
244
+ labels = [f"{PROVIDERS[k][1]}" + (f" — {PROVIDERS[k][3]}" if PROVIDERS[k][3] else "")
245
+ for k in keys]
246
+ idx = _pick(labels) # Esc → KeyboardInterrupt → 'cancelled' below
247
+ if idx is not None:
248
+ choice = keys[idx]
249
+ else: # typed fallback (no tty / selector unavailable)
250
+ for k in keys:
251
+ _id, label, _base, model0 = PROVIDERS[k]
252
+ out(f" {k}. {label}" + (f" ({model0})" if model0 else ""))
253
+ choice = inp(" > ").strip() or "1"
190
254
  pid, label, base_url, model = PROVIDERS.get(choice, PROVIDERS["1"])
191
255
  if pid == "custom":
192
256
  base_url = inp(" Base URL (OpenAI-compatible, e.g. https://host/v1): ").strip()
@@ -198,10 +262,27 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
198
262
  existing = existing.get(pid) if isinstance(existing, dict) else None
199
263
  existing = existing if isinstance(existing, dict) else {}
200
264
  existing_key = existing.get("api_key") or ""
201
- key_prompt = " API key (hidden, Enter to keep existing): " if existing_key else " API key (hidden): "
202
- key = getpw(key_prompt).strip() or existing_key
203
- model = (inp(f" Model [{existing.get('model') or model or 'required'}]: ").strip()
204
- or existing.get("model") or model)
265
+ key_prompt = (" API key (typed as ******, Enter to keep existing): " if existing_key
266
+ else " API key (typed as ******): ") if fancy else (
267
+ " API key (hidden, Enter to keep existing): " if existing_key
268
+ else " API key (hidden): ")
269
+ key = (_masked_input(key_prompt, getpw) if fancy else getpw(key_prompt)).strip() or existing_key
270
+ picked_model = None
271
+ if fancy:
272
+ cur = existing.get("model") or model
273
+ opts = [m for m in ([cur] if cur else []) + MODEL_SUGGESTIONS.get(pid, []) if m]
274
+ opts = list(dict.fromkeys(opts)) + ["type another model id…"]
275
+ out(" Model (↑/↓ + Enter):")
276
+ midx = _pick(opts)
277
+ if midx is not None:
278
+ picked_model = None if opts[midx].startswith("type another") else opts[midx]
279
+ if picked_model is None:
280
+ picked_model = inp(" Model id: ").strip()
281
+ if picked_model:
282
+ model = picked_model
283
+ else:
284
+ model = (inp(f" Model [{existing.get('model') or model or 'required'}]: ").strip()
285
+ or existing.get("model") or model)
205
286
  except (EOFError, KeyboardInterrupt):
206
287
  out("\n cancelled."); return 1
207
288
  if not key:
@@ -1267,6 +1267,102 @@ def _arrow_select(options: list[str], default: int = 0) -> "int | None":
1267
1267
  return idx
1268
1268
 
1269
1269
 
1270
+ def _menu_select(options: list[str], default: int = 0) -> "int | None":
1271
+ """VERTICAL arrow-key menu — one option per row: ↑/↓ (or ←/→) move, Enter chooses, Esc/Ctrl-C
1272
+ cancels. Exists because the single-line _arrow_select WRAPS with long/many options, and its
1273
+ clear-one-line redraw then stacks copies of itself down the screen (live-repro'd in the init
1274
+ wizard's 6-entry provider menu). This sibling owns EXACTLY len(options) rows: labels are clamped
1275
+ below the terminal width so a row can never wrap, and each redraw walks the cursor up over its
1276
+ own rows. Raw-mode note: OPOST is off, so every line break is an explicit \\r\\n (the staircase
1277
+ lesson). Same gates + return contract as _arrow_select: index | -1 cancelled | None when a
1278
+ selector can't safely run (caller falls back to typed input)."""
1279
+ import shutil
1280
+ import sys
1281
+ import threading
1282
+ if threading.current_thread() is not threading.main_thread():
1283
+ return None
1284
+ if not (sys.stdin.isatty() and sys.stdout.isatty()):
1285
+ return None
1286
+ try:
1287
+ import termios
1288
+ import tty
1289
+ except Exception: # noqa: BLE001 — non-POSIX → typed fallback
1290
+ return None
1291
+ fd = sys.stdin.fileno()
1292
+ try:
1293
+ old = termios.tcgetattr(fd)
1294
+ except Exception: # noqa: BLE001
1295
+ return None
1296
+ width = max(20, shutil.get_terminal_size((80, 24)).columns - 8)
1297
+ rows = [o if len(o) <= width else o[: width - 1] + "…" for o in options]
1298
+ idx = default if 0 <= default < len(rows) else 0
1299
+
1300
+ def draw(first: bool = False) -> None:
1301
+ if not first:
1302
+ sys.stdout.write(f"\x1b[{len(rows)}A") # walk back up over OUR rows only
1303
+ parts = []
1304
+ for i, o in enumerate(rows):
1305
+ body = f"\x1b[7m ▸ {o} \x1b[0m" if i == idx else f"\x1b[2m {o} \x1b[0m"
1306
+ parts.append("\r\x1b[2K " + body)
1307
+ sys.stdout.write("\r\n".join(parts) + "\r\n") # explicit \r\n: raw mode, OPOST off
1308
+ sys.stdout.flush()
1309
+
1310
+ raw_entered = False
1311
+ try:
1312
+ tty.setraw(fd)
1313
+ raw_entered = True
1314
+ try:
1315
+ termios.tcflush(fd, termios.TCIFLUSH)
1316
+ except Exception: # noqa: BLE001
1317
+ pass
1318
+ draw(first=True)
1319
+ while True:
1320
+ try:
1321
+ data = os.read(fd, 16) # raw fd read — see _arrow_select's note
1322
+ except OSError:
1323
+ idx = -1
1324
+ break
1325
+ if not data:
1326
+ idx = -1
1327
+ break
1328
+ if data[:1] in (b"\r", b"\n"): # Enter → choose
1329
+ break
1330
+ if data[:1] == b"\x03": # Ctrl-C → cancel
1331
+ idx = -1
1332
+ break
1333
+ if data[:1] == b"\x1b":
1334
+ if len(data) == 1: # bare ESC → cancel
1335
+ idx = -1
1336
+ break
1337
+ if data[1:2] in (b"[", b"O"):
1338
+ arrow = data[2:3]
1339
+ if arrow in (b"B", b"C"): # ↓ / →
1340
+ idx = (idx + 1) % len(rows)
1341
+ elif arrow in (b"A", b"D"): # ↑ / ←
1342
+ idx = (idx - 1) % len(rows)
1343
+ draw()
1344
+ if b"\r" in data[3:] or b"\n" in data[3:]:
1345
+ break
1346
+ continue
1347
+ except Exception: # noqa: BLE001 — any I/O error → typed fallback, never corrupt the terminal
1348
+ idx = None
1349
+ finally:
1350
+ if raw_entered:
1351
+ try:
1352
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
1353
+ except Exception: # noqa: BLE001
1354
+ try:
1355
+ termios.tcsetattr(fd, termios.TCSANOW, old)
1356
+ except Exception: # noqa: BLE001
1357
+ pass
1358
+ try:
1359
+ sys.stdout.write("\r") # cursor already sits below the block
1360
+ sys.stdout.flush()
1361
+ except Exception: # noqa: BLE001
1362
+ pass
1363
+ return idx
1364
+
1365
+
1270
1366
  def confirm(console: Console, name: str, detail: str, reason: str) -> str:
1271
1367
  """Approval prompt used by the permission hook when the TUI is active. Synchronous (no pt app
1272
1368
  is live mid-run), returns 'yes' | 'no' | 'always'. Arrow-key selectable; falls back to a typed
@@ -0,0 +1,109 @@
1
+ """PTY tests for _menu_select — the wizard's VERTICAL arrow menu. Regression for the live wizard bug:
2
+ the single-line _arrow_select wrapped with 6 long provider labels and its one-line redraw stacked copies
3
+ of itself down the screen. The vertical menu must (a) return the arrow-chosen index, (b) cancel on Esc,
4
+ and (c) redraw IN PLACE — cursor-up over exactly its own N rows (the anti-stacking invariant).
5
+ Run: PYTHONPATH=src python tests/test_menu_select.py (skips cleanly where no pty is available)
6
+ """
7
+ import os
8
+ import pty
9
+ import select
10
+ import subprocess
11
+ import sys
12
+ import time
13
+
14
+ _SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src")
15
+ sys.path.insert(0, _SRC)
16
+
17
+ CHECKS = []
18
+ def check(fn):
19
+ CHECKS.append(fn)
20
+ return fn
21
+
22
+
23
+ _OPTS = [f"Provider number {i} with a deliberately long label to force the old wrap bug ({i})"
24
+ for i in range(6)]
25
+
26
+ _CHILD = (
27
+ "import os, sys\n"
28
+ "from sliceagent.tui import _menu_select\n"
29
+ f"opts = {_OPTS!r}\n"
30
+ "idx = _menu_select(opts, default=0)\n"
31
+ "os._exit(100 if idx is None else (99 if idx == -1 else 42 + idx))\n"
32
+ )
33
+
34
+
35
+ def _run(presses: list) -> tuple:
36
+ """Run the child on a pty; return (exit_code, output_bytes)."""
37
+ try:
38
+ master, slave = pty.openpty()
39
+ except OSError:
40
+ return None, b""
41
+ env = {**os.environ, "PYTHONPATH": os.path.abspath(_SRC), "COLUMNS": "120", "LINES": "40"}
42
+ p = subprocess.Popen([sys.executable, "-c", _CHILD], stdin=slave, stdout=slave,
43
+ stderr=subprocess.DEVNULL, env=env, start_new_session=True)
44
+ os.close(slave)
45
+ buf = bytearray()
46
+ deadline = time.monotonic() + 15
47
+ sent = 0
48
+ while time.monotonic() < deadline and p.poll() is None:
49
+ r, _, _ = select.select([master], [], [], 0.1)
50
+ if r:
51
+ try:
52
+ buf.extend(os.read(master, 4096))
53
+ except OSError:
54
+ break
55
+ # feed keys only once the menu has rendered (its last row is on screen)
56
+ if sent < len(presses) and _OPTS[-1][:20].encode() in buf:
57
+ os.write(master, presses[sent]); sent += 1
58
+ time.sleep(0.15)
59
+ try:
60
+ p.wait(timeout=5)
61
+ except subprocess.TimeoutExpired:
62
+ p.kill(); p.wait()
63
+ try:
64
+ os.close(master)
65
+ except OSError:
66
+ pass
67
+ return p.returncode, bytes(buf)
68
+
69
+
70
+ @check
71
+ def down_down_enter_picks_the_third_option():
72
+ rc, _out = _run([b"\x1b[B", b"\x1b[B", b"\r"])
73
+ if rc is None:
74
+ return # no pty here — skip
75
+ assert rc == 44, f"expected idx 2 (exit 44), got {rc}"
76
+
77
+
78
+ @check
79
+ def esc_cancels():
80
+ rc, _out = _run([b"\x1b"])
81
+ if rc is None:
82
+ return
83
+ assert rc == 99, f"expected cancel (exit 99), got {rc}"
84
+
85
+
86
+ @check
87
+ def redraw_is_in_place_not_stacked():
88
+ rc, out = _run([b"\x1b[B", b"\r"])
89
+ if rc is None:
90
+ return
91
+ assert rc == 43, f"expected idx 1 (exit 43), got {rc}"
92
+ # the anti-stacking invariant: every redraw walks the cursor UP over its own 6 rows
93
+ assert b"\x1b[6A" in out, "redraw must cursor-up over exactly its own rows (in-place, not stacking)"
94
+ # and rows never exceed the terminal width (wrap is what broke the single-line selector)
95
+ import re
96
+ plain = re.sub(rb"\x1b\[[0-9;]*[A-Za-z]", b"", out)
97
+ for line in plain.split(b"\r\n"):
98
+ assert len(line) <= 120, f"a rendered row exceeds the terminal width ({len(line)} cols) — wrap risk"
99
+
100
+
101
+ if __name__ == "__main__":
102
+ ok = 0
103
+ for fn in CHECKS:
104
+ try:
105
+ fn(); ok += 1; print(f"PASS {fn.__name__}")
106
+ except Exception as e: # noqa: BLE001
107
+ print(f"FAIL {fn.__name__}: {type(e).__name__}: {e}")
108
+ print(f"\n{ok}/{len(CHECKS)} passed")
109
+ sys.exit(0 if ok == len(CHECKS) else 1)
@@ -134,7 +134,7 @@ class _OkLLM:
134
134
  def init_writes_a_valid_config_on_success():
135
135
  home = tempfile.mkdtemp(prefix="init-")
136
136
  rc = onboarding.run_init(
137
- inp=_seq("1", ""), # provider=moonshot, model=default
137
+ inp=_seq("5", ""), # provider=moonshot (menu slot 5), model=default
138
138
  getpw=_seq("sk-test-key"),
139
139
  llm_factory=lambda model: _OkLLM(),
140
140
  home=home)
@@ -180,7 +180,7 @@ def init_offers_to_save_after_a_failed_key_test():
180
180
  def init_custom_provider_prompts_base_url():
181
181
  home = tempfile.mkdtemp(prefix="init-custom-")
182
182
  rc = onboarding.run_init(
183
- inp=_seq("4", "https://my.host/v1", "my-model"), # custom → base url → model
183
+ inp=_seq("6", "https://my.host/v1", "my-model"), # custom → base url → model (menu: 6 = custom)
184
184
  getpw=_seq("k"),
185
185
  llm_factory=lambda model: _OkLLM(),
186
186
  home=home)
@@ -193,7 +193,7 @@ def init_custom_provider_prompts_base_url():
193
193
  @check
194
194
  def init_aborts_without_a_key():
195
195
  home = tempfile.mkdtemp(prefix="init-nokey-")
196
- rc = onboarding.run_init(inp=_seq("1", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
196
+ rc = onboarding.run_init(inp=_seq("5", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
197
197
  assert rc == 1, "no key → abort"
198
198
  assert not os.path.exists(os.path.join(home, ".sliceagent", "config.toml"))
199
199
 
@@ -224,7 +224,7 @@ def write_config_is_atomic_and_cleans_up_on_failure():
224
224
  @check
225
225
  def init_merge_keeps_existing_providers():
226
226
  home = tempfile.mkdtemp(prefix="init-merge-")
227
- onboarding.run_init(inp=_seq("1", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
227
+ onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
228
228
  # 2nd init on the existing config: "Add/update? [Y/n]" → "" (yes) → provider 2 (openai) → model ""
229
229
  onboarding.run_init(inp=_seq("", "2", ""), getpw=_seq("k-oai"), llm_factory=lambda m: _OkLLM(), home=home)
230
230
  data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
@@ -239,10 +239,10 @@ def reinit_blank_key_keeps_existing_key_for_the_same_provider():
239
239
  # prompt (no retype) must KEEP the saved key/model, not abort — the abort-on-blank behavior
240
240
  # (init_aborts_without_a_key) is only correct for a provider with no existing entry.
241
241
  home = tempfile.mkdtemp(prefix="init-reblank-")
242
- onboarding.run_init(inp=_seq("1", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
242
+ onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
243
243
  # 2nd init: "Add/update? [Y/n]" → "" (yes) → provider 1 (moonshot, already saved) → key "" (blank,
244
244
  # keep existing) → model "" (blank, keep existing)
245
- rc = onboarding.run_init(inp=_seq("", "1", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
245
+ rc = onboarding.run_init(inp=_seq("", "5", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
246
246
  assert rc == 0, "blank key on an already-configured provider must NOT abort"
247
247
  data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
248
248
  assert data["providers"]["moonshot"]["api_key"] == "k-moon", "the existing key must be kept, not wiped"
@@ -252,7 +252,7 @@ def reinit_blank_key_keeps_existing_key_for_the_same_provider():
252
252
  @check
253
253
  def config_use_switches_default_provider():
254
254
  home = tempfile.mkdtemp(prefix="cfg-use-")
255
- onboarding.run_init(inp=_seq("1", ""), getpw=_seq("k1"), llm_factory=lambda m: _OkLLM(), home=home)
255
+ onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k1"), llm_factory=lambda m: _OkLLM(), home=home)
256
256
  onboarding.run_init(inp=_seq("", "2", ""), getpw=_seq("k2"), llm_factory=lambda m: _OkLLM(), home=home)
257
257
  assert onboarding.run_config(["--use", "moonshot"], home=home) == 0
258
258
  data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))