netizen-cli 0.12.2__tar.gz → 0.13.0__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 (308) hide show
  1. {netizen_cli-0.12.2/netizen_cli.egg-info → netizen_cli-0.13.0}/PKG-INFO +1 -1
  2. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/README.md +8 -3
  3. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/config.example.yaml +8 -1
  4. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/__init__.py +1 -1
  5. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/port_config.py +22 -0
  6. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/admin.css +8 -1
  7. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/admin.js +91 -33
  8. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/index.html +30 -0
  9. netizen_cli-0.13.0/netizen_cli/admin/static/worktrees.js +227 -0
  10. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/web.py +163 -6
  11. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/bindings.py +104 -6
  12. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/controls.py +213 -29
  13. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel_app.py +159 -64
  14. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/codex_runtime.py +291 -21
  15. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/database_migrations.py +3 -3
  16. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/domain.py +3 -0
  17. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/main.py +10 -1
  18. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/management/service.py +95 -1
  19. netizen_cli-0.13.0/netizen_cli/migrations/v15.py +110 -0
  20. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/runtime/contracts.py +3 -0
  21. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/mcp.py +7 -5
  22. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/policy.py +7 -13
  23. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/presentation.py +1 -1
  24. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/schema.py +23 -2
  25. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/service.py +63 -8
  26. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/settings.py +7 -0
  27. netizen_cli-0.13.0/netizen_cli/worktrees/errors.py +11 -0
  28. netizen_cli-0.13.0/netizen_cli/worktrees/git.py +569 -0
  29. netizen_cli-0.13.0/netizen_cli/worktrees/models.py +56 -0
  30. netizen_cli-0.13.0/netizen_cli/worktrees/service.py +516 -0
  31. netizen_cli-0.13.0/netizen_cli/worktrees/settings.py +93 -0
  32. netizen_cli-0.13.0/netizen_cli/worktrees/store.py +371 -0
  33. {netizen_cli-0.12.2 → netizen_cli-0.13.0/netizen_cli.egg-info}/PKG-INFO +1 -1
  34. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli.egg-info/SOURCES.txt +17 -1
  35. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/pyproject.toml +1 -1
  36. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/admin.md +36 -4
  37. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/commands.md +2 -2
  38. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/concepts.md +4 -3
  39. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/feedback-and-files.md +2 -2
  40. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/fork-side-goal.md +3 -2
  41. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/native-capabilities.md +4 -0
  42. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/session-collaboration.md +15 -0
  43. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/sessions.md +30 -0
  44. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/user-guide.md +2 -1
  45. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_bindings.py +1 -0
  46. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cards.py +81 -10
  47. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_channel_app.py +21 -6
  48. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_data.py +52 -36
  49. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_codex_runtime.py +13 -5
  50. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_completion_mentions.py +3 -3
  51. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_database_migrations.py +9 -5
  52. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_deployment.py +2 -1
  53. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_goal_reply_separation.py +3 -1
  54. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_migration_schema_validation.py +4 -2
  55. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_partial_answer_delivery.py +1 -0
  56. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_scheduled_tasks.py +9 -1
  57. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_scheduled_channel.py +4 -0
  58. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_external_runtime.py +48 -2
  59. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_tools.py +145 -0
  60. netizen_cli-0.13.0/tests/test_worktree_channel_cwd.py +308 -0
  61. netizen_cli-0.13.0/tests/test_worktree_git.py +444 -0
  62. netizen_cli-0.13.0/tests/test_worktree_new.py +293 -0
  63. netizen_cli-0.13.0/tests/test_worktree_runtime_cwd.py +724 -0
  64. netizen_cli-0.13.0/tests/test_worktree_service.py +493 -0
  65. netizen_cli-0.13.0/tests/test_worktree_settings.py +127 -0
  66. netizen_cli-0.13.0/tests/test_worktree_storage.py +436 -0
  67. netizen_cli-0.13.0/tests/test_worktree_tools_integration.py +165 -0
  68. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/MANIFEST.in +0 -0
  69. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/_build.py +0 -0
  70. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/__main__.py +0 -0
  71. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/account_rate_limits.py +0 -0
  72. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/__init__.py +0 -0
  73. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/auth.py +0 -0
  74. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/credentials.py +0 -0
  75. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/errors.py +0 -0
  76. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/feishu.py +0 -0
  77. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/login.py +0 -0
  78. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/presentation.py +0 -0
  79. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/queries.py +0 -0
  80. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/chat-picker.css +0 -0
  81. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/chat-picker.js +0 -0
  82. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/identity.js +0 -0
  83. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/login.html +0 -0
  84. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/login.js +0 -0
  85. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/static/session-access.js +0 -0
  86. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/transport.py +0 -0
  87. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/admin/users.py +0 -0
  88. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/builtin_skills.py +0 -0
  89. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/__init__.py +0 -0
  90. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/callbacks.py +0 -0
  91. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/chat_target.py +0 -0
  92. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/defaults.py +0 -0
  93. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/fork.py +0 -0
  94. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/model_info.py +0 -0
  95. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/pagination.py +0 -0
  96. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/questions.py +0 -0
  97. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/reply.py +0 -0
  98. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/scheduled.py +0 -0
  99. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/session_ids.py +0 -0
  100. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/status.py +0 -0
  101. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cards/usage.py +0 -0
  102. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/__init__.py +0 -0
  103. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/completion_mentions.py +0 -0
  104. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/input_preparation.py +0 -0
  105. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/messages.py +0 -0
  106. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/ports.py +0 -0
  107. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/question_inputs.py +0 -0
  108. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/reactions.py +0 -0
  109. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/reply_presenter.py +0 -0
  110. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/session_pages.py +0 -0
  111. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/channel/topics.py +0 -0
  112. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/chat_avatars.py +0 -0
  113. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/chat_targets.py +0 -0
  114. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli.py +0 -0
  115. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli_data.py +0 -0
  116. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli_packages.py +0 -0
  117. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli_services.py +0 -0
  118. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli_setup.py +0 -0
  119. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli_update.py +0 -0
  120. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli_update_restore.py +0 -0
  121. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/cli_update_worker.py +0 -0
  122. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/completion_mention.py +0 -0
  123. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/defaults/__init__.py +0 -0
  124. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/defaults/models.py +0 -0
  125. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/defaults/service.py +0 -0
  126. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/defaults/store.py +0 -0
  127. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/deployment/__init__.py +0 -0
  128. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/deployment/restart_worker.py +0 -0
  129. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/deployment/update_executor.py +0 -0
  130. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/deployment/update_protocol.py +0 -0
  131. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/error_messages.py +0 -0
  132. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/experience.py +0 -0
  133. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/feishu_app_onboarding.py +0 -0
  134. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/feishu_app_permissions.py +0 -0
  135. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/feishu_links.py +0 -0
  136. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/git_status.py +0 -0
  137. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/image_inputs.py +0 -0
  138. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/instance.py +0 -0
  139. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/lark_app.py +0 -0
  140. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/management/__init__.py +0 -0
  141. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/management/blocking_io.py +0 -0
  142. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/management/chat_directory.py +0 -0
  143. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/management/chat_labels.py +0 -0
  144. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/management/coordination.py +0 -0
  145. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/management/updates.py +0 -0
  146. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/markdown_images.py +0 -0
  147. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/mcp_transport.py +0 -0
  148. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/message_content.py +0 -0
  149. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/message_history.py +0 -0
  150. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/message_preparation.py +0 -0
  151. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/message_projection.py +0 -0
  152. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/migrations/__init__.py +0 -0
  153. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/migrations/schema.py +0 -0
  154. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/migrations/v14.py +0 -0
  155. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/model_settings.py +0 -0
  156. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/package_resources.py +0 -0
  157. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/pagination.py +0 -0
  158. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/partial_answers.py +0 -0
  159. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/projects.py +0 -0
  160. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/prompt_projection.py +0 -0
  161. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/quoted_context.py +0 -0
  162. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/result_images.py +0 -0
  163. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/runtime/__init__.py +0 -0
  164. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/runtime/name_writes.py +0 -0
  165. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/runtime/thread_naming.py +0 -0
  166. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/schedules/__init__.py +0 -0
  167. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/schedules/mcp.py +0 -0
  168. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/schedules/models.py +0 -0
  169. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/schedules/scheduler.py +0 -0
  170. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/schedules/service.py +0 -0
  171. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/schedules/store.py +0 -0
  172. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sdk_gap_adapter.py +0 -0
  173. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/service_launcher.py +0 -0
  174. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/session_inventory.py +0 -0
  175. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/session_settings.py +0 -0
  176. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/__init__.py +0 -0
  177. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/delegation.py +0 -0
  178. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/sessions/errors.py +0 -0
  179. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/skill_references.py +0 -0
  180. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/task_text.py +0 -0
  181. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/terminal_cleanup.py +0 -0
  182. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/thread_history.py +0 -0
  183. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/turn_activity.py +0 -0
  184. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/turn_files.py +0 -0
  185. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/turn_patch_children.py +0 -0
  186. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/turn_plan_observer.py +0 -0
  187. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli/user_questions.py +0 -0
  188. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli.egg-info/dependency_links.txt +0 -0
  189. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli.egg-info/entry_points.txt +0 -0
  190. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli.egg-info/requires.txt +0 -0
  191. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/netizen_cli.egg-info/top_level.txt +0 -0
  192. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/setup.cfg +0 -0
  193. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-lark/SKILL.md +0 -0
  194. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/SKILL.md +0 -0
  195. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/faq.md +0 -0
  196. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/messages.md +0 -0
  197. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/scheduled-tasks.md +0 -0
  198. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/skills/netizen-user-guide/references/status-and-control.md +0 -0
  199. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_account_rate_limits.py +0 -0
  200. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_admin_update_executor_probe.py +0 -0
  201. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_binding_scheduled_channel.py +0 -0
  202. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_channel_fixtures.py +0 -0
  203. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_channel_input_preparation.py +0 -0
  204. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_chat_avatars.py +0 -0
  205. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_chat_target_cards.py +0 -0
  206. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_check_sdk.py +0 -0
  207. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli.py +0 -0
  208. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_distribution.py +0 -0
  209. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_integration.py +0 -0
  210. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_packages.py +0 -0
  211. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_packaging.py +0 -0
  212. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_services.py +0 -0
  213. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_setup.py +0 -0
  214. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cli_update.py +0 -0
  215. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_codex_sdk_capabilities.py +0 -0
  216. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_codex_sdk_upgrade_0161.py +0 -0
  217. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_codex_sdk_upgrade_0163.py +0 -0
  218. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_completion_mention_cards.py +0 -0
  219. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_completion_topic_delivery.py +0 -0
  220. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_cron_chat_targets.py +0 -0
  221. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_defaults_cards.py +0 -0
  222. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_defaults_channel.py +0 -0
  223. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_defaults_runtime.py +0 -0
  224. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_documentation_links.py +0 -0
  225. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_domain_experience.py +0 -0
  226. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_error_messages.py +0 -0
  227. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_feishu_app_onboarding.py +0 -0
  228. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_feishu_app_permissions.py +0 -0
  229. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_file_pagination.py +0 -0
  230. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_fork_cards.py +0 -0
  231. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_git_status.py +0 -0
  232. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_image_inputs.py +0 -0
  233. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_lark_app.py +0 -0
  234. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_lark_sdk_contract.py +0 -0
  235. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_main.py +0 -0
  236. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_markdown_images.py +0 -0
  237. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_message_content.py +0 -0
  238. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_message_history.py +0 -0
  239. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_message_material_inputs.py +0 -0
  240. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_message_preparation.py +0 -0
  241. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_model_info_cards.py +0 -0
  242. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_model_settings.py +0 -0
  243. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_name_writes.py +0 -0
  244. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_pagination.py +0 -0
  245. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_partial_answers.py +0 -0
  246. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_persistent_fork_bindings.py +0 -0
  247. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_persistent_fork_channel.py +0 -0
  248. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_persistent_fork_runtime.py +0 -0
  249. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_child_files.py +0 -0
  250. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_feishu_completion_mention.py +0 -0
  251. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_feishu_partial_answers.py +0 -0
  252. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_feishu_turn_file_card.py +0 -0
  253. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_persistent_fork.py +0 -0
  254. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_python_sdk.py +0 -0
  255. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_sdk_completion_race.py +0 -0
  256. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_sdk_response_recovery.py +0 -0
  257. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_skill_execution.py +0 -0
  258. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_probe_thread_naming.py +0 -0
  259. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_project_delete_channel.py +0 -0
  260. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_project_delete_runtime.py +0 -0
  261. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_project_deletion_storage.py +0 -0
  262. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_prompt_projection.py +0 -0
  263. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_question_cards.py +0 -0
  264. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_question_channel.py +0 -0
  265. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_question_target_parity.py +0 -0
  266. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_quoted_context.py +0 -0
  267. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_release_script.py +0 -0
  268. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_repository_hygiene.py +0 -0
  269. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_result_images.py +0 -0
  270. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_resume_channel.py +0 -0
  271. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_retired_deployment.py +0 -0
  272. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_runtime_resume_activation.py +0 -0
  273. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_runtime_thread_naming.py +0 -0
  274. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_schedule_card_retry.py +0 -0
  275. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_schedule_cards.py +0 -0
  276. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_schedule_mcp.py +0 -0
  277. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_schedule_run_now.py +0 -0
  278. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_schedule_scope_matrix.py +0 -0
  279. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_schedule_service.py +0 -0
  280. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_scheduled_runtime.py +0 -0
  281. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_scheduler.py +0 -0
  282. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_scheduler_presenter.py +0 -0
  283. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_schedules_storage.py +0 -0
  284. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_sdk_gap_adapter.py +0 -0
  285. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_service_launcher.py +0 -0
  286. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_card_pages.py +0 -0
  287. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_collaboration_app.py +0 -0
  288. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_defaults.py +0 -0
  289. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_id_cards.py +0 -0
  290. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_policy.py +0 -0
  291. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_session_settings.py +0 -0
  292. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_settings_projects.py +0 -0
  293. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_status_cards.py +0 -0
  294. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_status_commands.py +0 -0
  295. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_task_text.py +0 -0
  296. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_terminal_cleanup.py +0 -0
  297. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_thread_history.py +0 -0
  298. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_thread_naming.py +0 -0
  299. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_turn_activity.py +0 -0
  300. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_turn_files.py +0 -0
  301. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_turn_patch_children.py +0 -0
  302. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_turn_patch_statistics.py +0 -0
  303. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_turn_plan_observer.py +0 -0
  304. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_update_executor.py +0 -0
  305. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_usage_card.py +0 -0
  306. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_usage_control.py +0 -0
  307. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_user_guide_skill.py +0 -0
  308. {netizen_cli-0.12.2 → netizen_cli-0.13.0}/tests/test_user_questions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netizen-cli
3
- Version: 0.12.2
3
+ Version: 0.13.0
4
4
  Summary: Use native Codex threads through a Feishu Channel.
5
5
  Requires-Python: <3.15,>=3.11
6
6
  Requires-Dist: h11==0.16.0
@@ -103,7 +103,7 @@ Agent/脚本使用 `netizen update --json`:stdout 只输出最终 JSON,进
103
103
  ### 3. 在飞书开始第一次对话
104
104
 
105
105
  1. 打开机器人单聊;如需在群里使用,先将机器人加入目标群。
106
- 2. 发送 `/settings`,创建或登记一个项目并启用。项目就是 Codex 实际工作的目录。
106
+ 2. 发送 `/settings`,创建或登记一个项目并启用。项目登记一个默认工作目录。
107
107
  3. 发送 `/new`,在卡片中选择项目。模型配置可选择“继承 Codex”;过程卡默认开启,可按需关闭。
108
108
  4. 直接发送任务,例如:**“梳理这个项目的结构,并生成一份入门说明。”**
109
109
  5. 收到回复后继续交流;如果出现“本轮文件”,点击“发送”取回需要的文件。
@@ -116,6 +116,11 @@ Agent/脚本使用 `netizen update --json`:stdout 只输出最终 JSON,进
116
116
  群聊和群话题中的每条请求都需要重新 **@机器人**,包括命令。`/new` 只通过卡片创建会话,
117
117
  不接受参数。普通任务执行中发来的新消息用于调整当前任务,不会排队成下一个任务。
118
118
 
119
+ Git 项目可在 `/new` 中加载项目目录选项,选择已有受管 worktree 或新建 worktree;默认
120
+ 仍使用项目目录。新建只检出提交内容,不复制本地改动或执行环境初始化。
121
+ 配置、状态查询与删除在 [Admin Worktrees](skills/netizen-user-guide/references/admin.md#worktree-配置与管理)
122
+ 中完成;不同会话可以共享同一工作区。`/fork` 不提供新建或选择 worktree 的入口。
123
+
119
124
  安装遇到问题可从[部署文档](docs/deployment.md)继续;已能对话时,常见疑问见
120
125
  [使用 FAQ](skills/netizen-user-guide/references/faq.md#常见问题)。
121
126
 
@@ -126,7 +131,7 @@ Agent/脚本使用 `netizen update --json`:stdout 只输出最终 JSON,进
126
131
  | 想做什么 | 入口 |
127
132
  | --- | --- |
128
133
  | 创建或登记项目工作目录 | `/settings` |
129
- | 新建会话,选择项目 | `/new` |
134
+ | 新建会话,选择项目及受管 worktree | `/new` |
130
135
  | 查看、保存或删除当前聊天的会话默认配置 | `/defaults` |
131
136
  | 查看并切换已有会话 | `/sessions` |
132
137
  | 从当前空闲会话另建持久分支话题 | `/fork` |
@@ -149,7 +154,7 @@ Agent/脚本使用 `netizen update --json`:stdout 只输出最终 JSON,进
149
154
  /side 这个方案还有哪些边界情况需要考虑?
150
155
  ```
151
156
 
152
- Side 会在同一聊天中新建话题,可继续多轮讨论。它与原会话共享项目目录,空闲两小时或
157
+ Side 会在同一聊天中新建话题,可继续多轮讨论。它与原会话共享实际工作目录,空闲两小时或
153
158
  服务重启后过期;`/side close` 主动结束话题会话。Side 中不支持 Goal。
154
159
 
155
160
  **每天回顾项目进展。** 在已经选择项目的普通会话中发送:
@@ -9,9 +9,16 @@ instance:
9
9
  # User and chat availability are configured in the Feishu app console. Netizen
10
10
  # does not maintain a second allowlist.
11
11
  projects:
12
- # All native Codex Threads under this alias share the exact same cwd.
12
+ # Default cwd. /new can explicitly select a managed Worktree of this Project.
13
13
  test: /home/your-user/projects/test
14
14
 
15
+ # Instance-owned Git worktrees; Admin can change these settings for future creates.
16
+ # Omitted root uses <instance-root>/worktrees; existing checkouts are not moved.
17
+ # No Codex App global configuration or private managed index is modified.
18
+ worktrees:
19
+ # root: /home/your-user/.netizen/worktrees
20
+ fetchBeforeCreate: false
21
+
15
22
  channel:
16
23
  securityMode: audit
17
24
 
@@ -1,3 +1,3 @@
1
1
  """Netizen: a thin Feishu Channel for native Codex Threads."""
2
2
 
3
- __version__ = "0.12.2"
3
+ __version__ = "0.13.0"
@@ -6,6 +6,7 @@ import contextlib
6
6
  import os
7
7
  import stat
8
8
  import tempfile
9
+ import threading
9
10
  from dataclasses import dataclass, field
10
11
  from pathlib import Path
11
12
 
@@ -45,6 +46,27 @@ class ConfigFileSnapshot:
45
46
  )
46
47
 
47
48
 
49
+ class ConfigFileEditor:
50
+ """Serialize instance-owned section edits against one exact file snapshot.
51
+
52
+ Settings services share this object so their successful writes do not make
53
+ each other stale. External edits still conflict with the complete snapshot;
54
+ they are never silently merged or made effective in the running service.
55
+ """
56
+
57
+ def __init__(self, snapshot: ConfigFileSnapshot) -> None:
58
+ self._snapshot = snapshot
59
+ self._lock = threading.RLock()
60
+
61
+ def update_section(self, name: str, value: object) -> None:
62
+ with self._lock:
63
+ loaded = yaml.safe_load(self._snapshot.content)
64
+ if not isinstance(loaded, dict):
65
+ raise AdminPortConfigurationError("configuration root must be a mapping")
66
+ loaded[name] = value
67
+ self._snapshot = write_configuration(self._snapshot, loaded)
68
+
69
+
48
70
  def _identity(metadata: os.stat_result) -> tuple[int, ...]:
49
71
  return (
50
72
  metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_size,
@@ -101,7 +101,8 @@ input, select, textarea { min-width: 0; border: 1px solid #bac3d0; border-radius
101
101
  .time-range-calendar-icon { position: relative; width: 15px; height: 14px; flex: 0 0 auto; border: 1.5px solid currentColor; border-radius: 3px; color: var(--muted); }
102
102
  .time-range-calendar-icon::before { content: ""; position: absolute; left: -1.5px; right: -1.5px; top: 3px; border-top: 1.5px solid currentColor; }
103
103
  .time-range-calendar-icon::after { content: ""; position: absolute; width: 2px; height: 2px; left: 3px; top: 7px; border-radius: 50%; background: currentColor; box-shadow: 4px 0 currentColor, 8px 0 currentColor; }
104
- .time-range-summary { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
104
+ .time-range-summary { min-width: 0; flex: 1; white-space: pre-line; overflow-wrap: anywhere; }
105
+ .time-range-dates { color: var(--muted); font-size: 11px; }
105
106
  .time-range-chevron { flex: 0 0 auto; color: var(--muted); font-size: 16px; line-height: 1; }
106
107
  .time-range-quick-clear { position: absolute; z-index: 1; top: 50%; right: 30px; width: 24px; height: 24px; transform: translateY(-50%); border: 0; border-radius: 50%; padding: 0; background: transparent; color: var(--muted); font-size: 18px; line-height: 22px; }
107
108
  .time-range-quick-clear:hover { background: #edf1f6; color: var(--ink); }
@@ -160,6 +161,12 @@ tr:last-child td { border-bottom: 0; }
160
161
  .badge.on { background: #e5f6ed; color: #136c3c; }
161
162
  .badge.warn { background: #fff2df; color: #925400; }
162
163
  .actions { display: flex; flex-wrap: wrap; gap: 5px; }
164
+ .cascade-choice, .card-form label.cascade-choice { display: flex; align-items: center; gap: 6px; font-size: 12px; }
165
+ .cascade-choice input, .card-form .cascade-choice input { width: auto; margin: 0; }
166
+ #worktrees-body td { white-space: pre-line; overflow-wrap: anywhere; }
167
+ #worktrees-body td:last-child { min-width: 170px; }
168
+ #worktrees-body .action { margin-top: 8px; }
169
+ #worktree-result { white-space: pre-line; }
163
170
  .project-delete-result { margin: 16px 0; padding: 14px 16px; border: 1px solid var(--line); border-radius: 9px; background: var(--wash); font-size: 13px; line-height: 1.6; }
164
171
  .project-delete-result.error { border-color: #e3b8b4; color: var(--danger); }
165
172
  .project-delete-result p { margin: 0; white-space: pre-line; }
@@ -62,6 +62,7 @@ function expireAdminSession() {
62
62
  if (notice) notice.hidden = false;
63
63
  window.NetizenIdentity?.expire();
64
64
  window.NetizenSessionAccess?.expire();
65
+ window.NetizenWorktrees?.expire();
65
66
  setStatus("登录已过期,请重新登录。", true);
66
67
  }
67
68
 
@@ -444,11 +445,13 @@ function confirmProjectDelete(preview) {
444
445
  + "\n定时计划将被删除;后续会话清理失败也不会恢复计划。"
445
446
  + "\n\n原生会话、派生子会话、Codex App/CLI 历史和本地会话登记将永久删除,无法恢复。"
446
447
  + "\n全部会话删除确认成功后,才会删除 Project 登记;部分失败时会保留 Project 和剩余会话。"
448
+ + `\n关联受管 Worktrees:${preview.worktreeCount || 0},${preview.deleteWorktrees ? "同时安全删除;失败项保留,不强制丢弃成果" : "保留目录,可在 Worktrees 页面处理;不会自动关联到同名新 Project"}。`
449
+ + (preview.deleteWorktrees && preview.worktrees?.length ? `\n将删除的 checkout 根目录:\n${preview.worktrees.map((item) => `${item.id} · ${item.root}`).join("\n")}` : "")
447
450
  + `\n\n磁盘代码目录保留:${preview.project.cwd}`,
448
451
  );
449
452
  }
450
453
 
451
- async function deleteProject(project) {
454
+ async function deleteProject(project, deleteWorktrees = false) {
452
455
  const envelope = project.actions.previewDelete;
453
456
  if (!envelope || pendingProjectDeletes.has(project.alias)) return;
454
457
  pendingProjectDeletes.add(project.alias);
@@ -467,7 +470,7 @@ async function deleteProject(project) {
467
470
  const preview = await api("/api/v1/projects/delete-preview", {
468
471
  method: "POST",
469
472
  headers: { "Content-Type": "application/json" },
470
- body: JSON.stringify(actionPayload(envelope)),
473
+ body: JSON.stringify(actionPayload(envelope, deleteWorktrees ? { deleteWorktrees: true } : {})),
471
474
  });
472
475
  if (!confirmProjectDelete(preview)) {
473
476
  message = `已取消删除 Project「${project.alias}」,未提交删除。`;
@@ -498,6 +501,11 @@ async function deleteProject(project) {
498
501
  if (result.remainingScheduledRunCount) {
499
502
  details.push(`仍有 ${result.remainingScheduledRunCount} 条定时执行的交接或清理未确认。`);
500
503
  }
504
+ if (result.deletedWorktreeCount) message += `\n已删除 ${result.deletedWorktreeCount} 个受管 Worktree。`;
505
+ if (result.remainingWorktreeCount) {
506
+ if (result.deleteWorktrees) details.push(`仍有 ${result.remainingWorktreeCount} 个受管 Worktree 未安全删除,请在 Worktrees 页面检查。`);
507
+ else message += `\n已按选择保留 ${result.remainingWorktreeCount} 个受管 Worktree,可在 Worktrees 页面处理。`;
508
+ }
501
509
  return result;
502
510
  } catch (error) {
503
511
  isError = true;
@@ -551,8 +559,14 @@ async function loadProjects(cursor = undefined) {
551
559
  ));
552
560
  }
553
561
  if (project.actions.previewDelete) {
562
+ const cascadeLabel = document.createElement("label");
563
+ cascadeLabel.className = "cascade-choice";
564
+ const cascade = document.createElement("input");
565
+ cascade.type = "checkbox";
566
+ cascadeLabel.append(cascade, document.createTextNode("同时删除关联受管 Worktrees"));
567
+ actions.append(cascadeLabel);
554
568
  actions.append(actionButton(
555
- "删除 Project 及关联 Sessions", () => deleteProject(project), true,
569
+ "删除 Project 及关联 Sessions", () => deleteProject(project, cascade.checked), true,
556
570
  ));
557
571
  }
558
572
  if (project.deleting || pendingProjectDeletes.has(project.alias)) {
@@ -1022,6 +1036,10 @@ function formQuery(form, defaultPageSize = "25", refreshRelativeTime = true) {
1022
1036
  for (const [key, value] of new FormData(form)) {
1023
1037
  if (String(value)) query.append(key, String(value));
1024
1038
  }
1039
+ for (const root of form.querySelectorAll("[data-time-range]")) {
1040
+ const kind = timeRangeControllers.get(root)?.presetKind();
1041
+ if (kind) query.set(`${root.dataset.timeRangeField || "created"}Range`, kind);
1042
+ }
1025
1043
  for (const root of form.querySelectorAll("[data-session-multi], [data-side-multi]")) {
1026
1044
  const name = root.dataset.sessionMulti || root.dataset.sideMulti;
1027
1045
  const filters = root.dataset.sideMulti ? sideMultiFilters : sessionMultiFilters;
@@ -1034,7 +1052,7 @@ function formQuery(form, defaultPageSize = "25", refreshRelativeTime = true) {
1034
1052
  const identity = new URLSearchParams(state.sessionPage?.query || "").get("identity");
1035
1053
  if (identity) query.set("identity", identity);
1036
1054
  if (query.get("inventoryState") === "lazy") {
1037
- for (const key of ["createdFrom", "createdBefore", "updatedFrom", "updatedBefore"]) query.delete(key);
1055
+ for (const key of ["createdFrom", "createdBefore", "createdRange", "updatedFrom", "updatedBefore", "updatedRange"]) query.delete(key);
1038
1056
  } else if (!query.has("updatedFrom") && !query.has("updatedBefore")) {
1039
1057
  query.set("updatedRange", "all"); // URL intent only; never sent to the API.
1040
1058
  }
@@ -1082,10 +1100,6 @@ function formatLocalMinute(value) {
1082
1100
  return localMinuteValue(value).replace("T", " ");
1083
1101
  }
1084
1102
 
1085
- function compactLocalMinute(value) {
1086
- return `${twoDigits(value.getMonth() + 1)}-${twoDigits(value.getDate())} ${twoDigits(value.getHours())}:${twoDigits(value.getMinutes())}`;
1087
- }
1088
-
1089
1103
  function utcOffsetLabel(value) {
1090
1104
  const total = -value.getTimezoneOffset();
1091
1105
  const sign = total >= 0 ? "+" : "-";
@@ -1182,6 +1196,24 @@ function allTimeRange() {
1182
1196
  };
1183
1197
  }
1184
1198
 
1199
+ function timeRangeSummary(from, before, kind = "custom", now = new Date()) {
1200
+ if (!from && !before) return "全部时间";
1201
+ const rolling = kind.startsWith("last-");
1202
+ const start = new Date(from);
1203
+ // Show the last included minute, including a partially included rolling minute.
1204
+ const end = before ? new Date(new Date(before).getTime() - 1) : now;
1205
+ const startLabel = Number.isNaN(start.getTime()) ? from || "不限起点" : formatLocalMinute(start);
1206
+ const endLabel = !before && !rolling ? "不限终点"
1207
+ : Number.isNaN(end.getTime()) ? before : formatLocalMinute(end);
1208
+ const label = timeRangePresetLabels[kind];
1209
+ const name = label && kind !== "all" ? `${label}${!before && rolling ? "(至今)" : ""}` : "";
1210
+ const dates = [from ? start : null, before || rolling ? end : null]
1211
+ .filter((value) => value && !Number.isNaN(value.getTime()));
1212
+ const offsets = [...new Set(dates.map(utcOffsetLabel))].join(" → ");
1213
+ const heading = [name, offsets].filter(Boolean).join(" · ");
1214
+ return `${heading ? `${heading}\n` : ""}${startLabel} ~ ${endLabel}`;
1215
+ }
1216
+
1185
1217
  function presetTimeRange(kind, now = new Date(), openEnded = false) {
1186
1218
  if (kind === "all") return allTimeRange();
1187
1219
  let from;
@@ -1210,18 +1242,16 @@ function presetTimeRange(kind, now = new Date(), openEnded = false) {
1210
1242
  before = new Date(now.getTime());
1211
1243
  from = new Date(now.getTime() - durationDays * 24 * 60 * 60 * 1000);
1212
1244
  }
1213
- const label = timeRangePresetLabels[kind];
1214
1245
  const rolling = kind.startsWith("last-");
1215
- const summary = rolling && !openEnded
1216
- ? `${label} · 至 ${compactLocalMinute(before)} · ${utcOffsetLabel(before)}`
1217
- : `${label} · ${utcOffsetLabel(from)}`;
1246
+ const fromUtc = canonicalUtc(from);
1247
+ const beforeUtc = openEnded && rolling ? "" : canonicalUtc(before);
1218
1248
  return {
1219
1249
  kind,
1220
- from: canonicalUtc(from),
1221
- before: openEnded && rolling ? "" : canonicalUtc(before),
1250
+ from: fromUtc,
1251
+ before: beforeUtc,
1222
1252
  startValue: localMinuteValue(from),
1223
- endValue: localMinuteValue(new Date(before.getTime() - 60 * 1000)),
1224
- summary,
1253
+ endValue: localMinuteValue(new Date(before.getTime() - 1)),
1254
+ summary: timeRangeSummary(fromUtc, beforeUtc, kind, now),
1225
1255
  };
1226
1256
  }
1227
1257
 
@@ -1252,7 +1282,7 @@ function customTimeRange(startValue, endValue) {
1252
1282
  before: canonicalUtc(before),
1253
1283
  startValue,
1254
1284
  endValue,
1255
- summary: `${formatLocalMinute(parsedStart.date)} — ${formatLocalMinute(parsedEnd.date)} · ${utcOffsetLabel(parsedStart.date)}`,
1285
+ summary: timeRangeSummary(canonicalUtc(parsedStart.date), canonicalUtc(before)),
1256
1286
  },
1257
1287
  error: "",
1258
1288
  invalid: null,
@@ -1318,7 +1348,19 @@ function initializeTimeRangeFilter(root) {
1318
1348
  function renderApplied() {
1319
1349
  fromNode.value = applied.from;
1320
1350
  beforeNode.value = applied.before;
1321
- summary.textContent = applied.summary;
1351
+ applied.summary = timeRangeSummary(applied.from, applied.before, applied.kind);
1352
+ const [heading, bounds] = applied.summary.split("\n");
1353
+ if (bounds) {
1354
+ const name = document.createElement("span");
1355
+ name.textContent = `${heading}\n`;
1356
+ const dates = document.createElement("span");
1357
+ dates.className = "time-range-dates";
1358
+ dates.textContent = bounds;
1359
+ summary.replaceChildren(name, dates);
1360
+ } else summary.textContent = applied.summary;
1361
+ const dates = [applied.from, applied.before || new Date()].filter(Boolean)
1362
+ .map((value) => new Date(value)).filter((value) => !Number.isNaN(value.getTime()));
1363
+ trigger.title = `${applied.summary}\n${zoneDescription(dates)}`;
1322
1364
  trigger.classList.toggle("has-value", applied.kind !== "all");
1323
1365
  trigger.setAttribute("aria-label", `${label}:${applied.summary}`);
1324
1366
  quickClear.hidden = applied.kind === "all";
@@ -1405,7 +1447,7 @@ function initializeTimeRangeFilter(root) {
1405
1447
  }
1406
1448
 
1407
1449
  function prepareQuery() {
1408
- if (applied.kind === "all" || applied.kind === "custom") return;
1450
+ if (applied.kind === "all" || applied.kind === "custom" || applied.restored) return;
1409
1451
  applied = preset(applied.kind);
1410
1452
  renderApplied();
1411
1453
  }
@@ -1430,15 +1472,17 @@ function initializeTimeRangeFilter(root) {
1430
1472
  const controller = {
1431
1473
  cancel: cancelDraft,
1432
1474
  prepareQuery,
1433
- setBounds(from, before) {
1475
+ refreshSummary: renderApplied,
1476
+ presetKind() { return applied.kind !== "all" && applied.kind !== "custom" ? applied.kind : null; },
1477
+ setBounds(from, before, kind = "custom") {
1434
1478
  if (!from && !before) { this.reset("all"); return; }
1435
1479
  const startDate = new Date(from);
1436
1480
  const beforeDate = new Date(before);
1437
- const endDate = new Date(beforeDate.getTime() - 60000);
1438
- applied = { kind: "custom", from, before,
1481
+ const endDate = new Date(beforeDate.getTime() - 1);
1482
+ // URL bounds stay absolute; the preset name is presentation metadata only.
1483
+ applied = { kind: Object.hasOwn(timeRangePresetLabels, kind) && kind !== "all" ? kind : "custom", from, before, restored: true,
1439
1484
  startValue: Number.isNaN(startDate.getTime()) ? "" : localMinuteValue(startDate),
1440
- endValue: Number.isNaN(endDate.getTime()) ? "" : localMinuteValue(endDate),
1441
- summary: `${Number.isNaN(startDate.getTime()) ? from || "不限起点" : formatLocalMinute(startDate)} — ${Number.isNaN(endDate.getTime()) ? before || "不限终点" : formatLocalMinute(endDate)}` };
1485
+ endValue: Number.isNaN(endDate.getTime()) ? "" : localMinuteValue(endDate) };
1442
1486
  draft = { ...applied };
1443
1487
  closePopover({ restoreFocus: false });
1444
1488
  renderApplied();
@@ -1836,7 +1880,7 @@ async function loadSessionPage(kind, page, render) {
1836
1880
  const candidate = { ...page, query: query.toString(), previousCursors: [...page.previousCursors] };
1837
1881
  await loadSessionProjectOptions(kind === "side" ? sideMultiFilters : sessionMultiFilters, isCurrent);
1838
1882
  if (!isCurrent()) return false;
1839
- query.delete("updatedRange");
1883
+ for (const field of ["created", "updated"]) query.delete(`${field}Range`);
1840
1884
  if (page.cursor) query.set("cursor", page.cursor);
1841
1885
  const data = await api(`/api/v1/${kind === "side" ? "side-topics" : "sessions"}?${query}`);
1842
1886
  if (!isCurrent()) return false;
@@ -1849,6 +1893,9 @@ async function loadSessionPage(kind, page, render) {
1849
1893
  renderSessionPagination(kind);
1850
1894
  renderSessionSort(kind);
1851
1895
  renderSessionFreshness();
1896
+ for (const root of document.querySelector(`#${kind}-filter`).querySelectorAll("[data-time-range]")) {
1897
+ timeRangeControllers.get(root)?.refreshSummary();
1898
+ }
1852
1899
  syncPageUrl();
1853
1900
  setStatus("已更新。");
1854
1901
  return true;
@@ -2694,20 +2741,22 @@ function confirmDiscardChanges(changed) {
2694
2741
 
2695
2742
  function hasUnsavedEditors() {
2696
2743
  return sessionSettingsHasChanges() || defaultEditorHasChanges() || scheduleEditorHasChanges()
2697
- || Boolean(pendingScheduleDraft?.dirty) || Boolean(window.NetizenSessionAccess?.dirty());
2744
+ || Boolean(pendingScheduleDraft?.dirty) || Boolean(window.NetizenSessionAccess?.dirty())
2745
+ || Boolean(window.NetizenWorktrees?.dirty());
2698
2746
  }
2699
2747
 
2700
2748
  // Navigation asks once before closing any editor. Busy submissions retain the
2701
2749
  // existing fail-closed boundary; this helper never cancels or repeats a POST.
2702
2750
  function confirmDiscardEditors() {
2703
2751
  if (sessionSettingsEditor?.saving || defaultsBusy || (scheduleEditor && scheduleInput("fields").disabled)
2704
- || window.NetizenSessionAccess?.busy()) return false;
2752
+ || window.NetizenSessionAccess?.busy() || window.NetizenWorktrees?.busy()) return false;
2705
2753
  if (!confirmDiscardChanges(hasUnsavedEditors())) return false;
2706
2754
  closeSessionSettingsEditor({ confirmed: true });
2707
2755
  closeDefaultEditor({ confirmed: true });
2708
2756
  if (scheduleEditor) closeScheduleEditor({ confirmed: true });
2709
2757
  cancelScheduleSelection({ confirmed: true });
2710
2758
  window.NetizenSessionAccess?.discard();
2759
+ window.NetizenWorktrees?.discard();
2711
2760
  return true;
2712
2761
  }
2713
2762
 
@@ -3801,11 +3850,11 @@ async function moveScheduleRunsPage(direction) {
3801
3850
 
3802
3851
  // Only durable query intent belongs in URLs. Grants, editor drafts and live
3803
3852
  // keyset cursors stay in this document and are never restored as server facts.
3804
- const adminPagePaths = { projects: "/projects", schedules: "/schedules", defaults: "/defaults",
3853
+ const adminPagePaths = { projects: "/projects", worktrees: "/worktrees", schedules: "/schedules", defaults: "/defaults",
3805
3854
  updates: "/maintenance", system: "/system" };
3806
3855
  const sessionUrlKeys = ["project", "scopeKind", "inventoryState", "current", "chatId", "topicId", "identity",
3807
- "createdFrom", "createdBefore", "updatedFrom", "updatedBefore", "updatedRange", "pageSize", "sortBy", "sortDirection"];
3808
- const sideUrlKeys = ["project", "state", "chatId", "createdFrom", "createdBefore", "pageSize", "sortBy", "sortDirection"];
3856
+ "createdFrom", "createdBefore", "createdRange", "updatedFrom", "updatedBefore", "updatedRange", "pageSize", "sortBy", "sortDirection"];
3857
+ const sideUrlKeys = ["project", "state", "chatId", "createdFrom", "createdBefore", "createdRange", "pageSize", "sortBy", "sortDirection"];
3809
3858
  let navigationReady = false;
3810
3859
  let navigationIndex = 0;
3811
3860
  let navigationHref = "";
@@ -3826,13 +3875,20 @@ function pageQuery(tab, sessionTab, raw = "") {
3826
3875
  if (!["active", "archived", "lazy"].includes(query.get(field))) query.set(field, "active");
3827
3876
  else query.set(field, query.get(field));
3828
3877
  if (query.get(field) === "lazy") {
3829
- for (const key of ["createdFrom", "createdBefore", "updatedFrom", "updatedBefore", "updatedRange"]) query.delete(key);
3878
+ for (const key of ["createdFrom", "createdBefore", "createdRange", "updatedFrom", "updatedBefore", "updatedRange"]) query.delete(key);
3830
3879
  query.set("sortBy", "createdAt");
3831
3880
  } else if (!query.has("updatedFrom") && !query.has("updatedBefore") && query.get("updatedRange") !== "all") {
3832
3881
  const range = presetTimeRange("last-3-months", new Date(), true);
3833
3882
  query.set("updatedFrom", range.from);
3883
+ query.set("updatedRange", range.kind);
3834
3884
  }
3835
- if (query.has("updatedFrom") || query.has("updatedBefore")) query.delete("updatedRange");
3885
+ }
3886
+ for (const field of ["created", "updated"]) {
3887
+ const kind = query.get(`${field}Range`);
3888
+ const hasBounds = query.has(`${field}From`) || query.has(`${field}Before`);
3889
+ const valid = Object.hasOwn(timeRangePresetLabels, kind) && (kind === "all" ? !hasBounds : hasBounds);
3890
+ if (valid) query.set(`${field}Range`, kind);
3891
+ else query.delete(`${field}Range`);
3836
3892
  }
3837
3893
  if (!query.has("pageSize")) query.set("pageSize", "20");
3838
3894
  if (!query.has("sortBy")) query.set("sortBy", "updatedAt");
@@ -3915,7 +3971,7 @@ function restorePageFilters(route) {
3915
3971
  document.querySelector(`#${kind}-page-size`).value = query.get("pageSize");
3916
3972
  for (const root of document.querySelector(`#${kind}-filter`).querySelectorAll("[data-time-range]")) {
3917
3973
  const field = root.dataset.timeRangeField || "created";
3918
- timeRangeControllers.get(root).setBounds(query.get(`${field}From`) || "", query.get(`${field}Before`) || "");
3974
+ timeRangeControllers.get(root).setBounds(query.get(`${field}From`) || "", query.get(`${field}Before`) || "", query.get(`${field}Range`) || "custom");
3919
3975
  }
3920
3976
  if (kind === "session") {
3921
3977
  document.querySelector("#session-inventory-state").value = query.get("inventoryState");
@@ -3993,6 +4049,7 @@ async function refresh(tab, cursor = undefined) {
3993
4049
  setStatus("正在读取服务端事实…");
3994
4050
  try {
3995
4051
  if (tab === "projects" && await loadProjects(cursor) === false) return false;
4052
+ if (tab === "worktrees" && await window.NetizenWorktrees.load() === false) return false;
3996
4053
  if (tab === "updates") await loadUpdates();
3997
4054
  if (tab === "schedules" && await loadSchedules(cursor) === false) return false;
3998
4055
  if (tab === "defaults") await loadDefaults();
@@ -4126,6 +4183,7 @@ document.querySelector("#side-page-size").addEventListener("change", () => {
4126
4183
  });
4127
4184
  document.querySelector("#projects-previous").addEventListener("click", () => moveListPage("projects", "previous"));
4128
4185
  document.querySelector("#projects-next").addEventListener("click", () => moveListPage("projects", "next"));
4186
+ window.NetizenWorktrees?.init({ api, actionPayload, setStatus, queryProjectOptions, isExpired: () => state.authExpired });
4129
4187
  document.querySelector("#sessions-previous").addEventListener("click", () => moveSessionPage("previous"));
4130
4188
  document.querySelector("#sessions-next").addEventListener("click", () => moveSessionPage("next"));
4131
4189
  document.querySelector("#sides-previous").addEventListener("click", () => moveSidePage("previous"));
@@ -9,6 +9,7 @@
9
9
  <script src="/static/chat-picker.js" defer></script>
10
10
  <script src="/static/identity.js" defer></script>
11
11
  <script src="/static/session-access.js" defer></script>
12
+ <script src="/static/worktrees.js" defer></script>
12
13
  <script src="/static/admin.js" defer></script>
13
14
  </head>
14
15
  <body>
@@ -24,6 +25,7 @@
24
25
 
25
26
  <nav class="tabs" aria-label="管理模块">
26
27
  <a class="tab active" data-tab="projects" href="/projects" aria-current="page">Projects</a>
28
+ <a class="tab" data-tab="worktrees" href="/worktrees">Worktrees</a>
27
29
  <a class="tab" data-tab="sessions" href="/sessions/ordinary">Sessions</a>
28
30
  <a class="tab" data-tab="schedules" href="/schedules">定时任务</a>
29
31
  <a class="tab" data-tab="defaults" href="/defaults">默认会话配置</a>
@@ -67,6 +69,34 @@
67
69
  </nav>
68
70
  </section>
69
71
 
72
+ <section id="worktrees" class="panel" aria-labelledby="worktrees-heading" hidden>
73
+ <div class="section-head"><div><p class="eyebrow">MANAGED CHECKOUTS</p><h2 id="worktrees-heading">Worktrees</h2></div><button class="refresh" data-refresh="worktrees">刷新</button></div>
74
+ <p class="hint">仅管理本实例创建的工作区。多个会话可共享文件和分支;列表按需查询,不扫描未提交内容,也不能证明没有外部客户端使用。</p>
75
+ <div class="forms-grid">
76
+ <form id="worktree-settings" class="card-form">
77
+ <h3>配置</h3>
78
+ <label>新建根目录(留空使用实例默认目录)<input id="worktree-root" maxlength="4096"></label>
79
+ <p id="worktree-effective-root" class="hint"></p>
80
+ <label class="cascade-choice"><input id="worktree-fetch" type="checkbox">创建前 fetch;失败则终止创建</label>
81
+ <p class="hint">只影响之后创建的位置,已有工作区仍按原路径管理;不修改 Codex App 配置。</p>
82
+ <button id="worktree-settings-save" type="submit" disabled>保存配置</button>
83
+ </form>
84
+ <form id="worktree-create" class="card-form">
85
+ <h3>新建 Worktree</h3>
86
+ <label>Project<select id="worktree-project" required><option value="">选择 Project</option></select></label>
87
+ <label>检出方式<select id="worktree-mode"><option value="detached">Detached(不绑定命名分支)</option><option value="existing">检出已有本地分支</option><option value="new">创建并检出新分支</option></select></label>
88
+ <label id="worktree-ref-label">起点提交 / 分支<input id="worktree-ref" list="worktree-refs" placeholder="HEAD"><datalist id="worktree-refs"></datalist></label>
89
+ <label id="worktree-existing-label" hidden>本地分支<select id="worktree-existing"></select></label>
90
+ <label id="worktree-new-label" hidden>新分支名称<input id="worktree-new-branch" maxlength="1024"></label>
91
+ <p id="worktree-create-note" class="hint">只检出已提交内容,不复制未提交、未跟踪或忽略文件,不执行环境初始化。不创建会话。</p>
92
+ <button id="worktree-create-submit" type="submit" disabled>创建 Worktree</button>
93
+ </form>
94
+ </div>
95
+ <p id="worktree-result" class="status" role="status" aria-live="polite"></p>
96
+ <div class="table-wrap"><table><thead><tr><th>Project / 标识</th><th>工作目录</th><th>当前状态</th><th>分支 / HEAD</th><th>关联会话</th><th>操作</th></tr></thead><tbody id="worktrees-body"></tbody></table></div>
97
+ <nav class="pagination" aria-label="Worktrees 分页"><button id="worktrees-previous" class="next" disabled>上一页</button><span id="worktrees-page" aria-live="polite">第 1 页</span><button id="worktrees-next" class="next" disabled>下一页</button></nav>
98
+ </section>
99
+
70
100
  <section id="sessions" class="panel" aria-labelledby="sessions-heading" hidden>
71
101
  <div class="section-head">
72
102
  <div><p class="eyebrow">CONVERSATIONS</p><h2 id="sessions-heading">Sessions</h2></div>