multacd 2.0.0b8__tar.gz → 2.0.0b9__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.
- {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/workflows/release.yml +1 -1
- {multacd-2.0.0b8 → multacd-2.0.0b9}/CHANGELOG.md +11 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/PKG-INFO +1 -1
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/version.py +1 -1
- {multacd-2.0.0b8 → multacd-2.0.0b9}/pyproject.toml +1 -1
- {multacd-2.0.0b8 → multacd-2.0.0b9}/scripts/install.sh +1 -1
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/test_selftests.py +6 -0
- multacd-2.0.0b9/tests/unit/test_tui_r1.py +88 -0
- multacd-2.0.0b9/tests/unit/test_tui_r2.py +24 -0
- multacd-2.0.0b9/tests/unit/test_tui_r3.py +53 -0
- multacd-2.0.0b9/tests/unit/test_tui_r4.py +39 -0
- multacd-2.0.0b9/tests/unit/test_tui_r5.py +35 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/app.py +4 -0
- multacd-2.0.0b9/tui/layout.py +92 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/screens/main_screen.py +242 -50
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/chat_panel.py +48 -9
- multacd-2.0.0b9/tui/widgets/context_sidebar.py +67 -0
- multacd-2.0.0b9/tui/widgets/footer_bar.py +86 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/info_panel.py +31 -8
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/input_bar.py +28 -0
- multacd-2.0.0b9/tui/widgets/provider_selector.py +151 -0
- multacd-2.0.0b9/tui/widgets/session_bar.py +45 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/slash_palette.py +112 -19
- {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/pull_request_template.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/workflows/test.yml +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/.gitignore +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/CONTRIBUTING.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/DESIGN.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/LICENSE +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/README.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/generator.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/privacy.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/git_source.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/news_source.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/todo_source.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/agent_events.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/agent_loop.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/clipboard.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/codebase.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/config.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/llm_client.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/mode_manager.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/permissions.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/plugin_loader.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/prompt_composer.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/__main__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/anthropic.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/openai_compat.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/bus.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/orchestrator.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/query_generator.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/session_state.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/core/updater.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/daemon/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/daemon/ipc.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/daemon/process.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/configuration.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/index.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/install.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/mkdocs.yml +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/modes/coding.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/modes/personal.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/modes/research.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/plugins.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/quickstart.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/main.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/memory/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/memory/context.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/memory/store.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/multacd.spec +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/plan/PLAN-phase1.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/plan/PLAN-phase2.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/plugins/README.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/plugins/example_plugin.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/requirements.txt +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/roadmap/ROADMAP.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/engine.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/jobs/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/jobs/briefing_job.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/jobs/research_job.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/scripts/install.ps1 +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/__main__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/base.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/brave.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/duckduckgo.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/exa.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/serpapi.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/tavily.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/soul.md +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/conftest.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/integration/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/integration/test_agent_loop.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_agent_controller.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_binary.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_config.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_docs.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_entrypoint.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_github.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_installer.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_permissions.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_plugin_loader.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_privacy_filter.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_provider_keys.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_research_bus.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_search_providers/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_search_providers/test_fallback.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_search_providers/test_tavily.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_session_state.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_splash.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_bash.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_read_file.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_web_scrape.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_write_file.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_updater.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/access_control.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/agent.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/bot.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/file_handler.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/formatter.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/handlers.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/ask.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/skill.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/task.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/todo_write.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/lint_python.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/run_python.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/run_tests.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/codebase/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/codebase/scan_codebase.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/common.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/apply_patch.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/delete_file.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/edit_file.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/glob.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/grep.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/list_dir.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/move_file.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/multi_edit.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/read_file.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/read_many_files.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/write_file.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/_helper.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_add.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_branch.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_checkout.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_commit.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_diff.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_log.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_merge.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_pull.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_push.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_status.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/forget.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/recall.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/remember.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/cancel_job.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/daemon_status.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/generate_briefing.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/get_jobs.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/schedule_job.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/send_telegram.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/user_manager.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/registry.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/deep_research.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/export_research.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/quick_research.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/web_scrape.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/web_search.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/shell/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/shell/bash.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/streaming.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/web/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/web/web_fetch.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/controllers/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/controllers/agent_controller.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/icons.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/markup_safe.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/screens/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/screens/setup_wizard.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/themes/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/themes/__main__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/tokens.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/__init__.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/confirm_dialog.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/diff_viewer.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/file_tree.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/model_selector.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/permission_popup.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/sources_panel.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/status_bar.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/thinking_bar.py +0 -0
- {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/tool_activity.py +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Format: Keep a Changelog. Versi: Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## [2.0.0b9] - 2026-09-13
|
|
6
|
+
|
|
7
|
+
Beta v2 kesembilan (redesign TUI, tanpa ubah agent): shell layout
|
|
8
|
+
(session bar + sidebar responsif + footer, TUI-R1), gaya pesan chat
|
|
9
|
+
(aksen user + meta jawaban, TUI-R2), palette overlay + grup (TUI-R3),
|
|
10
|
+
provider selector popup + model overlay (TUI-R4), snapshot konteks
|
|
11
|
+
jujur in/out + status (TUI-R5), input adaptif layar pendek (TUI-R6).
|
|
12
|
+
Fix: short_workdir Windows (#41). Catatan: #44 (skills), #47 (limit/MCP).
|
|
13
|
+
|
|
14
|
+
Install: `pip install "multacd==2.0.0b9"` (tanpa --pre).
|
|
15
|
+
|
|
5
16
|
## [2.0.0b8] - 2026-09-13
|
|
6
17
|
|
|
7
18
|
Beta v2 kedelapan (refactor arsitektur, tanpa ubah fitur): kontrak event
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: multacd
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0b9
|
|
4
4
|
Summary: Agentic TUI for coding, research, and personal automation
|
|
5
5
|
Project-URL: Homepage, https://github.com/prototypeall850-creator/multacd
|
|
6
6
|
Project-URL: Issues, https://github.com/prototypeall850-creator/multacd/issues
|
|
@@ -8,7 +8,7 @@ Saat bump versi (Step 10 → 1.0.0): ubah pyproject + FALLBACK di sini.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
FALLBACK = "2.0.
|
|
11
|
+
FALLBACK = "2.0.0b9" # beta v2; samakan dengan pyproject.toml
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def get_version() -> str:
|
|
@@ -78,7 +78,7 @@ termux_pip_guide() {
|
|
|
78
78
|
echo " (ringan: tanpa kompilasi Rust — dependensi wheel murni)"
|
|
79
79
|
echo ""
|
|
80
80
|
echo " 2. Install multacd:"
|
|
81
|
-
echo " pip install \"multacd==2.0.
|
|
81
|
+
echo " pip install \"multacd==2.0.0b9\""
|
|
82
82
|
echo ""
|
|
83
83
|
echo " 3. Jalankan: multacd"
|
|
84
84
|
echo ""
|
|
@@ -55,6 +55,12 @@ SELF_TEST_MODULES = [
|
|
|
55
55
|
"tui.markup_safe",
|
|
56
56
|
"tui.widgets.thinking_bar",
|
|
57
57
|
"tui.widgets.status_bar",
|
|
58
|
+
"tui.widgets.info_panel",
|
|
59
|
+
"tui.layout",
|
|
60
|
+
"tui.widgets.session_bar",
|
|
61
|
+
"tui.widgets.context_sidebar",
|
|
62
|
+
"tui.widgets.footer_bar",
|
|
63
|
+
"tui.widgets.provider_selector",
|
|
58
64
|
"tui.widgets.permission_popup",
|
|
59
65
|
"tui.widgets.tool_activity",
|
|
60
66
|
"tui.widgets.slash_palette",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Unit: shell TUI-R1 — layout breakpoint + bar/sidebar/footer (headless)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tui.layout import ShellLayout, layout_for_width
|
|
6
|
+
from tui.widgets.context_sidebar import get_mcp_servers, render_mcp
|
|
7
|
+
from tui.widgets.footer_bar import render_footer, short_workdir
|
|
8
|
+
from tui.widgets.session_bar import render_session_title
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_breakpoints_brief():
|
|
12
|
+
assert layout_for_width(160) == ShellLayout(True, "24%", False, 5)
|
|
13
|
+
assert layout_for_width(120).sidebar_width == "24%"
|
|
14
|
+
assert layout_for_width(119) == ShellLayout(True, "18%", False, 5)
|
|
15
|
+
assert layout_for_width(90).sidebar_visible is True
|
|
16
|
+
assert layout_for_width(89) == ShellLayout(False, "18%", False, 5)
|
|
17
|
+
assert layout_for_width(80).sidebar_visible is False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_manual_override_ctrl_i():
|
|
21
|
+
assert layout_for_width(160, False).sidebar_visible is False
|
|
22
|
+
assert layout_for_width(80, True) == ShellLayout(True, "45%", False, 5)
|
|
23
|
+
assert layout_for_width(160, True).sidebar_width == "24%"
|
|
24
|
+
assert layout_for_width(80, False).sidebar_visible is False
|
|
25
|
+
assert layout_for_width(80, None).sidebar_visible is False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_input_height_short_screens():
|
|
29
|
+
assert layout_for_width(120, None, 40).input_height == 5
|
|
30
|
+
assert layout_for_width(120, None, 30).input_height == 5
|
|
31
|
+
assert layout_for_width(80, None, 29).input_height == 3
|
|
32
|
+
assert layout_for_width(80, None, 24).input_height == 3
|
|
33
|
+
assert layout_for_width(80, True, 24) == ShellLayout(True, "45%", False, 3)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_footer_compact_narrow():
|
|
37
|
+
assert layout_for_width(69).footer_compact is True
|
|
38
|
+
assert layout_for_width(70).footer_compact is False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_session_title_single():
|
|
42
|
+
assert render_session_title("myapp") == "myapp"
|
|
43
|
+
assert render_session_title("") == "—"
|
|
44
|
+
assert render_session_title(" ") == "—"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_footer_render_no_fake():
|
|
48
|
+
assert "ctrl+i panel" in render_footer("~/p", "— · —", False)
|
|
49
|
+
assert "ctrl+i panel" not in render_footer("~/p", "— · —", True)
|
|
50
|
+
assert "—" in render_footer("~/p", "", False) # usage kosong = jujur
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_short_workdir():
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
home = str(Path.home())
|
|
57
|
+
assert short_workdir(home) == "~"
|
|
58
|
+
assert short_workdir(f"{home}/proj") == "~/proj"
|
|
59
|
+
assert short_workdir("") == "?"
|
|
60
|
+
chopped = short_workdir("/a/" + "x" * 60, 32)
|
|
61
|
+
assert len(chopped) == 32 and chopped.startswith("…")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_mcp_empty_honest():
|
|
65
|
+
assert get_mcp_servers() == []
|
|
66
|
+
assert render_mcp([]) == "MCP\n—"
|
|
67
|
+
s = render_mcp([{"name": "figma", "status": "Connected"}])
|
|
68
|
+
assert "● figma" in s and "Connected" in s
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_widgets_no_crash_unmounted():
|
|
72
|
+
from tui.widgets.context_sidebar import ContextSidebar
|
|
73
|
+
from tui.widgets.footer_bar import FooterBar
|
|
74
|
+
from tui.widgets.session_bar import SessionBar
|
|
75
|
+
|
|
76
|
+
bar = SessionBar.__new__(SessionBar)
|
|
77
|
+
bar._title = ""
|
|
78
|
+
SessionBar.set_title(bar, "myapp")
|
|
79
|
+
assert bar._title == "myapp"
|
|
80
|
+
|
|
81
|
+
foot = FooterBar.__new__(FooterBar)
|
|
82
|
+
foot._workdir, foot._usage, foot._compact = "?", "—", False
|
|
83
|
+
FooterBar.set_data(foot, "~/p", "1 · —")
|
|
84
|
+
FooterBar.set_compact(foot, True)
|
|
85
|
+
assert foot._compact is True
|
|
86
|
+
|
|
87
|
+
side = ContextSidebar.__new__(ContextSidebar)
|
|
88
|
+
side.set_mcp([]) # suppress: tanpa mount tak crash
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Unit: chat visual TUI-R2 — meta murni + kontrak close (headless)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tui.widgets.chat_panel import render_meta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_meta_full_with_usage():
|
|
9
|
+
m = render_meta("code", "groq/llama-3.3-70b", 4.7, 475)
|
|
10
|
+
assert m == "code · llama-3.3-70b · 4.7s · 101 tok/s", m
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_meta_no_usage_no_fake_rate():
|
|
14
|
+
m = render_meta("code", "custom/model", 3.2, 0)
|
|
15
|
+
assert m == "code · model · 3.2s", m
|
|
16
|
+
assert "tok/s" not in m
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_meta_model_short_and_mode():
|
|
20
|
+
m = render_meta("research", "x", 0.0, 0)
|
|
21
|
+
assert m.startswith("research · x · 0.0s"), m
|
|
22
|
+
long_m = render_meta("code", "prov/" + "n" * 50, 1.0, 0)
|
|
23
|
+
assert len("prov/" + "n" * 50) > 28
|
|
24
|
+
assert "n" * 50 not in long_m # dipotong 28 char
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Unit: command palette TUI-R3 — grup + overlay flag (headless)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tui.widgets.slash_palette import (
|
|
6
|
+
COMMAND_GROUPS,
|
|
7
|
+
GROUP_OF,
|
|
8
|
+
SlashPalette,
|
|
9
|
+
grouped_matches,
|
|
10
|
+
is_header,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_all_commands_grouped_once():
|
|
15
|
+
from core.mode_manager import PALETTE_COMMANDS
|
|
16
|
+
|
|
17
|
+
grouped = [c for _, cmds in COMMAND_GROUPS for c in cmds]
|
|
18
|
+
assert sorted(grouped) == sorted(c for c, _ in PALETTE_COMMANDS)
|
|
19
|
+
assert len(set(grouped)) == len(grouped) == 12
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_grouped_structure_and_ranking():
|
|
23
|
+
from core.mode_manager import PALETTE_COMMANDS
|
|
24
|
+
|
|
25
|
+
g = grouped_matches("")
|
|
26
|
+
headers = [c for c, _ in g if is_header((c, ""))]
|
|
27
|
+
assert headers == ["##Mode", "##Model & provider", "##Sesi"]
|
|
28
|
+
# Tiap grup: subset command existing, urutan ikut PALETTE_COMMANDS.
|
|
29
|
+
order = [c for c, _ in PALETTE_COMMANDS]
|
|
30
|
+
for grp, cmds in COMMAND_GROUPS:
|
|
31
|
+
rows = [c for c, _ in g if GROUP_OF.get(c) == grp]
|
|
32
|
+
assert rows == [c for c in order if c in cmds], (grp, rows)
|
|
33
|
+
assert GROUP_OF["/model"] == "Model & provider"
|
|
34
|
+
assert GROUP_OF["/clear"] == "Sesi"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_grouped_filter_hides_empty_groups():
|
|
38
|
+
g = grouped_matches("mo")
|
|
39
|
+
assert [c for c, _ in g] == ["##Model & provider", "/model", "/models"]
|
|
40
|
+
assert grouped_matches("zzz") == []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_overlay_flag_lifecycle_unmounted():
|
|
44
|
+
pal = SlashPalette()
|
|
45
|
+
assert pal.overlay_open is False
|
|
46
|
+
pal.open("", overlay=True) # tanpa mount: _rebuild suppress, class ok
|
|
47
|
+
assert pal.overlay_open is True
|
|
48
|
+
assert pal.selected_command == "/code" # header di-skip
|
|
49
|
+
pal.move(3) # /code → ... skip header, tetap command
|
|
50
|
+
assert pal.selected_command is not None
|
|
51
|
+
assert not is_header((pal._matches[pal._index][0], ""))
|
|
52
|
+
pal.close()
|
|
53
|
+
assert pal.overlay_open is False and pal._matches == []
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Unit: selectors TUI-R4 — provider rows + status jujur (headless)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tui.widgets.provider_selector import (
|
|
6
|
+
ProviderSelector,
|
|
7
|
+
build_rows,
|
|
8
|
+
render_row,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_rows_grouped_empty_hidden():
|
|
13
|
+
rows = build_rows([("groq", "Groq", True)], [("tavily", "Tavily", False)])
|
|
14
|
+
assert [r[0] for r in rows] == ["head", "llm", "head", "search"]
|
|
15
|
+
assert build_rows([], []) == []
|
|
16
|
+
assert [r[0] for r in build_rows([], [("t", "T", True)])] == ["head", "search"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_row_status_honest():
|
|
20
|
+
assert "connected" in str(render_row(("llm", "groq", "Groq", True)))
|
|
21
|
+
assert "belum" in str(render_row(("llm", "x", "X", False)))
|
|
22
|
+
assert "aktif" in str(render_row(("search", "tavily", "Tavily", True)))
|
|
23
|
+
head = render_row(("head", "Provider LLM"))
|
|
24
|
+
assert "Provider LLM" in str(head) and "●" not in str(head)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_nav_skips_headers_unmounted():
|
|
28
|
+
sel = ProviderSelector()
|
|
29
|
+
sel.open([("groq", "Groq", True), ("custom", "Custom", False)],
|
|
30
|
+
[("tavily", "Tavily", False)])
|
|
31
|
+
assert sel.is_open and sel.selected == ("llm", "groq")
|
|
32
|
+
sel.move(1)
|
|
33
|
+
assert sel.selected == ("llm", "custom")
|
|
34
|
+
sel.move(1) # lompat header search
|
|
35
|
+
assert sel.selected == ("search", "tavily")
|
|
36
|
+
sel.move(-1)
|
|
37
|
+
assert sel.selected == ("llm", "custom")
|
|
38
|
+
sel.close()
|
|
39
|
+
assert not sel.is_open and sel.selected is None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Unit: context sidebar TUI-R5 — snapshot jujur (headless)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from core.session_state import AgentStatus
|
|
6
|
+
from tui.widgets.info_panel import render_snapshot
|
|
7
|
+
from tui.widgets.status_bar import agent_status_for
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_agent_status_words():
|
|
11
|
+
assert agent_status_for(AgentStatus.IDLE) == "idle"
|
|
12
|
+
assert agent_status_for(AgentStatus.EXECUTING_TOOL) == "thinking"
|
|
13
|
+
assert agent_status_for(AgentStatus.WAITING_PERMISSION) == "waiting"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_usage_split_official_vs_unknown():
|
|
17
|
+
off = render_snapshot({"mode": "code", "project": "p", "git": "g",
|
|
18
|
+
"tokens": "1.200", "prompt": "1.000",
|
|
19
|
+
"completion": "200", "cost": "—", "messages": 1,
|
|
20
|
+
"tools": 0, "model": "m", "status": "idle"})
|
|
21
|
+
assert "In: 1.000 · Out: 200" in off and "~" not in off.split("In:")[1].split("\n")[0]
|
|
22
|
+
unk = render_snapshot({"mode": "code", "project": "p", "git": "g",
|
|
23
|
+
"tokens": "~800", "prompt": "—", "completion": "—",
|
|
24
|
+
"cost": "—", "messages": 1, "tools": 0,
|
|
25
|
+
"model": "m", "status": "idle"})
|
|
26
|
+
assert "In: — · Out: —" in unk
|
|
27
|
+
assert "Context: ~800 tokens" in unk
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_no_limit_no_percent_no_fake():
|
|
31
|
+
s = render_snapshot({"mode": "code", "project": "p", "git": "g",
|
|
32
|
+
"tokens": 0, "prompt": "—", "completion": "—",
|
|
33
|
+
"cost": "—", "messages": 0, "tools": 0,
|
|
34
|
+
"model": "m", "status": "idle"})
|
|
35
|
+
assert "%" not in s and "Limit" not in s
|
|
@@ -27,6 +27,10 @@ _PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
|
27
27
|
class MultacdApp(App[None]):
|
|
28
28
|
"""App TUI. `config` wajib valid; `llm_client` opsional (buat test)."""
|
|
29
29
|
|
|
30
|
+
# TUI-R3: Ctrl+P milik SlashPalette kita. Palette bawaan Textual
|
|
31
|
+
# (priority binding yang selalu menang) dimatikan via opt-out resmi.
|
|
32
|
+
ENABLE_COMMAND_PALETTE = False
|
|
33
|
+
|
|
30
34
|
BINDINGS = [
|
|
31
35
|
# priority=True: menang atas binding widget fokus (mis. Ctrl+C = copy di Input).
|
|
32
36
|
Binding("ctrl+c", "safe_quit", "Quit", priority=True),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shell layout TUI-R1 — breakpoint murni, headless testable.
|
|
2
|
+
|
|
3
|
+
Aturan (TUI_REDESIGN.md §6):
|
|
4
|
+
|
|
5
|
+
width >= 120 → sidebar tampil, 24%
|
|
6
|
+
width 90-119 → sidebar tampil, 18%
|
|
7
|
+
width < 90 → sidebar hidden, chat 100%
|
|
8
|
+
|
|
9
|
+
Ctrl+I = manual override (None = ikut auto). Layar sempit (<70 kolom):
|
|
10
|
+
footer dipangkas (hints pendek). Tanpa import Textual — pure logic.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
SIDEBAR_WIDE = 120 # >= ini: sidebar lega
|
|
18
|
+
SIDEBAR_MIN = 90 # < ini: sidebar hidden (auto)
|
|
19
|
+
FOOTER_COMPACT = 70 # < ini: footer hints pendek
|
|
20
|
+
INPUT_COMPACT_ROWS = 30 # < ini: input 3 baris (kasih ruang chat di HP)
|
|
21
|
+
WIDE_PCT = "24%"
|
|
22
|
+
MID_PCT = "18%"
|
|
23
|
+
MANUAL_PCT = "45%" # toggle manual di layar sempit: kasih ruang baca
|
|
24
|
+
INPUT_TALL = 5
|
|
25
|
+
INPUT_SHORT = 3
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ShellLayout:
|
|
30
|
+
sidebar_visible: bool
|
|
31
|
+
sidebar_width: str
|
|
32
|
+
footer_compact: bool
|
|
33
|
+
input_height: int = INPUT_TALL
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def layout_for_width(width: int, manual: bool | None = None,
|
|
37
|
+
height: int = 40) -> ShellLayout:
|
|
38
|
+
"""Keputusan layout dari lebar + tinggi terminal + override manual.
|
|
39
|
+
|
|
40
|
+
manual=None → ikut auto; True → paksa tampil; False → paksa hidden.
|
|
41
|
+
Tak pernah raise; width/height aneh (<=0) = layar sempit.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
w = int(width)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
w = 0
|
|
47
|
+
try:
|
|
48
|
+
h = int(height)
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
h = 0
|
|
51
|
+
auto_visible = w >= SIDEBAR_MIN
|
|
52
|
+
auto_width = WIDE_PCT if w >= SIDEBAR_WIDE else MID_PCT
|
|
53
|
+
if manual is None:
|
|
54
|
+
visible, chosen = auto_visible, auto_width
|
|
55
|
+
elif manual:
|
|
56
|
+
# Manual tampil di layar sempit: kasih 45% biar kebaca.
|
|
57
|
+
chosen = auto_width if auto_visible else MANUAL_PCT
|
|
58
|
+
visible = True
|
|
59
|
+
else:
|
|
60
|
+
visible, chosen = False, auto_width
|
|
61
|
+
return ShellLayout(
|
|
62
|
+
sidebar_visible=visible,
|
|
63
|
+
sidebar_width=chosen,
|
|
64
|
+
footer_compact=w < FOOTER_COMPACT,
|
|
65
|
+
input_height=INPUT_SHORT if 0 < h < INPUT_COMPACT_ROWS else INPUT_TALL,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
assert layout_for_width(160) == ShellLayout(True, "24%", False, 5)
|
|
71
|
+
assert layout_for_width(120) == ShellLayout(True, "24%", False, 5)
|
|
72
|
+
assert layout_for_width(119) == ShellLayout(True, "18%", False, 5)
|
|
73
|
+
assert layout_for_width(90) == ShellLayout(True, "18%", False, 5)
|
|
74
|
+
assert layout_for_width(89) == ShellLayout(False, "18%", False, 5)
|
|
75
|
+
assert layout_for_width(80) == ShellLayout(False, "18%", False, 5)
|
|
76
|
+
assert layout_for_width(60) == ShellLayout(False, "18%", True, 5)
|
|
77
|
+
assert layout_for_width(69).footer_compact is True
|
|
78
|
+
assert layout_for_width(70).footer_compact is False
|
|
79
|
+
# Manual override Ctrl+I.
|
|
80
|
+
assert layout_for_width(160, False) == ShellLayout(False, "24%", False, 5)
|
|
81
|
+
assert layout_for_width(80, True) == ShellLayout(True, "45%", False, 5)
|
|
82
|
+
assert layout_for_width(160, True) == ShellLayout(True, "24%", False, 5)
|
|
83
|
+
assert layout_for_width(80, False) == ShellLayout(False, "18%", False, 5)
|
|
84
|
+
assert layout_for_width(0) == ShellLayout(False, "18%", True, 5)
|
|
85
|
+
assert layout_for_width(-5).sidebar_visible is False
|
|
86
|
+
# TUI-R6: input pendek di layar pendek (<30 baris).
|
|
87
|
+
assert layout_for_width(120, None, 40).input_height == 5
|
|
88
|
+
assert layout_for_width(120, None, 30).input_height == 5
|
|
89
|
+
assert layout_for_width(80, None, 24).input_height == 3
|
|
90
|
+
assert layout_for_width(80, None, 29).input_height == 3
|
|
91
|
+
assert layout_for_width(80, None, 0).input_height == 5 # unknown = aman
|
|
92
|
+
print("✅ layout self-test OK (breakpoint + override + input)")
|