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.
- {sliceagent-0.1.2 → sliceagent-0.1.4}/CHANGELOG.md +27 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/PKG-INFO +3 -4
- {sliceagent-0.1.2 → sliceagent-0.1.4}/QUICKSTART.md +6 -4
- {sliceagent-0.1.2 → sliceagent-0.1.4}/README.md +2 -3
- {sliceagent-0.1.2 → sliceagent-0.1.4}/install.sh +2 -3
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/__init__.py +1 -1
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/cli.py +2 -1
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/llm.py +18 -1
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/onboarding.py +92 -11
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/tui.py +96 -0
- sliceagent-0.1.4/tests/test_menu_select.py +109 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_onboarding.py +7 -7
- sliceagent-0.1.4/tests/test_provider_lineup.py +95 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.dockerignore +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.env.example +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/dependabot.yml +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/workflows/ci.yml +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.github/workflows/publish.yml +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/.gitignore +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/CODE_OF_CONDUCT.md +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/CONTRIBUTING.md +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/Dockerfile +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/LICENSE +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/NOTICE +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/SECURITY.md +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/examples/plugins/hello/__init__.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/examples/plugins/hello/plugin.toml +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/pyproject.toml +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/scripts/gen_config_reference.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/scripts/run_tests.sh +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/sliceagent.toml.example +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/__main__.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/access.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/agents.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/background_review.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/binsniff.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/clock.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/code_grep.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/code_index.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/config.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/context_overflow.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/envspec.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/errors.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/events.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/finding_types.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/flags.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/fuzzy.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/guardrails.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/guidance.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/hippocampus.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/hooks.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/interfaces.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/loop.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/mcp_client.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/mcp_security.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/memory.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/metrics.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/model_catalog.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/monitor.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/neocortex.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/oracle.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/pagetable.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/pfc.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/plugins.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/policy.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/procman.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/prompt.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/records.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/recovery.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/regions.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/registry.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/retriever.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/safety.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/sandbox.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/scheduler.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/search_index.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/seed.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/sensory_cortex.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/session.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/skill_provenance.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/skill_usage.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/skills.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/subagent.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/subdir_hints.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/swap.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/taskstate.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/telemetry.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/terminal.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/text_utils.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/tool_summary.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/tools.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/src/sliceagent/web.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/README.md +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_active_focus.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_agents.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_ask_echo.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_ask_user.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_background_review.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_binary_view.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bound_is_relevance.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_breadth_e2e.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_2b.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_breadth_wave.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_current_request.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_linenum.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_loop_wave.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_memory_wave.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_p0a.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_p0b.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_p0c.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_selfreview.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_selfreview_siblings.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_skills_h1.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_slice_wave.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_subagent_wave.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bugfix_tools_wave.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_bughunt_fixes.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_cache_manifest.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_checkpoint.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_cli_smoke.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_closure.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_code_grep.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_code_index.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_consolidate.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_context_overflow.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_conversation.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_coresidency.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_echo_before_blocking.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_episode.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_errors_backoff.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_esc_sentinel.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_exec_env.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_finding_types.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_flags.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_fuzzy.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_ghost_index.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_guardrails.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_guidance.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_history.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_invariant_fixes.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_live_composer.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_llm_streaming.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_llm_usage_cache.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_llm_watchdog.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_log_redaction.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_loop_overflow.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mcp_output_cap.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mcp_runtime.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_memory.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_memory_persist.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_metrics.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mining.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_mission_tier.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_model_catalog.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_monitor.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_pageout_compaction.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_permission_patterns.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_pinned_composer.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_plan_tier.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_procman.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_product_features.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_progress_status.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_prompt_ab.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_provenance.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_proxy_interrupt.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_reach.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_readonly_subagent.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_recall_search.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_records.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_refault.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_registry_validation.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_reliability.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_repo_map_bounded.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_requirements.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_review_fixes.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_route_lexical.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_safety.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_seal_markdown.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_search_index.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_skill_meta.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_skill_metadata.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_subdir_hints.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_task_agnostic.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_task_state_roundtrip.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_telemetry_convergence.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_terminal.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tool_dedup.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tool_result_ok.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tools_robust.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_topic_switch.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_trajectory.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui_menus.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui_render.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_tui_widgets.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_turn_budget.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_verification_agent.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_web.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_workspace.py +0 -0
- {sliceagent-0.1.2 → sliceagent-0.1.4}/tests/test_world_region.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
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
|
|
18
|
+
sliceagent
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`~/.sliceagent/config.toml` (mode `0600` —
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -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("
|
|
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("
|
|
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": ("
|
|
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": ("
|
|
20
|
-
"4": ("
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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 (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("", "
|
|
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("
|
|
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"))
|