nexforge-cli 0.2.14__tar.gz → 0.2.15__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 (199) hide show
  1. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/PKG-INFO +2 -2
  2. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/README.md +1 -1
  3. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/01-/345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md +1 -1
  4. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/02-/347/225/214/351/235/242/344/270/216/345/277/253/346/215/267/351/224/256.md +20 -4
  5. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/README.md +1 -1
  6. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/pyproject.toml +1 -1
  7. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/__init__.py +1 -1
  8. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/i18n.py +20 -3
  9. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/screens/chat.py +100 -4
  10. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/screens/chat_actions.py +42 -0
  11. nexforge_cli-0.2.15/src/nexforge/ui/widgets/file_browser.py +163 -0
  12. nexforge_cli-0.2.15/src/nexforge/ui/widgets/file_menu.py +74 -0
  13. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/multiline_prompt.py +4 -1
  14. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_cross_platform.py +14 -1
  15. nexforge_cli-0.2.15/tests/test_file_browser.py +197 -0
  16. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/.gitignore +0 -0
  17. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/INSTALL.md +0 -0
  18. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/STATS_API.md +0 -0
  19. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/agent.svg +0 -0
  20. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/cost.svg +0 -0
  21. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/edit.svg +0 -0
  22. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/err.svg +0 -0
  23. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/file.svg +0 -0
  24. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/model.svg +0 -0
  25. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/ok.svg +0 -0
  26. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/read.svg +0 -0
  27. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/running.svg +0 -0
  28. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/search.svg +0 -0
  29. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/shell.svg +0 -0
  30. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/thinking.svg +0 -0
  31. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/web.svg +0 -0
  32. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/assets/icons/write.svg +0 -0
  33. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/config.example.toml +0 -0
  34. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/devlog/2026-07-07.md +0 -0
  35. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/doc//344/273/243/347/240/201/344/274/230/345/214/226/350/256/241/345/210/222.md" +0 -0
  36. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/doc//347/211/271/346/200/247/346/250/241/345/235/227/346/263/250/345/206/214/345/210/266-/345/256/236/346/226/275/350/256/241/345/210/222.md" +0 -0
  37. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/doc//351/241/271/347/233/256/346/200/273/347/233/221/345/276/252/347/216/257-/345/274/200/345/217/221/350/256/241/345/210/222.md" +0 -0
  38. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/docs/submission/README-/346/212/225/347/250/277/350/257/264/346/230/216.md" +0 -0
  39. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/docs/submission/nexforge.md +0 -0
  40. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/docs/submission/nexforge.zh-CN.md +0 -0
  41. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/03-/346/250/241/345/236/213/351/205/215/347/275/256.md" +0 -0
  42. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/04-/345/267/245/345/205/267/345/217/202/350/200/203.md" +0 -0
  43. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/05-/346/225/260/346/215/256/344/270/216/346/220/234/347/264/242.md" +0 -0
  44. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/06-/346/235/203/351/231/220/344/270/216/346/250/241/345/274/217.md" +0 -0
  45. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/07-/344/274/232/350/257/235/344/270/216/350/256/260/345/277/206.md" +0 -0
  46. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/08-/350/207/252/345/256/232/344/271/211/351/205/215/347/275/256.md" +0 -0
  47. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/09-/345/270/270/350/247/201/351/227/256/351/242/230.md" +0 -0
  48. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual/10-/346/217/222/344/273/266/345/274/200/345/217/221.md" +0 -0
  49. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/manual//351/231/204/345/275/225-/345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -0
  50. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/README.md +0 -0
  51. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/guiauto/__init__.py +0 -0
  52. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/guiauto/action.py +0 -0
  53. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/guiauto/base.py +0 -0
  54. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/guiauto/tools.py +0 -0
  55. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/guiauto/vision.py +0 -0
  56. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/guiauto/window.py +0 -0
  57. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/install_plugin.py +0 -0
  58. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/requirements.txt +0 -0
  59. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/smoke_safe.py +0 -0
  60. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/verify_final.py +0 -0
  61. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/verify_host.py +0 -0
  62. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/plugin_gui_auto/verify_plugin.py +0 -0
  63. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/scripts/publish.sh +0 -0
  64. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/scripts/smoke_provider.py +0 -0
  65. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/scripts/smoke_tui.py +0 -0
  66. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/__main__.py +0 -0
  67. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/app.py +0 -0
  68. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/cli.py +0 -0
  69. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/config.py +0 -0
  70. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/__init__.py +0 -0
  71. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/agent.py +0 -0
  72. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/context.py +0 -0
  73. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/federation.py +0 -0
  74. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/goal_manager.py +0 -0
  75. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/jobs.py +0 -0
  76. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/knowledge_base.py +0 -0
  77. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/memory_engine.py +0 -0
  78. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/model_catalog.py +0 -0
  79. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/permissions.py +0 -0
  80. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/plugin.py +0 -0
  81. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/project_mapper.py +0 -0
  82. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/provider.py +0 -0
  83. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/router.py +0 -0
  84. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/session.py +0 -0
  85. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/summarizer.py +0 -0
  86. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/system_inject.py +0 -0
  87. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/tool_manifest.py +0 -0
  88. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/types.py +0 -0
  89. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/core/update_check.py +0 -0
  90. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/__init__.py +0 -0
  91. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/auto_skills.py +0 -0
  92. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/base.py +0 -0
  93. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/federation_feature.py +0 -0
  94. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/kb_feature.py +0 -0
  95. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/map_feature.py +0 -0
  96. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/memory_feature.py +0 -0
  97. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/mode_features.py +0 -0
  98. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/plan_feature.py +0 -0
  99. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/registry.py +0 -0
  100. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/session_feature.py +0 -0
  101. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/slash_feature.py +0 -0
  102. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/stats_feature.py +0 -0
  103. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/features/tool_manifest_feature.py +0 -0
  104. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/icons.py +0 -0
  105. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/mcp/__init__.py +0 -0
  106. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/provider/__init__.py +0 -0
  107. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/provider/_retry.py +0 -0
  108. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/provider/anthropic.py +0 -0
  109. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/provider/balance.py +0 -0
  110. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/provider/deepseek.py +0 -0
  111. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/provider/openai.py +0 -0
  112. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/provider/qianwen.py +0 -0
  113. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/skills/loader.py +0 -0
  114. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/stats/__init__.py +0 -0
  115. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/stats/heartbeat.py +0 -0
  116. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/store/__init__.py +0 -0
  117. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/store/director_store.py +0 -0
  118. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/store/plan_store.py +0 -0
  119. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/store/session_store.py +0 -0
  120. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/theme.tcss +0 -0
  121. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/__init__.py +0 -0
  122. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/_paths.py +0 -0
  123. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/background.py +0 -0
  124. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/base.py +0 -0
  125. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/data_tools.py +0 -0
  126. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/edit_file.py +0 -0
  127. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/federation.py +0 -0
  128. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/fs_ops.py +0 -0
  129. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/git.py +0 -0
  130. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/image_tools.py +0 -0
  131. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/kb_tools.py +0 -0
  132. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/list_dir.py +0 -0
  133. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/multi_edit.py +0 -0
  134. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/ops_tools.py +0 -0
  135. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/read_file.py +0 -0
  136. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/registry.py +0 -0
  137. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/screenshot_tools.py +0 -0
  138. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/search.py +0 -0
  139. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/search_content.py +0 -0
  140. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/search_files.py +0 -0
  141. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/shell.py +0 -0
  142. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/web.py +0 -0
  143. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/tools/write_file.py +0 -0
  144. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/__init__.py +0 -0
  145. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/clipboard_image.py +0 -0
  146. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/screen_api.py +0 -0
  147. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/screens/__init__.py +0 -0
  148. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/screens/splash.py +0 -0
  149. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/slash_handler.py +0 -0
  150. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/__init__.py +0 -0
  151. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
  152. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/banner.py +0 -0
  153. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
  154. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/diff_modal.py +0 -0
  155. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/image_sidebar.py +0 -0
  156. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/messages.py +0 -0
  157. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/mode_warning.py +0 -0
  158. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/model_sidebar.py +0 -0
  159. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
  160. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
  161. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/sidebar.py +0 -0
  162. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/ui/widgets/statusbar.py +0 -0
  163. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/utils/__init__.py +0 -0
  164. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/utils/logging.py +0 -0
  165. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/web/__init__.py +0 -0
  166. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/web/server.py +0 -0
  167. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/src/nexforge/web/static/index.html +0 -0
  168. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/conftest.py +0 -0
  169. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_app_mouse.py +0 -0
  170. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_approval.py +0 -0
  171. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_config.py +0 -0
  172. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_director_loop.py +0 -0
  173. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_director_store.py +0 -0
  174. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_dual_panel.py +0 -0
  175. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_federation.py +0 -0
  176. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_goal_confirm.py +0 -0
  177. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_i18n.py +0 -0
  178. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_icons.py +0 -0
  179. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_lang_cmd.py +0 -0
  180. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_m2_tools.py +0 -0
  181. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_manual_consistency.py +0 -0
  182. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_model_catalog.py +0 -0
  183. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_p2_core.py +0 -0
  184. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_plugin.py +0 -0
  185. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_sanitize.py +0 -0
  186. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_screenshot_tools.py +0 -0
  187. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_session_repair.py +0 -0
  188. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_session_store.py +0 -0
  189. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_sidebar.py +0 -0
  190. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_sidebar_actions.py +0 -0
  191. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_splash_modal.py +0 -0
  192. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_statusbar.py +0 -0
  193. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_system_inject.py +0 -0
  194. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_tool_iters.py +0 -0
  195. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_tools.py +0 -0
  196. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_tui.py +0 -0
  197. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_update_check.py +0 -0
  198. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_vision_model.py +0 -0
  199. {nexforge_cli-0.2.14 → nexforge_cli-0.2.15}/tests/test_vision_tool_retention.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nexforge-cli
3
- Version: 0.2.14
3
+ Version: 0.2.15
4
4
  Summary: NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)
5
5
  Project-URL: Homepage, https://gitee.com/Lighthorn/nexforgecli
6
6
  Author: NexForge
@@ -51,7 +51,7 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  NexForgeCLI 是一个运行在终端里的智能体助手,既支持 DeepSeek/千问等多模型,也是**插件宿主**(plugins 目录即插即用)。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
53
53
 
54
- > 当前版本:**v0.2.14** · PyPI: `pip install nexforge-cli`
54
+ > 当前版本:**v0.2.15** · PyPI: `pip install nexforge-cli`
55
55
 
56
56
  ---
57
57
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  NexForgeCLI 是一个运行在终端里的智能体助手,既支持 DeepSeek/千问等多模型,也是**插件宿主**(plugins 目录即插即用)。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
10
10
 
11
- > 当前版本:**v0.2.14** · PyPI: `pip install nexforge-cli`
11
+ > 当前版本:**v0.2.15** · PyPI: `pip install nexforge-cli`
12
12
 
13
13
  ---
14
14
 
@@ -1,6 +1,6 @@
1
1
  # 安装与配置
2
2
 
3
- > NexForgeCLI v0.2.14
3
+ > NexForgeCLI v0.2.15
4
4
 
