sliceagent 0.1.3__tar.gz → 0.1.5__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.3 → sliceagent-0.1.5}/CHANGELOG.md +14 -0
  2. {sliceagent-0.1.3 → sliceagent-0.1.5}/PKG-INFO +1 -1
  3. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/__init__.py +1 -1
  4. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/onboarding.py +14 -11
  5. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/tui.py +96 -0
  6. sliceagent-0.1.5/tests/test_menu_select.py +109 -0
  7. {sliceagent-0.1.3 → sliceagent-0.1.5}/.dockerignore +0 -0
  8. {sliceagent-0.1.3 → sliceagent-0.1.5}/.env.example +0 -0
  9. {sliceagent-0.1.3 → sliceagent-0.1.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {sliceagent-0.1.3 → sliceagent-0.1.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {sliceagent-0.1.3 → sliceagent-0.1.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  12. {sliceagent-0.1.3 → sliceagent-0.1.5}/.github/dependabot.yml +0 -0
  13. {sliceagent-0.1.3 → sliceagent-0.1.5}/.github/workflows/ci.yml +0 -0
  14. {sliceagent-0.1.3 → sliceagent-0.1.5}/.github/workflows/publish.yml +0 -0
  15. {sliceagent-0.1.3 → sliceagent-0.1.5}/.gitignore +0 -0
  16. {sliceagent-0.1.3 → sliceagent-0.1.5}/CODE_OF_CONDUCT.md +0 -0
  17. {sliceagent-0.1.3 → sliceagent-0.1.5}/CONTRIBUTING.md +0 -0
  18. {sliceagent-0.1.3 → sliceagent-0.1.5}/Dockerfile +0 -0
  19. {sliceagent-0.1.3 → sliceagent-0.1.5}/LICENSE +0 -0
  20. {sliceagent-0.1.3 → sliceagent-0.1.5}/NOTICE +0 -0
  21. {sliceagent-0.1.3 → sliceagent-0.1.5}/QUICKSTART.md +0 -0
  22. {sliceagent-0.1.3 → sliceagent-0.1.5}/README.md +0 -0
  23. {sliceagent-0.1.3 → sliceagent-0.1.5}/SECURITY.md +0 -0
  24. {sliceagent-0.1.3 → sliceagent-0.1.5}/examples/plugins/hello/__init__.py +0 -0
  25. {sliceagent-0.1.3 → sliceagent-0.1.5}/examples/plugins/hello/plugin.toml +0 -0
  26. {sliceagent-0.1.3 → sliceagent-0.1.5}/install.sh +0 -0
  27. {sliceagent-0.1.3 → sliceagent-0.1.5}/pyproject.toml +0 -0
  28. {sliceagent-0.1.3 → sliceagent-0.1.5}/scripts/gen_config_reference.py +0 -0
  29. {sliceagent-0.1.3 → sliceagent-0.1.5}/scripts/run_tests.sh +0 -0
  30. {sliceagent-0.1.3 → sliceagent-0.1.5}/sliceagent.toml.example +0 -0
  31. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/__main__.py +0 -0
  32. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/access.py +0 -0
  33. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/agents.py +0 -0
  34. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/background_review.py +0 -0
  35. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/binsniff.py +0 -0
  36. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/cli.py +0 -0
  37. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/clock.py +0 -0
  38. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/code_grep.py +0 -0
  39. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/code_index.py +0 -0
  40. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/config.py +0 -0
  41. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/context_overflow.py +0 -0
  42. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/envspec.py +0 -0
  43. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/errors.py +0 -0
  44. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/events.py +0 -0
  45. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/finding_types.py +0 -0
  46. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/flags.py +0 -0
  47. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/fuzzy.py +0 -0
  48. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/guardrails.py +0 -0
  49. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/guidance.py +0 -0
  50. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/hippocampus.py +0 -0
  51. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/hooks.py +0 -0
  52. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/interfaces.py +0 -0
  53. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/llm.py +0 -0
  54. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/loop.py +0 -0
  55. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/mcp_client.py +0 -0
  56. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/mcp_security.py +0 -0
  57. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/memory.py +0 -0
  58. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/metrics.py +0 -0
  59. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/model_catalog.py +0 -0
  60. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/monitor.py +0 -0
  61. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/neocortex.py +0 -0
  62. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/oracle.py +0 -0
  63. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/pagetable.py +0 -0
  64. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/pfc.py +0 -0
  65. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/plugins.py +0 -0
  66. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/policy.py +0 -0
  67. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/procman.py +0 -0
  68. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/prompt.py +0 -0
  69. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/records.py +0 -0
  70. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/recovery.py +0 -0
  71. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/regions.py +0 -0
  72. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/registry.py +0 -0
  73. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/retriever.py +0 -0
  74. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/safety.py +0 -0
  75. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/sandbox.py +0 -0
  76. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/scheduler.py +0 -0
  77. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/search_index.py +0 -0
  78. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/seed.py +0 -0
  79. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/sensory_cortex.py +0 -0
  80. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/session.py +0 -0
  81. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/skill_provenance.py +0 -0
  82. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/skill_usage.py +0 -0
  83. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/skills.py +0 -0
  84. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/subagent.py +0 -0
  85. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/subdir_hints.py +0 -0
  86. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/swap.py +0 -0
  87. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/taskstate.py +0 -0
  88. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/telemetry.py +0 -0
  89. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/terminal.py +0 -0
  90. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/text_utils.py +0 -0
  91. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/tool_summary.py +0 -0
  92. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/tools.py +0 -0
  93. {sliceagent-0.1.3 → sliceagent-0.1.5}/src/sliceagent/web.py +0 -0
  94. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/README.md +0 -0
  95. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_active_focus.py +0 -0
  96. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_agents.py +0 -0
  97. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_ask_echo.py +0 -0
  98. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_ask_user.py +0 -0
  99. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_background_review.py +0 -0
  100. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_binary_view.py +0 -0
  101. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bound_is_relevance.py +0 -0
  102. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_breadth_e2e.py +0 -0
  103. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_2b.py +0 -0
  104. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_breadth_wave.py +0 -0
  105. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_current_request.py +0 -0
  106. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_linenum.py +0 -0
  107. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_loop_wave.py +0 -0
  108. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_memory_wave.py +0 -0
  109. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_p0a.py +0 -0
  110. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_p0b.py +0 -0
  111. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_p0c.py +0 -0
  112. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_selfreview.py +0 -0
  113. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_selfreview_siblings.py +0 -0
  114. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_skills_h1.py +0 -0
  115. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_slice_wave.py +0 -0
  116. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_subagent_wave.py +0 -0
  117. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bugfix_tools_wave.py +0 -0
  118. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_bughunt_fixes.py +0 -0
  119. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_cache_manifest.py +0 -0
  120. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_checkpoint.py +0 -0
  121. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_cli_smoke.py +0 -0
  122. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_closure.py +0 -0
  123. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_code_grep.py +0 -0
  124. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_code_index.py +0 -0
  125. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_consolidate.py +0 -0
  126. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_context_overflow.py +0 -0
  127. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_conversation.py +0 -0
  128. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_coresidency.py +0 -0
  129. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_echo_before_blocking.py +0 -0
  130. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_episode.py +0 -0
  131. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_errors_backoff.py +0 -0
  132. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_esc_sentinel.py +0 -0
  133. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_exec_env.py +0 -0
  134. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_finding_types.py +0 -0
  135. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_flags.py +0 -0
  136. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_fuzzy.py +0 -0
  137. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_ghost_index.py +0 -0
  138. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_guardrails.py +0 -0
  139. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_guidance.py +0 -0
  140. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_history.py +0 -0
  141. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_invariant_fixes.py +0 -0
  142. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_live_composer.py +0 -0
  143. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_llm_streaming.py +0 -0
  144. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_llm_usage_cache.py +0 -0
  145. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_llm_watchdog.py +0 -0
  146. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_log_redaction.py +0 -0
  147. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_loop_overflow.py +0 -0
  148. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_mcp_output_cap.py +0 -0
  149. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_mcp_runtime.py +0 -0
  150. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_memory.py +0 -0
  151. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_memory_persist.py +0 -0
  152. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_metrics.py +0 -0
  153. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_mining.py +0 -0
  154. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_mission_tier.py +0 -0
  155. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_model_catalog.py +0 -0
  156. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_monitor.py +0 -0
  157. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_onboarding.py +0 -0
  158. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_pageout_compaction.py +0 -0
  159. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_permission_patterns.py +0 -0
  160. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_pinned_composer.py +0 -0
  161. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_plan_tier.py +0 -0
  162. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_procman.py +0 -0
  163. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_product_features.py +0 -0
  164. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_progress_status.py +0 -0
  165. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_prompt_ab.py +0 -0
  166. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_provenance.py +0 -0
  167. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_provider_lineup.py +0 -0
  168. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_proxy_interrupt.py +0 -0
  169. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_reach.py +0 -0
  170. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_readonly_subagent.py +0 -0
  171. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_recall_search.py +0 -0
  172. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_records.py +0 -0
  173. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_refault.py +0 -0
  174. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_registry_validation.py +0 -0
  175. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_reliability.py +0 -0
  176. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_repo_map_bounded.py +0 -0
  177. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_requirements.py +0 -0
  178. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_review_fixes.py +0 -0
  179. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_route_lexical.py +0 -0
  180. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_safety.py +0 -0
  181. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_seal_markdown.py +0 -0
  182. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_search_index.py +0 -0
  183. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_skill_meta.py +0 -0
  184. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_skill_metadata.py +0 -0
  185. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_subdir_hints.py +0 -0
  186. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_task_agnostic.py +0 -0
  187. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_task_state_roundtrip.py +0 -0
  188. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_telemetry_convergence.py +0 -0
  189. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_terminal.py +0 -0
  190. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_tool_dedup.py +0 -0
  191. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_tool_result_ok.py +0 -0
  192. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_tools_robust.py +0 -0
  193. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_topic_switch.py +0 -0
  194. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_trajectory.py +0 -0
  195. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_tui.py +0 -0
  196. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_tui_menus.py +0 -0
  197. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_tui_render.py +0 -0
  198. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_tui_widgets.py +0 -0
  199. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_turn_budget.py +0 -0
  200. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_verification_agent.py +0 -0
  201. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_web.py +0 -0
  202. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_workspace.py +0 -0
  203. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_world_region.py +0 -0
  204. {sliceagent-0.1.3 → sliceagent-0.1.5}/tests/test_world_state.py +0 -0
@@ -5,6 +5,20 @@ this project aims for [Semantic Versioning](https://semver.org/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.5] — 2026-07-02
9
+
10
+ ### Changed
11
+ - Wizard step order is now **provider → model → key** ("choose what you want, then prove you can") —
12
+ the key is the last thing typed, so the live test follows it immediately.
13
+
14
+ ## [0.1.4] — 2026-07-02
15
+
16
+ ### Fixed
17
+ - **Wizard menus render as a proper vertical list.** The single-line selector wrapped with six long
18
+ provider labels, and its clear-one-line redraw stacked copies of itself down the screen. New
19
+ `_menu_select`: one option per row, width-clamped labels (wrap impossible), in-place cursor-up
20
+ redraws, explicit `\r\n` in raw mode. PTY-tested, including the anti-stacking invariant.
21
+
8
22
  ## [0.1.3] — 2026-07-02
9
23
 
10
24
  The five-door provider lineup + a wizard that feels like one.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sliceagent
3
- Version: 0.1.3
3
+ Version: 0.1.5
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
@@ -1,3 +1,3 @@
1
1
  """sliceagent — a memory-native coding agent (deterministic, bounded, reconstructed context)."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.5"
@@ -58,14 +58,15 @@ def _masked_input(prompt_text: str, fallback):
58
58
 
59
59
 
60
60
  def _pick(options: list, default: int = 0):
61
- """Arrow-key selector for the wizard (reuses the TUI's _arrow_select: ↑/↓ or ←/→ + Enter).
62
- Returns the index; raises KeyboardInterrupt on Esc (→ the wizard's normal 'cancelled' path);
63
- returns None when a selector can't safely run so the caller falls back to typed input."""
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."""
64
65
  try:
65
- from .tui import _arrow_select
66
+ from .tui import _menu_select
66
67
  except ImportError:
67
68
  return None
68
- idx = _arrow_select(options, default=default)
69
+ idx = _menu_select(options, default=default)
69
70
  if idx == -1:
70
71
  raise KeyboardInterrupt
71
72
  return idx
@@ -260,12 +261,8 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
260
261
  existing = _read_config(path).get("providers")
261
262
  existing = existing.get(pid) if isinstance(existing, dict) else None
262
263
  existing = existing if isinstance(existing, dict) else {}
263
- existing_key = existing.get("api_key") or ""
264
- key_prompt = (" API key (typed as ******, Enter to keep existing): " if existing_key
265
- else " API key (typed as ******): ") if fancy else (
266
- " API key (hidden, Enter to keep existing): " if existing_key
267
- else " API key (hidden): ")
268
- key = (_masked_input(key_prompt, getpw) if fancy else getpw(key_prompt)).strip() or existing_key
264
+ # ORDER (user-decided): provider → MODEL → key. "Choose what you want, then prove you can"
265
+ # the key is the last thing typed, so the live test follows it immediately.
269
266
  picked_model = None
270
267
  if fancy:
271
268
  cur = existing.get("model") or model
@@ -282,6 +279,12 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
282
279
  else:
283
280
  model = (inp(f" Model [{existing.get('model') or model or 'required'}]: ").strip()
284
281
  or existing.get("model") or model)
282
+ existing_key = existing.get("api_key") or ""
283
+ key_prompt = (" API key (typed as ******, Enter to keep existing): " if existing_key
284
+ else " API key (typed as ******): ") if fancy else (
285
+ " API key (hidden, Enter to keep existing): " if existing_key
286
+ else " API key (hidden): ")
287
+ key = (_masked_input(key_prompt, getpw) if fancy else getpw(key_prompt)).strip() or existing_key
285
288
  except (EOFError, KeyboardInterrupt):
286
289
  out("\n cancelled."); return 1
287
290
  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)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes