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