5
5
  ## 环境要求
6
6
 
@@ -9,10 +9,10 @@
9
9
  │ [用户消息] │
10
10
  │ [AI 回复 · 思考面板(可折叠) · 工具调用卡片] │
11
11
  │ │
12
- │ ── 本轮结束 · F4 复制最后回复 │
12
+ │ ── 本轮结束 · Ctrl+C 复制最后回复 │
13
13
  ├── 输入区 ────────────────────────────────────────────────┤
14
14
  │ > 请输入… │
15
- │ F1帮助 · F2模式 · F3编辑器 · F5会话 · F6模型 · F10快捷回复│
15
+ │ F1帮助 · F2模式 · F3编辑 · F4文件 · F5会话 · F6模型 · F10快捷回复│
16
16
  ├── 状态栏 ────────────────────────────────────────────────┤
17
17
  │ 🔥 全自动 · deepseek · flash · 思考:开 · 8k token · ¥0.00│
18
18
  └──────────────────────────────────────────────────────────┘
@@ -43,19 +43,35 @@
43
43
  | `F1` | 帮助 |
44
44
  | `F2` | 切换审批模式(审批/半自动/全自动) |
45
45
  | `F3` | 系统默认编辑器编辑文本 |
46
- | `F4` | 复制最后一条 AI 回复 |
46
+ | `F4` | 文件浏览器侧边栏(点击引用/右键编辑) |
47
47
  | `F5` | 会话侧边栏(切换/重命名/删除) |
48
48
  | `F6` | 模型选择侧边栏 |
49
49
  | `F7` | 切换运维模式 |
50
50
  | `F8` | 切换目标模式(总监循环) |
51
51
  | `F9` | 粘贴截图 |
52
52
  | `F10` | 激活快捷回复 |
53
- | `Ctrl+C` | 复制最后 AI 回复(备选键) |
53
+ | `Ctrl+C` | 复制最后 AI 回复 |
54
54
  | `Ctrl+L` | 清屏 |
55
55
  | `↑/↓` | 滚动对话 |
56
56
  | `PageUp/PageDown` | 上下翻页 |
57
57
  | `End` | 滚到底部 |
58
58
 
59
+ ## 文件浏览器
60
+
61
+ 按 `F4` 打开右侧文件浏览器。打开后**直接显示项目根下的文件与目录**(树形,自动隐藏 `.git`、`__pycache__`、`.nexforge` 等噪音目录):
62
+
63
+ | 操作 | 效果 |
64
+ |------|------|
65
+ | 点击文件 | 引用文件路径到输入框,可继续输入指令 |
66
+ | 点击目录 | 展开/折叠该目录 |
67
+ | 右键文件 | 弹出操作菜单(引用路径 / 打开编辑 / 复制路径 / 复制内容) |
68
+ | 右键目录 | 弹出操作菜单(引用目录路径 / 在系统文件管理器中打开 / 复制路径) |
69
+
70
+ - **引用**:把文件/目录路径(项目根内为相对路径)插入输入框,聚焦后可直接补充"分析这个文件"之类指令
71
+ - **打开编辑**:文件用系统默认程序打开(Windows 记事本/关联程序、macOS `open`、Linux `xdg-open`);目录则在系统文件管理器中打开
72
+ - **复制内容**:复制文件文本内容到剪贴板(超过 512KB 拒绝,防止卡顿)
73
+ - 键盘:F4 打开后焦点即在文件树,`↑/↓` 移动、`→` 展开、`Enter` 引用选中项、`Esc` 关闭并回输入框
74
+
59
75
  ## 斜杠命令
60
76
 
61
77
  **模型与账户**
@@ -1,4 +1,4 @@
1
- # NexForgeCLI 用户手册 · v0.2.14
1
+ # NexForgeCLI 用户手册 · v0.2.15
2
2
 
3
3
  ## 这是什么
4
4
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nexforge-cli"
7
- version = "0.2.14"
7
+ version = "0.2.15"
8
8
  description = "NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,5 +3,5 @@
3
3
  TUI(全屏交互)+ CLI(one-shot / 管道)双模式。
