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.
Files changed (214) hide show
  1. {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/workflows/release.yml +1 -1
  2. {multacd-2.0.0b8 → multacd-2.0.0b9}/CHANGELOG.md +11 -0
  3. {multacd-2.0.0b8 → multacd-2.0.0b9}/PKG-INFO +1 -1
  4. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/version.py +1 -1
  5. {multacd-2.0.0b8 → multacd-2.0.0b9}/pyproject.toml +1 -1
  6. {multacd-2.0.0b8 → multacd-2.0.0b9}/scripts/install.sh +1 -1
  7. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/test_selftests.py +6 -0
  8. multacd-2.0.0b9/tests/unit/test_tui_r1.py +88 -0
  9. multacd-2.0.0b9/tests/unit/test_tui_r2.py +24 -0
  10. multacd-2.0.0b9/tests/unit/test_tui_r3.py +53 -0
  11. multacd-2.0.0b9/tests/unit/test_tui_r4.py +39 -0
  12. multacd-2.0.0b9/tests/unit/test_tui_r5.py +35 -0
  13. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/app.py +4 -0
  14. multacd-2.0.0b9/tui/layout.py +92 -0
  15. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/screens/main_screen.py +242 -50
  16. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/chat_panel.py +48 -9
  17. multacd-2.0.0b9/tui/widgets/context_sidebar.py +67 -0
  18. multacd-2.0.0b9/tui/widgets/footer_bar.py +86 -0
  19. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/info_panel.py +31 -8
  20. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/input_bar.py +28 -0
  21. multacd-2.0.0b9/tui/widgets/provider_selector.py +151 -0
  22. multacd-2.0.0b9/tui/widgets/session_bar.py +45 -0
  23. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/slash_palette.py +112 -19
  24. {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/pull_request_template.md +0 -0
  27. {multacd-2.0.0b8 → multacd-2.0.0b9}/.github/workflows/test.yml +0 -0
  28. {multacd-2.0.0b8 → multacd-2.0.0b9}/.gitignore +0 -0
  29. {multacd-2.0.0b8 → multacd-2.0.0b9}/CONTRIBUTING.md +0 -0
  30. {multacd-2.0.0b8 → multacd-2.0.0b9}/DESIGN.md +0 -0
  31. {multacd-2.0.0b8 → multacd-2.0.0b9}/LICENSE +0 -0
  32. {multacd-2.0.0b8 → multacd-2.0.0b9}/README.md +0 -0
  33. {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/__init__.py +0 -0
  34. {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/generator.py +0 -0
  35. {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/privacy.py +0 -0
  36. {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/__init__.py +0 -0
  37. {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/git_source.py +0 -0
  38. {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/news_source.py +0 -0
  39. {multacd-2.0.0b8 → multacd-2.0.0b9}/briefing/sources/todo_source.py +0 -0
  40. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/__init__.py +0 -0
  41. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/agent_events.py +0 -0
  42. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/agent_loop.py +0 -0
  43. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/clipboard.py +0 -0
  44. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/codebase.py +0 -0
  45. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/config.py +0 -0
  46. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/llm_client.py +0 -0
  47. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/mode_manager.py +0 -0
  48. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/permissions.py +0 -0
  49. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/plugin_loader.py +0 -0
  50. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/prompt_composer.py +0 -0
  51. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/__init__.py +0 -0
  52. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/__main__.py +0 -0
  53. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/anthropic.py +0 -0
  54. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/providers/openai_compat.py +0 -0
  55. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/__init__.py +0 -0
  56. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/bus.py +0 -0
  57. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/orchestrator.py +0 -0
  58. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/research/query_generator.py +0 -0
  59. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/session_state.py +0 -0
  60. {multacd-2.0.0b8 → multacd-2.0.0b9}/core/updater.py +0 -0
  61. {multacd-2.0.0b8 → multacd-2.0.0b9}/daemon/__init__.py +0 -0
  62. {multacd-2.0.0b8 → multacd-2.0.0b9}/daemon/ipc.py +0 -0
  63. {multacd-2.0.0b8 → multacd-2.0.0b9}/daemon/process.py +0 -0
  64. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/configuration.md +0 -0
  65. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/index.md +0 -0
  66. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/install.md +0 -0
  67. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/mkdocs.yml +0 -0
  68. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/modes/coding.md +0 -0
  69. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/modes/personal.md +0 -0
  70. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/modes/research.md +0 -0
  71. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/plugins.md +0 -0
  72. {multacd-2.0.0b8 → multacd-2.0.0b9}/docs/quickstart.md +0 -0
  73. {multacd-2.0.0b8 → multacd-2.0.0b9}/main.py +0 -0
  74. {multacd-2.0.0b8 → multacd-2.0.0b9}/memory/__init__.py +0 -0
  75. {multacd-2.0.0b8 → multacd-2.0.0b9}/memory/context.py +0 -0
  76. {multacd-2.0.0b8 → multacd-2.0.0b9}/memory/store.py +0 -0
  77. {multacd-2.0.0b8 → multacd-2.0.0b9}/multacd.spec +0 -0
  78. {multacd-2.0.0b8 → multacd-2.0.0b9}/plan/PLAN-phase1.md +0 -0
  79. {multacd-2.0.0b8 → multacd-2.0.0b9}/plan/PLAN-phase2.md +0 -0
  80. {multacd-2.0.0b8 → multacd-2.0.0b9}/plugins/README.md +0 -0
  81. {multacd-2.0.0b8 → multacd-2.0.0b9}/plugins/example_plugin.py +0 -0
  82. {multacd-2.0.0b8 → multacd-2.0.0b9}/requirements.txt +0 -0
  83. {multacd-2.0.0b8 → multacd-2.0.0b9}/roadmap/ROADMAP.md +0 -0
  84. {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/__init__.py +0 -0
  85. {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/engine.py +0 -0
  86. {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/jobs/__init__.py +0 -0
  87. {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/jobs/briefing_job.py +0 -0
  88. {multacd-2.0.0b8 → multacd-2.0.0b9}/scheduler/jobs/research_job.py +0 -0
  89. {multacd-2.0.0b8 → multacd-2.0.0b9}/scripts/install.ps1 +0 -0
  90. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/__init__.py +0 -0
  91. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/__main__.py +0 -0
  92. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/base.py +0 -0
  93. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/brave.py +0 -0
  94. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/duckduckgo.py +0 -0
  95. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/exa.py +0 -0
  96. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/serpapi.py +0 -0
  97. {multacd-2.0.0b8 → multacd-2.0.0b9}/search_providers/tavily.py +0 -0
  98. {multacd-2.0.0b8 → multacd-2.0.0b9}/soul.md +0 -0
  99. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/__init__.py +0 -0
  100. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/conftest.py +0 -0
  101. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/integration/__init__.py +0 -0
  102. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/integration/test_agent_loop.py +0 -0
  103. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/__init__.py +0 -0
  104. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_agent_controller.py +0 -0
  105. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_binary.py +0 -0
  106. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_config.py +0 -0
  107. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_docs.py +0 -0
  108. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_entrypoint.py +0 -0
  109. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_github.py +0 -0
  110. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_installer.py +0 -0
  111. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_permissions.py +0 -0
  112. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_plugin_loader.py +0 -0
  113. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_privacy_filter.py +0 -0
  114. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_provider_keys.py +0 -0
  115. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_research_bus.py +0 -0
  116. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_search_providers/__init__.py +0 -0
  117. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_search_providers/test_fallback.py +0 -0
  118. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_search_providers/test_tavily.py +0 -0
  119. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_session_state.py +0 -0
  120. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_splash.py +0 -0
  121. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/__init__.py +0 -0
  122. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_bash.py +0 -0
  123. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_read_file.py +0 -0
  124. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_web_scrape.py +0 -0
  125. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_tools/test_write_file.py +0 -0
  126. {multacd-2.0.0b8 → multacd-2.0.0b9}/tests/unit/test_updater.py +0 -0
  127. {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/__init__.py +0 -0
  128. {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/access_control.py +0 -0
  129. {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/agent.py +0 -0
  130. {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/bot.py +0 -0
  131. {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/file_handler.py +0 -0
  132. {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/formatter.py +0 -0
  133. {multacd-2.0.0b8 → multacd-2.0.0b9}/tg/handlers.py +0 -0
  134. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/__init__.py +0 -0
  135. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/__init__.py +0 -0
  136. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/ask.py +0 -0
  137. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/skill.py +0 -0
  138. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/task.py +0 -0
  139. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/agent/todo_write.py +0 -0
  140. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/__init__.py +0 -0
  141. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/lint_python.py +0 -0
  142. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/run_python.py +0 -0
  143. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/code/run_tests.py +0 -0
  144. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/codebase/__init__.py +0 -0
  145. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/codebase/scan_codebase.py +0 -0
  146. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/common.py +0 -0
  147. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/__init__.py +0 -0
  148. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/apply_patch.py +0 -0
  149. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/delete_file.py +0 -0
  150. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/edit_file.py +0 -0
  151. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/glob.py +0 -0
  152. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/grep.py +0 -0
  153. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/list_dir.py +0 -0
  154. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/move_file.py +0 -0
  155. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/multi_edit.py +0 -0
  156. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/read_file.py +0 -0
  157. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/read_many_files.py +0 -0
  158. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/filesystem/write_file.py +0 -0
  159. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/__init__.py +0 -0
  160. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/_helper.py +0 -0
  161. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_add.py +0 -0
  162. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_branch.py +0 -0
  163. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_checkout.py +0 -0
  164. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_commit.py +0 -0
  165. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_diff.py +0 -0
  166. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_log.py +0 -0
  167. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_merge.py +0 -0
  168. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_pull.py +0 -0
  169. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_push.py +0 -0
  170. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/git/git_status.py +0 -0
  171. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/__init__.py +0 -0
  172. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/forget.py +0 -0
  173. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/recall.py +0 -0
  174. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/memory/remember.py +0 -0
  175. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/__init__.py +0 -0
  176. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/cancel_job.py +0 -0
  177. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/daemon_status.py +0 -0
  178. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/generate_briefing.py +0 -0
  179. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/get_jobs.py +0 -0
  180. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/schedule_job.py +0 -0
  181. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/send_telegram.py +0 -0
  182. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/personal/user_manager.py +0 -0
  183. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/registry.py +0 -0
  184. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/__init__.py +0 -0
  185. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/deep_research.py +0 -0
  186. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/export_research.py +0 -0
  187. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/quick_research.py +0 -0
  188. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/web_scrape.py +0 -0
  189. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/research/web_search.py +0 -0
  190. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/shell/__init__.py +0 -0
  191. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/shell/bash.py +0 -0
  192. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/streaming.py +0 -0
  193. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/web/__init__.py +0 -0
  194. {multacd-2.0.0b8 → multacd-2.0.0b9}/tools/web/web_fetch.py +0 -0
  195. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/__init__.py +0 -0
  196. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/controllers/__init__.py +0 -0
  197. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/controllers/agent_controller.py +0 -0
  198. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/icons.py +0 -0
  199. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/markup_safe.py +0 -0
  200. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/screens/__init__.py +0 -0
  201. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/screens/setup_wizard.py +0 -0
  202. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/themes/__init__.py +0 -0
  203. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/themes/__main__.py +0 -0
  204. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/tokens.py +0 -0
  205. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/__init__.py +0 -0
  206. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/confirm_dialog.py +0 -0
  207. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/diff_viewer.py +0 -0
  208. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/file_tree.py +0 -0
  209. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/model_selector.py +0 -0
  210. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/permission_popup.py +0 -0
  211. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/sources_panel.py +0 -0
  212. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/status_bar.py +0 -0
  213. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/thinking_bar.py +0 -0
  214. {multacd-2.0.0b8 → multacd-2.0.0b9}/tui/widgets/tool_activity.py +0 -0
@@ -101,7 +101,7 @@ jobs:
101
101
 
102
102
  ```bash
103
103
  pkg install -y python git
104
- pip install "multacd==2.0.0b8"
104
+ pip install "multacd==2.0.0b9"
105
105
  ```
106
106
 
107
107
  Windows (PowerShell):
@@ -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.0b8
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.0b8" # beta v2; samakan dengan pyproject.toml
11
+ FALLBACK = "2.0.0b9" # beta v2; samakan dengan pyproject.toml
12
12
 
13
13
 
14
14
  def get_version() -> str:
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "multacd"
11
- version = "2.0.0b8"
11
+ version = "2.0.0b9"
12
12
  description = "Agentic TUI for coding, research, and personal automation"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.10"
@@ -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.0b8\""
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)")