4
4
  """
5
5
 
6
- __version__ = "0.2.14"
6
+ __version__ = "0.2.15"
7
7
  __all__ = ["__version__"]
@@ -50,8 +50,8 @@ def t(key: str, **fmt: str) -> str:
50
50
  MESSAGES: dict[str, dict[str, str]] = {
51
51
  # ---- 底部提示行 --------------------------------------------------------
52
52
  "ui.prompt_hint": {
53
- "zh": "F1帮助 · F2模式 · F3编辑 · F4复制 · F5会话 · F6模型 · F7运维 · F8目标 · F9粘贴截图 · F10快捷回复 · Ctrl+L清屏 · Esc退出 · Enter发送",
54
- "en": "F1 help · F2 mode · F3 editor · F4 copy · F5 sessions · F6 models · F7 ops · F8 goal · F9 paste image · F10 quick reply · Ctrl+L clear · Esc quit · Enter send",
53
+ "zh": "F1帮助 · F2模式 · F3编辑 · F4文件 · F5会话 · F6模型 · F7运维 · F8目标 · F9粘贴截图 · F10快捷回复 · Ctrl+L清屏 · Esc退出 · Enter发送",
54
+ "en": "F1 help · F2 mode · F3 editor · F4 files · F5 sessions · F6 models · F7 ops · F8 goal · F9 paste image · F10 quick reply · Ctrl+L clear · Esc quit · Enter send",
55
55
  },
56
56
  "ui.banner_sub": {
57
57
  "zh": "多模型终端智能体",
@@ -178,6 +178,14 @@ MESSAGES: dict[str, dict[str, str]] = {
178
178
  "note.lang_switched": {"zh": "界面语言已切换", "en": "UI language switched"},
179
179
  "note.copied_last": {"zh": "已复制最后一条 AI 回复。", "en": "Copied the last AI reply."},
180
180
  "note.no_reply": {"zh": "暂无 AI 回复。", "en": "No AI reply yet."},
181
+ "note.file_referenced": {"zh": "已引用 {path}", "en": "Referenced {path}"},
182
+ "note.file_opened": {"zh": "已打开 {path}", "en": "Opened {path}"},
183
+ "note.file_open_fail": {"zh": "打开失败: {exc}", "en": "Open failed: {exc}"},
184
+ "note.file_path_copied": {"zh": "路径已复制", "en": "Path copied"},
185
+ "note.file_content_copied": {"zh": "内容已复制", "en": "Content copied"},
186
+ "note.file_too_large": {"zh": "文件过大(>512KB), 未复制", "en": "File too large (>512KB), not copied"},
187
+ "note.file_read_fail": {"zh": "读取失败: {exc}", "en": "Read failed: {exc}"},
188
+ "note.copy_fail": {"zh": "复制失败(剪贴板不可用)", "en": "Copy failed (clipboard unavailable)"},
181
189
  "note.qr_not_set": {"zh": "未设置快捷回复。请先 /qr <文本>", "en": "Quick reply not set. Use /qr <text> first"},
182
190
  "note.qr_activated": {"zh": "快捷回复已激活({n}次): {txt}", "en": "Quick reply activated ({n}x): {txt}"},
183
191
  "note.director_stopped": {
@@ -187,7 +195,7 @@ MESSAGES: dict[str, dict[str, str]] = {
187
195
  "note.task_stopped": {"zh": "⏹ 已停止当前 AI 任务。", "en": "⏹ Current AI task stopped."},
188
196
  "note.turn_timeout": {"zh": "⏱ 单轮执行超时({n}秒),已中断。可调整 config.toml 的 turn_timeout。", "en": "⏱ Turn timeout ({n}s) reached, interrupted. Adjust turn_timeout in config.toml."},
189
197
  "note.output_truncated": {"zh": "✂️ 输出达到长度上限被截断, 输入\"继续\"可续写 (可调大 config.toml 的 max_output_tokens)", "en": "✂️ Output truncated by length limit. Type \"continue\" to resume (raise max_output_tokens in config.toml)."},
190
- "note.turn_done": {"zh": "── 本轮结束 · F4 复制最后回复", "en": "── Turn done · F4 to copy last reply"},
198
+ "note.turn_done": {"zh": "── 本轮结束 · Ctrl+C 复制最后回复", "en": "── Turn done · Ctrl+C to copy last reply"},
191
199
  "note.quick_reply_remaining": {"zh": "📨 快捷回复({n}次剩余): {txt}", "en": "📨 Quick reply ({n} left): {txt}"},
192
200
  "note.verifying_provider": {"zh": "正在验证 {name}...", "en": "Verifying {name}..."},
193
201
  "note.verifying_all": {"zh": "正在验证所有 provider...", "en": "Verifying all providers..."},
@@ -316,6 +324,15 @@ MESSAGES: dict[str, dict[str, str]] = {
316
324
  "img.clear": {"zh": "✖ 清空全部", "en": "✖ Clear all"},
317
325
  "img.empty": {"zh": "(无截图)", "en": "(no images)"},
318
326
 
327
+ # ---- 文件浏览器 ---------------------------------------------------------
328
+ "filebrowser.title": {"zh": "📂 文件浏览", "en": "📂 Files"},
329
+ "filebrowser.hint": {"zh": "点击文件=引用 · 右键=操作 · 点目录=展开", "en": "Click file=reference · Right-click=actions · Click dir=expand"},
330
+ "filebrowser.menu.title": {"zh": "文件操作", "en": "File actions"},
331
+ "filebrowser.menu.ref": {"zh": "引用路径到输入框", "en": "Reference path to prompt"},
332
+ "filebrowser.menu.edit": {"zh": "打开(系统程序)", "en": "Open with system app"},
333
+ "filebrowser.menu.copy_path": {"zh": "复制路径", "en": "Copy path"},
334
+ "filebrowser.menu.copy_content": {"zh": "复制文件内容", "en": "Copy file content"},
335
+
319
336
  # ---- 消息组件 ---------------------------------------------------------
320
337
  "msg.director_review": {"zh": "◆ 项目总监 · 审核意见", "en": "◆ Director · Review"},
321
338
  "msg.director_label": {"zh": "◆ 项目总监: ", "en": "◆ Director: "},
@@ -9,11 +9,11 @@ from __future__ import annotations
9
9
  import asyncio
10
10
  from pathlib import Path
11
11
 
12
- from textual import work
12
+ from textual import on, work
13
13
  from textual.app import ComposeResult
14
14
  from textual.containers import Horizontal, Vertical, VerticalScroll
15
15
  from textual.screen import Screen
16
- from textual.widgets import Button, Label, Static
16
+ from textual.widgets import Button, DirectoryTree, Label, Static
17
17
  from nexforge.ui.widgets.multiline_prompt import MultilinePrompt
18
18
 
19
19
  from nexforge.config import NexForgeConfig
@@ -40,6 +40,8 @@ from nexforge.ui.widgets.sidebar import Sidebar
40
40
  from nexforge.ui.widgets.plan_sidebar import PlanSidebar
41
41
  from nexforge.ui.widgets.model_sidebar import ModelSidebar
42
42
  from nexforge.ui.widgets.image_sidebar import ImageSidebar
43
+ from nexforge.ui.widgets.file_browser import FileBrowser, FileBrowserTree
44
+ from nexforge.ui.widgets.file_menu import FileMenuModal
43
45
  from nexforge.skills.loader import Skill, discover_skills, skill_index_str
44
46
  from nexforge.store.session_store import _next_id, list_sessions, save_session
45
47
  import nexforge.store.plan_store as plan_store
@@ -58,8 +60,8 @@ class ChatScreen(ChatActionsMixin, Screen):
58
60
  ("f10", "quick_reply", "快捷回复"),
59
61
  ("f1", "show_help", "帮助"),
60
62
  ("f3", "external_edit", "编辑器"),
61
- ("f4", "copy_last", "复制"),
62
- ("ctrl+c", "copy_last", "复制备选"),
63
+ ("f4", "toggle_file_browser", "文件"),
64
+ ("ctrl+c", "copy_last", "复制"),
63
65
  ("pageup", "scroll_up", "上滚"),
64
66
  ("pagedown", "scroll_down", "下滚"),
65
67
  ("up", "line_up", "上一行"),
@@ -82,6 +84,7 @@ class ChatScreen(ChatActionsMixin, Screen):
82
84
  self._plan_sidebar: PlanSidebar | None = None
83
85
  self._model_sidebar: ModelSidebar | None = None
84
86
  self._image_sidebar: ImageSidebar | None = None
87
+ self._file_browser: FileBrowser | None = None
85
88
  self._balance: str = ""
86
89
  self._session_id: str = ""
87
90
  self._session_name: str = ""
@@ -139,10 +142,13 @@ class ChatScreen(ChatActionsMixin, Screen):
139
142
  yield self._model_sidebar
140
143
  self._image_sidebar = ImageSidebar(id="image-sidebar")
141
144
  yield self._image_sidebar
145
+ self._file_browser = FileBrowser(id="file-browser")
146
+ yield self._file_browser
142
147
  self._sidebar.display = False # 初始隐藏
143
148
  self._plan_sidebar.display = False
144
149
  self._model_sidebar.display = False
145
150
  self._image_sidebar.display = False
151
+ self._file_browser.display = False
146
152
  self._set_dev_log_visible(False) # 上栏工作区初始隐藏(非目标模式单栏)
147
153
 
148
154
  async def on_unmount(self) -> None:
@@ -1082,6 +1088,96 @@ class ChatScreen(ChatActionsMixin, Screen):
1082
1088
  self._image_sidebar.clear_all()
1083
1089
  self._image_sidebar.display = False
1084
1090
 
1091
+ # ---- 文件浏览器 -------------------------------------------------------
1092
+
1093
+ @on(FileBrowserTree.FileContextMenu)
1094
+ def _on_file_context_menu(self, event: FileBrowserTree.FileContextMenu) -> None:
1095
+ """右键文件/目录 → 弹操作菜单。"""
1096
+ path = event.path
1097
+ try:
1098
+ is_dir = path.is_dir()
1099
+ except OSError:
1100
+ is_dir = False
1101
+ self.app.push_screen(
1102
+ FileMenuModal(str(path), is_dir=is_dir),
1103
+ callback=lambda action: self._handle_file_action(action, path),
1104
+ )
1105
+
1106
+ @on(DirectoryTree.FileSelected)
1107
+ def _on_file_selected(self, event: DirectoryTree.FileSelected) -> None:
1108
+ """左键文件 → 引用路径到输入框。"""
1109
+ self._reference_file(event.path)
1110
+
1111
+ def _handle_file_action(self, action: str | None, path: Path) -> None:
1112
+ if action is None:
1113
+ return
1114
+ if action == "ref":
1115
+ self._reference_file(path)
1116
+ elif action == "edit":
1117
+ self._open_file(path)
1118
+ elif action == "copy_path":
1119
+ self._copy_text(str(path), t("note.file_path_copied"))
1120
+ elif action == "copy_content":
1121
+ self._copy_file_content(path)
1122
+
1123
+ def _reference_file(self, path: Path) -> None:
1124
+ """把文件路径引用到输入框末尾(项目根内用相对路径,否则绝对路径)。
1125
+
1126
+ 注意: 用 document.end 指定插入位置 —— TextArea 的 text setter / 光标
1127
+ 可能在文档开头, 直接 insert 会把引用插到最前面; 快速引用应追加到末尾。
1128
+ """
1129
+ display = self._rel_path(path)
1130
+ try:
1131
+ prompt = self.query_one("#prompt", MultilinePrompt)
1132
+ sep = "" if not prompt.text or prompt.text.endswith(" ") else " "
1133
+ prompt.insert(sep + display, prompt.document.end)
1134
+ prompt.focus()
1135
+ except Exception:
1136
+ pass
1137
+ self._note(t("note.file_referenced", path=display), scroll=True)
1138
+
1139
+ def _open_file(self, path: Path) -> None:
1140
+ """用系统默认程序打开文件(编辑/查看)。"""
1141
+ import os
1142
+ import subprocess
1143
+ import sys
1144
+ try:
1145
+ if sys.platform == "win32":
1146
+ os.startfile(str(path)) # type: ignore[attr-defined]
1147
+ elif sys.platform == "darwin":
1148
+ subprocess.run(["open", str(path)], check=False)
1149
+ else:
1150
+ subprocess.run(["xdg-open", str(path)], check=False)
1151
+ self._note(t("note.file_opened", path=path.name), scroll=True)
1152
+ except Exception as exc:
1153
+ self._note(t("note.file_open_fail", exc=str(exc)), error=True)
1154
+
1155
+ def _copy_text(self, text: str, ok_note: str) -> None:
1156
+ import pyperclip
1157
+ try:
1158
+ pyperclip.copy(text)
1159
+ self._note(ok_note, scroll=True)
1160
+ except Exception:
1161
+ self._note(t("note.copy_fail"), error=True)
1162
+
1163
+ def _copy_file_content(self, path: Path) -> None:
1164
+ try:
1165
+ if path.stat().st_size > 512 * 1024:
1166
+ self._note(t("note.file_too_large"), error=True)
1167
+ return
1168
+ content = path.read_text(encoding="utf-8", errors="replace")
1169
+ self._copy_text(content, t("note.file_content_copied"))
1170
+ except Exception as exc:
1171
+ self._note(t("note.file_read_fail", exc=str(exc)), error=True)
1172
+
1173
+ def _rel_path(self, path: Path) -> str:
1174
+ from nexforge.core.context import get_project_root
1175
+ try:
1176
+ rel = path.resolve().relative_to(get_project_root().resolve())
1177
+ return str(rel).replace("\\", "/")
1178
+ except (ValueError, OSError):
1179
+ return str(path.resolve()).replace("\\", "/")
1180
+
1085
1181
  # ---- 计划文件操作 ---------------------------------------------------
1086
1182
  # ---- action_* methods moved to ChatActionsMixin (chat_actions.py) ----
1087
1183
 
@@ -73,6 +73,48 @@ class ChatActionsMixin:
73
73
  self._force_reflow()
74
74
  self._scroll_to_bottom()
75
75
 
76
+ def action_toggle_file_browser(self) -> None:
77
+ self: ChatScreen
78
+ fb = self._file_browser
79
+ if not fb:
80
+ return
81
+ opening = not fb.display
82
+ fb.display = opening
83
+ if opening:
84
+ fb.refresh_tree()
85
+ # 打开 → 焦点移到文件树: 键盘立即可用(方向键浏览 / Enter 引用)。
86
+ # 延迟到 reflow 完成后 widget 可见才可 focus。
87
+ self.call_later(self._focus_file_tree)
88
+ else:
89
+ self._focus_prompt()
90
+ self._force_reflow()
91
+ self._scroll_to_bottom()
92
+
93
+ def action_close_file_browser(self) -> None:
94
+ """关闭文件浏览器并回输入框(树的 Esc 绑定调用)。"""
95
+ self: ChatScreen
96
+ if self._file_browser and self._file_browser.display:
97
+ self._file_browser.display = False
98
+ self._force_reflow()
99
+ self._focus_prompt()
100
+
101
+ def _focus_file_tree(self) -> None:
102
+ self: ChatScreen
103
+ try:
104
+ if self._file_browser and self._file_browser.display:
105
+ tree = self._file_browser.tree
106
+ if tree is not None:
107
+ tree.focus()
108
+ except Exception:
109
+ pass
110
+
111
+ def _focus_prompt(self) -> None:
112
+ self: ChatScreen
113
+ try:
114
+ self.query_one("#prompt").focus()
115
+ except Exception:
116
+ pass
117
+
76
118
  def action_toggle_ops_mode(self) -> None:
77
119
  self: ChatScreen
78
120
  feat = self._try_feature("ops_mode")
@@ -0,0 +1,163 @@
1
+ """文件浏览器侧边栏 —— 右侧面板,树形展示项目文件。
2
+
3
+ - 左键点击文件 → 发出 FileSelected(引用路径到输入框)
4
+ - 左键点击目录 → 展开/折叠
5
+ - 右键点击文件 → 发出 FileContextMenu(弹出操作菜单:引用/编辑/复制)
6
+ - 自动过滤 .git / __pycache__ / .nexforge 等噪音目录
7
+
8
+ F4 开关;默认隐藏。事件冒泡到 ChatScreen 处理。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ from textual import events
16
+ from textual.app import ComposeResult
17
+ from textual.containers import Vertical
18
+ from textual.message import Message
19
+ from textual.widgets import DirectoryTree, Label
20
+
21
+ from nexforge.i18n import t
22
+
23
+ # 目录树默认隐藏的噪音目录(不进入树, 文件仍正常显示)
24
+ _NOISY_DIRS = {
25
+ ".git", "__pycache__", ".nexforge", "node_modules", ".venv", "venv",
26
+ "dist", "build", ".pytest_cache", ".mypy_cache", ".ruff_cache",
27
+ ".idea", ".vscode", ".eggs",
28
+ }
29
+
30
+
31
+ def _is_noisy_dir(name: str) -> bool:
32
+ """目录名是否噪音(精确名或 *.egg-info 后缀)。"""
33
+ return name in _NOISY_DIRS or name.endswith(".egg-info")
34
+
35
+
36
+ class FileBrowserTree(DirectoryTree):
37
+ """文件树:在 DirectoryTree 基础上区分左右键 + 过滤噪音目录。"""
38
+
39
+ # Esc: 关闭文件浏览器并回输入框(树获得焦点时 Esc 不应冒泡触发退出确认)
40
+ BINDINGS = [
41
+ ("escape", "close_browser", ""),
42
+ ]
43
+
44
+ class FileContextMenu(Message):
45
+ """右键点击文件时发出,携带文件路径。"""
46
+
47
+ def __init__(self, path: Path) -> None:
48
+ super().__init__()
49
+ self.path: Path = path
50
+
51
+ def action_close_browser(self) -> None:
52
+ try:
53
+ self.screen.action_close_file_browser()
54
+ except Exception:
55
+ pass
56
+
57
+ def filter_paths(self, paths):
58
+ """过滤掉噪音目录,文件与正常目录保留。"""
59
+ for p in paths:
60
+ try:
61
+ if p.is_dir() and _is_noisy_dir(p.name):
62
+ continue
63
+ except OSError:
64
+ pass
65
+ yield p
66
+
67
+ async def _on_click(self, event: events.Click) -> None:
68
+ """右键:文件/目录都弹操作菜单(引用/打开/复制), 不触发默认选中/展开;
69
+ 左键走默认逻辑(文件→引用, 目录→展开)。"""
70
+ if event.button == 3: # 3 = 右键
71
+ meta = event.style.meta
72
+ if "line" in meta:
73
+ node = self.get_node_at_line(meta["line"])
74
+ if node is not None and node.data is not None:
75
+ self.post_message(self.FileContextMenu(node.data.path))
76
+ event.stop()
77
+ event.prevent_default()
78
+ return
79
+ await super()._on_click(event)
80
+
81
+
82
+ class FileBrowser(Vertical):
83
+ """文件浏览器侧边栏容器。"""
84
+
85
+ DEFAULT_CSS = """
86
+ FileBrowser {
87
+ width: 42;
88
+ height: 100%;
89
+ border-left: outer #22D3EE;
90
+ background: #12121a;
91
+ padding: 0 1 1 1;
92
+ }
93
+ #fb-title {
94
+ color: #22D3EE;
95
+ text-style: bold;
96
+ height: auto;
97
+ margin-top: 1;
98
+ }
99
+ #fb-path {
100
+ color: $text-muted;
101
+ height: auto;
102
+ margin-bottom: 1;
103
+ }
104
+ #fb-tree {
105
+ height: 1fr;
106
+ scrollbar-size-vertical: 1;
107
+ scrollbar-color: #22D3EE 30%;
108
+ }
109
+ #fb-hint {
110
+ color: $text-muted;
111
+ height: auto;
112
+ margin-top: 1;
113
+ }
114
+ """
115
+
116
+ def __init__(self, root: str | Path | None = None, *args, **kwargs) -> None:
117
+ super().__init__(*args, **kwargs)
118
+ if root is None:
119
+ try:
120
+ from nexforge.core.context import get_project_root
121
+ root = get_project_root()
122
+ except Exception:
123
+ root = Path.cwd()
124
+ self._root = Path(root)
125
+ self.display = False
126
+
127
+ def compose(self) -> ComposeResult:
128
+ yield Label(t("filebrowser.title"), id="fb-title")
129
+ yield Label(str(self._root), id="fb-path")
130
+ tree = FileBrowserTree(self._root, id="fb-tree")
131
+ # 隐藏根节点行: 树直接从"根目录之下"开始显示第一层文件/目录,
132
+ # 避免打开后只看到孤零零一个根目录行、还要再点一次才看到内容。
133
+ tree.show_root = False
134
+ yield tree
135
+ yield Label(t("filebrowser.hint"), id="fb-hint")
136
+
137
+ @property
138
+ def tree(self) -> FileBrowserTree | None:
139
+ try:
140
+ return self.query_one("#fb-tree", FileBrowserTree)
141
+ except Exception:
142
+ return None
143
+
144
+ def set_root(self, root: str | Path) -> None:
145
+ """切换根目录并刷新树。
146
+
147
+ 只设 path 即可 —— DirectoryTree 的 watch_path 会异步重建 + reload,
148
+ 不要手动再调 reload(),避免双重加载竞态。
149
+ """
150
+ self._root = Path(root)
151
+ try:
152
+ self.query_one("#fb-path", Label).update(str(self._root))
153
+ except Exception:
154
+ pass
155
+ tree = self.tree
156
+ if tree is not None and tree.is_attached:
157
+ tree.path = self._root
158
+
159
+ def refresh_tree(self) -> None:
160
+ """刷新当前根目录内容。"""
161
+ tree = self.tree
162
+ if tree is not None and tree.is_attached:
163
+ tree.reload()
@@ -0,0 +1,74 @@
1
+ """文件操作菜单 —— 右键点击文件/目录时弹出的操作列表。
2
+
3
+ 返回操作 key(字符串): "ref" / "edit" / "copy_path" / "copy_content"。
4
+ 目录不显示 "copy_content"(复制内容无意义, 打开=系统文件管理器)。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from textual.app import ComposeResult
10
+ from textual.containers import Vertical
11
+ from textual.screen import ModalScreen
12
+ from textual.widgets import Label, OptionList
13
+ from textual.widgets.option_list import Option
14
+
15
+ from nexforge.i18n import t
16
+
17
+
18
+ class FileMenuModal(ModalScreen[str]):
19
+ DEFAULT_CSS = """
20
+ FileMenuModal {
21
+ align: center middle;
22
+ }
23
+ FileMenuModal > Vertical {
24
+ width: 44;
25
+ background: #1a1a24;
26
+ border: round #22D3EE;
27
+ padding: 1 2;
28
+ }
29
+ #fm-title {
30
+ color: #22D3EE;
31
+ text-style: bold;
32
+ height: auto;
33
+ margin-bottom: 1;
34
+ }
35
+ #fm-name {
36
+ color: $text-muted;
37
+ height: auto;
38
+ margin-bottom: 1;
39
+ }
40
+ #fm-options {
41
+ height: auto;
42
+ }
43
+ """
44
+
45
+ def __init__(self, path: str, is_dir: bool = False) -> None:
46
+ super().__init__()
47
+ self._path = path
48
+ self._is_dir = is_dir
49
+
50
+ def compose(self) -> ComposeResult:
51
+ options = [
52
+ Option(t("filebrowser.menu.ref"), id="ref"),
53
+ Option(t("filebrowser.menu.edit"), id="edit"),
54
+ Option(t("filebrowser.menu.copy_path"), id="copy_path"),
55
+ ]
56
+ if not self._is_dir:
57
+ options.append(Option(t("filebrowser.menu.copy_content"), id="copy_content"))
58
+ with Vertical():
59
+ yield Label(t("filebrowser.menu.title"), id="fm-title")
60
+ yield Label(self._path, id="fm-name")
61
+ yield OptionList(*options, id="fm-options")
62
+
63
+ def on_mount(self) -> None:
64
+ try:
65
+ self.query_one("#fm-options", OptionList).focus()
66
+ except Exception:
67
+ pass
68
+
69
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
70
+ self.dismiss(event.option.id)
71
+
72
+ def on_key(self, event) -> None:
73
+ if event.key == "escape":
74
+ self.dismiss(None)
@@ -15,7 +15,7 @@ class MultilinePrompt(TextArea):
15
15
  ("f1", "forward_help", ""),
16
16
  ("f2", "forward_toggle_mode", ""),
17
17
  ("f3", "forward_external_edit", ""),
18
- ("f4", "forward_copy_last", ""),
18
+ ("f4", "forward_file_browser", ""),
19
19
  ("f5", "forward_toggle_sidebar", ""),
20
20
  ("f6", "forward_model_sidebar", ""),
21
21
  ("f7", "forward_ops", ""),
@@ -41,6 +41,9 @@ class MultilinePrompt(TextArea):
41
41
  def action_forward_copy_last(self) -> None:
42
42
  self.screen.action_copy_last()
43
43
 
44
+ def action_forward_file_browser(self) -> None:
45
+ self.screen.action_toggle_file_browser()
46
+
44
47
  def action_forward_toggle_sidebar(self) -> None:
45
48
  self.screen.action_toggle_sidebar()
46
49
 
@@ -45,6 +45,8 @@ MODULES = [
45
45
  "nexforge.ui.slash_handler",
46
46
  "nexforge.ui.screen_api",
47
47
  "nexforge.ui.clipboard_image",
48
+ "nexforge.ui.widgets.file_browser",
49
+ "nexforge.ui.widgets.file_menu",
48
50
  "nexforge.web.server",
49
51
  "nexforge.mcp",
50
52
  "nexforge.features.registry",
@@ -56,10 +58,21 @@ MODULES = [
56
58
 
57
59
  @pytest.mark.parametrize("mod", MODULES)
58
60
  def test_import_without_windll(mod):
59
- """无 windll 环境下所有模块必须可导入(子进程隔离验证, 不污染当前测试进程)。"""
61
+ """无 windll 环境下所有模块必须可导入(子进程隔离验证, 不污染当前测试进程)。
62
+
63
+ 仅在真实 POSIX(Linux/macOS)上完整有效: 那里 ctypes 天然无 windll 属性,
64
+ click/platformdirs 等第三方库按 sys.platform 走 posix 分支, 不碰 windll。
65
+ Windows 上删 windll 无法真实模拟 Linux(click 走 win32 分支模块级访问
66
+ windll.kernel32; 改 sys.platform 又会让 platformdirs 尝试 os.getuid 而
67
+ Windows os 模块没有它), 故 provider 模块(import 链含 click)在 win32 跳过,
68
+ 其 Linux 正确性由 test_linux_full_deploy_chain 覆盖。
69
+ """
60
70
  import subprocess
61
71
  import textwrap
62
72
 
73
+ if sys.platform == "win32" and mod.startswith("nexforge.provider."):
74
+ pytest.skip("provider import 链含 click(win32 分支硬依赖 windll), 仅真实 POSIX 可验证")
75
+
63
76
  code = textwrap.dedent(f"""
64
77
  import ctypes, sys
65
78
  if hasattr(ctypes, "windll"):