netizen-cli 0.13.1__tar.gz → 0.13.2__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 (313) hide show
  1. {netizen_cli-0.13.1/netizen_cli.egg-info → netizen_cli-0.13.2}/PKG-INFO +1 -1
  2. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/__init__.py +1 -1
  3. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/admin.css +27 -10
  4. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/admin.js +100 -7
  5. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/index.html +6 -0
  6. netizen_cli-0.13.2/netizen_cli/admin/static/login.html +41 -0
  7. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/web.py +4 -4
  8. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/bindings.py +2 -2
  9. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/controls.py +94 -72
  10. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/defaults.py +166 -14
  11. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel_app.py +51 -7
  12. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/database_migrations.py +5 -1
  13. netizen_cli-0.13.2/netizen_cli/defaults/models.py +85 -0
  14. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/defaults/service.py +76 -9
  15. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/defaults/store.py +32 -4
  16. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/management/service.py +68 -1
  17. netizen_cli-0.13.2/netizen_cli/migrations/v16.py +104 -0
  18. {netizen_cli-0.13.1 → netizen_cli-0.13.2/netizen_cli.egg-info}/PKG-INFO +1 -1
  19. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli.egg-info/SOURCES.txt +3 -0
  20. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/pyproject.toml +1 -1
  21. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/sessions.md +25 -7
  22. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/user-guide.md +2 -0
  23. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cards.py +41 -5
  24. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_channel_app.py +1 -1
  25. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_data.py +8 -6
  26. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_database_migrations.py +4 -4
  27. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_defaults_cards.py +163 -5
  28. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_defaults_channel.py +120 -0
  29. netizen_cli-0.13.2/tests/test_defaults_worktree_channel.py +374 -0
  30. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_migration_schema_validation.py +1 -1
  31. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_defaults.py +153 -0
  32. netizen_cli-0.13.2/tests/test_session_defaults_migration.py +106 -0
  33. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_storage.py +1 -1
  34. netizen_cli-0.13.1/netizen_cli/admin/static/login.html +0 -30
  35. netizen_cli-0.13.1/netizen_cli/defaults/models.py +0 -35
  36. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/MANIFEST.in +0 -0
  37. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/README.md +0 -0
  38. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/_build.py +0 -0
  39. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/config.example.yaml +0 -0
  40. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/__main__.py +0 -0
  41. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/account_rate_limits.py +0 -0
  42. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/__init__.py +0 -0
  43. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/auth.py +0 -0
  44. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/credentials.py +0 -0
  45. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/errors.py +0 -0
  46. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/feishu.py +0 -0
  47. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/login.py +0 -0
  48. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/port_config.py +0 -0
  49. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/presentation.py +0 -0
  50. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/queries.py +0 -0
  51. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/chat-picker.css +0 -0
  52. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/chat-picker.js +0 -0
  53. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/identity.js +0 -0
  54. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/login.js +0 -0
  55. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/session-access.js +0 -0
  56. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/static/worktrees.js +0 -0
  57. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/transport.py +0 -0
  58. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/admin/users.py +0 -0
  59. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/builtin_skills.py +0 -0
  60. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/__init__.py +0 -0
  61. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/callbacks.py +0 -0
  62. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/chat_target.py +0 -0
  63. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/fork.py +0 -0
  64. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/model_info.py +0 -0
  65. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/pagination.py +0 -0
  66. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/questions.py +0 -0
  67. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/reply.py +0 -0
  68. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/scheduled.py +0 -0
  69. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/session_ids.py +0 -0
  70. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/status.py +0 -0
  71. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cards/usage.py +0 -0
  72. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/__init__.py +0 -0
  73. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/completion_mentions.py +0 -0
  74. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/input_preparation.py +0 -0
  75. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/messages.py +0 -0
  76. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/ports.py +0 -0
  77. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/question_inputs.py +0 -0
  78. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/reactions.py +0 -0
  79. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/reply_presenter.py +0 -0
  80. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/session_pages.py +0 -0
  81. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/channel/topics.py +0 -0
  82. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/chat_avatars.py +0 -0
  83. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/chat_targets.py +0 -0
  84. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli.py +0 -0
  85. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli_data.py +0 -0
  86. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli_packages.py +0 -0
  87. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli_services.py +0 -0
  88. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli_setup.py +0 -0
  89. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli_update.py +0 -0
  90. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli_update_restore.py +0 -0
  91. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/cli_update_worker.py +0 -0
  92. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/codex_runtime.py +0 -0
  93. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/completion_mention.py +0 -0
  94. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/defaults/__init__.py +0 -0
  95. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/deployment/__init__.py +0 -0
  96. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/deployment/restart_worker.py +0 -0
  97. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/deployment/update_executor.py +0 -0
  98. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/deployment/update_protocol.py +0 -0
  99. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/domain.py +0 -0
  100. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/error_messages.py +0 -0
  101. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/experience.py +0 -0
  102. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/feishu_app_onboarding.py +0 -0
  103. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/feishu_app_permissions.py +0 -0
  104. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/feishu_links.py +0 -0
  105. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/git_status.py +0 -0
  106. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/image_inputs.py +0 -0
  107. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/instance.py +0 -0
  108. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/lark_app.py +0 -0
  109. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/main.py +0 -0
  110. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/management/__init__.py +0 -0
  111. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/management/blocking_io.py +0 -0
  112. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/management/chat_directory.py +0 -0
  113. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/management/chat_labels.py +0 -0
  114. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/management/coordination.py +0 -0
  115. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/management/updates.py +0 -0
  116. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/markdown_images.py +0 -0
  117. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/mcp_transport.py +0 -0
  118. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/message_content.py +0 -0
  119. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/message_history.py +0 -0
  120. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/message_preparation.py +0 -0
  121. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/message_projection.py +0 -0
  122. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/migrations/__init__.py +0 -0
  123. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/migrations/schema.py +0 -0
  124. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/migrations/v14.py +0 -0
  125. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/migrations/v15.py +0 -0
  126. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/model_settings.py +0 -0
  127. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/package_resources.py +0 -0
  128. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/pagination.py +0 -0
  129. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/partial_answers.py +0 -0
  130. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/projects.py +0 -0
  131. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/prompt_projection.py +0 -0
  132. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/quoted_context.py +0 -0
  133. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/result_images.py +0 -0
  134. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/runtime/__init__.py +0 -0
  135. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/runtime/contracts.py +0 -0
  136. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/runtime/name_writes.py +0 -0
  137. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/runtime/thread_naming.py +0 -0
  138. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/schedules/__init__.py +0 -0
  139. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/schedules/mcp.py +0 -0
  140. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/schedules/models.py +0 -0
  141. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/schedules/scheduler.py +0 -0
  142. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/schedules/service.py +0 -0
  143. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/schedules/store.py +0 -0
  144. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sdk_gap_adapter.py +0 -0
  145. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/service_launcher.py +0 -0
  146. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/session_inventory.py +0 -0
  147. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/session_settings.py +0 -0
  148. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/__init__.py +0 -0
  149. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/delegation.py +0 -0
  150. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/errors.py +0 -0
  151. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/mcp.py +0 -0
  152. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/policy.py +0 -0
  153. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/presentation.py +0 -0
  154. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/schema.py +0 -0
  155. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/sessions/service.py +0 -0
  156. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/settings.py +0 -0
  157. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/skill_references.py +0 -0
  158. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/task_text.py +0 -0
  159. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/terminal_cleanup.py +0 -0
  160. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/thread_history.py +0 -0
  161. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/turn_activity.py +0 -0
  162. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/turn_files.py +0 -0
  163. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/turn_patch_children.py +0 -0
  164. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/turn_plan_observer.py +0 -0
  165. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/user_questions.py +0 -0
  166. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/worktrees/errors.py +0 -0
  167. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/worktrees/git.py +0 -0
  168. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/worktrees/models.py +0 -0
  169. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/worktrees/service.py +0 -0
  170. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/worktrees/settings.py +0 -0
  171. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli/worktrees/store.py +0 -0
  172. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli.egg-info/dependency_links.txt +0 -0
  173. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli.egg-info/entry_points.txt +0 -0
  174. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli.egg-info/requires.txt +0 -0
  175. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/netizen_cli.egg-info/top_level.txt +0 -0
  176. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/setup.cfg +0 -0
  177. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-lark/SKILL.md +0 -0
  178. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/SKILL.md +0 -0
  179. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/admin.md +0 -0
  180. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/commands.md +0 -0
  181. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/concepts.md +0 -0
  182. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/faq.md +0 -0
  183. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/feedback-and-files.md +0 -0
  184. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/fork-side-goal.md +0 -0
  185. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/messages.md +0 -0
  186. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/native-capabilities.md +0 -0
  187. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/scheduled-tasks.md +0 -0
  188. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/session-collaboration.md +0 -0
  189. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/status-and-control.md +0 -0
  190. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_account_rate_limits.py +0 -0
  191. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_admin_update_executor_probe.py +0 -0
  192. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_binding_scheduled_channel.py +0 -0
  193. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_bindings.py +0 -0
  194. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_channel_fixtures.py +0 -0
  195. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_channel_input_preparation.py +0 -0
  196. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_chat_avatars.py +0 -0
  197. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_chat_target_cards.py +0 -0
  198. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_check_sdk.py +0 -0
  199. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli.py +0 -0
  200. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_distribution.py +0 -0
  201. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_integration.py +0 -0
  202. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_packages.py +0 -0
  203. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_packaging.py +0 -0
  204. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_services.py +0 -0
  205. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_setup.py +0 -0
  206. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cli_update.py +0 -0
  207. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_codex_runtime.py +0 -0
  208. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_codex_sdk_capabilities.py +0 -0
  209. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_codex_sdk_upgrade_0161.py +0 -0
  210. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_codex_sdk_upgrade_0163.py +0 -0
  211. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_completion_mention_cards.py +0 -0
  212. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_completion_mentions.py +0 -0
  213. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_completion_topic_delivery.py +0 -0
  214. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_cron_chat_targets.py +0 -0
  215. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_defaults_runtime.py +0 -0
  216. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_deployment.py +0 -0
  217. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_documentation_links.py +0 -0
  218. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_domain_experience.py +0 -0
  219. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_error_messages.py +0 -0
  220. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_feishu_app_onboarding.py +0 -0
  221. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_feishu_app_permissions.py +0 -0
  222. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_file_pagination.py +0 -0
  223. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_fork_cards.py +0 -0
  224. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_git_status.py +0 -0
  225. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_goal_reply_separation.py +0 -0
  226. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_image_inputs.py +0 -0
  227. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_lark_app.py +0 -0
  228. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_lark_sdk_contract.py +0 -0
  229. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_main.py +0 -0
  230. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_markdown_images.py +0 -0
  231. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_message_content.py +0 -0
  232. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_message_history.py +0 -0
  233. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_message_material_inputs.py +0 -0
  234. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_message_preparation.py +0 -0
  235. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_model_info_cards.py +0 -0
  236. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_model_settings.py +0 -0
  237. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_name_writes.py +0 -0
  238. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_pagination.py +0 -0
  239. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_partial_answer_delivery.py +0 -0
  240. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_partial_answers.py +0 -0
  241. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_persistent_fork_bindings.py +0 -0
  242. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_persistent_fork_channel.py +0 -0
  243. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_persistent_fork_runtime.py +0 -0
  244. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_child_files.py +0 -0
  245. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_feishu_completion_mention.py +0 -0
  246. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_feishu_partial_answers.py +0 -0
  247. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_feishu_turn_file_card.py +0 -0
  248. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_persistent_fork.py +0 -0
  249. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_python_sdk.py +0 -0
  250. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_scheduled_tasks.py +0 -0
  251. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_sdk_completion_race.py +0 -0
  252. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_sdk_response_recovery.py +0 -0
  253. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_skill_execution.py +0 -0
  254. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_probe_thread_naming.py +0 -0
  255. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_project_delete_channel.py +0 -0
  256. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_project_delete_runtime.py +0 -0
  257. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_project_deletion_storage.py +0 -0
  258. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_prompt_projection.py +0 -0
  259. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_question_cards.py +0 -0
  260. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_question_channel.py +0 -0
  261. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_question_target_parity.py +0 -0
  262. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_quoted_context.py +0 -0
  263. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_release_script.py +0 -0
  264. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_repository_hygiene.py +0 -0
  265. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_result_images.py +0 -0
  266. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_resume_channel.py +0 -0
  267. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_retired_deployment.py +0 -0
  268. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_runtime_resume_activation.py +0 -0
  269. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_runtime_thread_naming.py +0 -0
  270. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_schedule_card_retry.py +0 -0
  271. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_schedule_cards.py +0 -0
  272. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_schedule_mcp.py +0 -0
  273. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_schedule_run_now.py +0 -0
  274. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_schedule_scope_matrix.py +0 -0
  275. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_schedule_service.py +0 -0
  276. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_scheduled_channel.py +0 -0
  277. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_scheduled_runtime.py +0 -0
  278. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_scheduler.py +0 -0
  279. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_scheduler_presenter.py +0 -0
  280. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_schedules_storage.py +0 -0
  281. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_sdk_gap_adapter.py +0 -0
  282. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_service_launcher.py +0 -0
  283. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_card_pages.py +0 -0
  284. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_collaboration_app.py +0 -0
  285. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_external_runtime.py +0 -0
  286. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_id_cards.py +0 -0
  287. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_policy.py +0 -0
  288. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_settings.py +0 -0
  289. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_session_tools.py +0 -0
  290. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_settings_projects.py +0 -0
  291. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_status_cards.py +0 -0
  292. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_status_commands.py +0 -0
  293. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_task_text.py +0 -0
  294. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_terminal_cleanup.py +0 -0
  295. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_thread_history.py +0 -0
  296. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_thread_naming.py +0 -0
  297. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_turn_activity.py +0 -0
  298. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_turn_files.py +0 -0
  299. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_turn_patch_children.py +0 -0
  300. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_turn_patch_statistics.py +0 -0
  301. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_turn_plan_observer.py +0 -0
  302. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_update_executor.py +0 -0
  303. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_usage_card.py +0 -0
  304. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_usage_control.py +0 -0
  305. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_user_guide_skill.py +0 -0
  306. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_user_questions.py +0 -0
  307. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_channel_cwd.py +0 -0
  308. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_git.py +0 -0
  309. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_new.py +0 -0
  310. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_runtime_cwd.py +0 -0
  311. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_service.py +0 -0
  312. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_settings.py +0 -0
  313. {netizen_cli-0.13.1 → netizen_cli-0.13.2}/tests/test_worktree_tools_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netizen-cli
3
- Version: 0.13.1
3
+ Version: 0.13.2
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
@@ -1,3 +1,3 @@
1
1
  """Netizen: a thin Feishu Channel for native Codex Threads."""
2
2
 
3
- __version__ = "0.13.1"
3
+ __version__ = "0.13.2"
@@ -200,11 +200,32 @@ button:disabled { cursor: default; opacity: .48; }
200
200
  .service-restart p { line-height: 1.6; }
201
201
  #updates a { color: var(--blue); }
202
202
  button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px solid #8cb4ff; outline-offset: 2px; }
203
- .login-body { min-height: 100vh; display: grid; place-items: center; padding: 20px; }
204
- .login-card { width: min(420px, 100%); background: white; border: 1px solid var(--line); border-radius: 14px; padding: 28px; }
205
- .login-card label { display: grid; gap: 7px; }
206
- .login-card input { margin-bottom: 14px; }
207
- .login-card button { width: 100%; border: 0; border-radius: 8px; padding: 10px; background: var(--blue); color: white; }
203
+ .login-body { min-height: 100vh; min-height: 100svh; display: grid; place-items: center; padding: clamp(12px, 3vw, 24px); }
204
+ .login-card { width: min(420px, 100%); min-width: 0; margin: 0; padding: clamp(20px, 5vw, 28px); border: 1px solid var(--line); border-radius: 14px; background: var(--paper); box-shadow: 0 8px 28px rgb(23 39 68 / 6%); font-size: 14px; line-height: 1.6; }
205
+ .login-heading h1 { font-size: 28px; line-height: 1.2; }
206
+ .login-heading p { margin: 8px 0 0; color: var(--muted); font-size: 13px; }
207
+ .login-instance { margin: 20px 0; padding: 10px 12px; border-radius: 8px; background: var(--wash); }
208
+ .login-instance > span { color: var(--muted); font-size: 12px; }
209
+ .login-instance .instance-root { margin: 3px 0 0; line-height: 1.5; }
210
+ .login-card .login-button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px; background: var(--paper); color: var(--ink); font-size: 14px; line-height: 22px; font-weight: 500; text-align: center; text-decoration: none; }
211
+ .login-card .login-primary { width: 100%; border-color: var(--blue); background: var(--blue); color: white; }
212
+ .login-card .login-cancel { border-color: transparent; background: transparent; color: var(--muted); }
213
+ .login-card .login-secondary:hover:not(:disabled), .login-card .login-cancel:hover:not(:disabled) { background: var(--wash); }
214
+ .login-card .login-button:focus-visible { outline: 3px solid #8cb4ff; outline-offset: 2px; }
215
+ .login-scan { margin-top: 20px; }
216
+ .login-scan:has(.login-qr[hidden]) { display: none; }
217
+ .login-divider { display: flex; align-items: center; gap: 12px; margin: 0; color: var(--muted); font-size: 12px; }
218
+ .login-divider::before, .login-divider::after { content: ""; flex: 1; border-top: 1px solid var(--line); }
219
+ .login-qr { display: block; width: min(208px, 100%); height: auto; margin: 12px auto 0; }
220
+ .login-user-code { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 8px; margin: 12px 0 0; color: var(--muted); font-size: 12px; }
221
+ #login-code { min-width: 0; border-radius: 6px; padding: 4px 8px; background: var(--wash); color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 16px; letter-spacing: .04em; overflow-wrap: anywhere; }
222
+ .login-deadline { margin: 6px 0 0; color: var(--muted); font-size: 12px; text-align: center; }
223
+ .login-deadline:empty { display: none; }
224
+ .login-card .status { margin: 16px 0 0; font-size: 13px; text-align: center; overflow-wrap: anywhere; }
225
+ .login-actions { display: flex; justify-content: center; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
226
+ .login-actions:not(:has(> :not([hidden]))) { display: none; }
227
+ #login-retry { margin-top: 16px; }
228
+ .login-safety { margin: 20px 0 0; border-top: 1px solid var(--line); padding-top: 14px; color: var(--muted); font-size: 12px; }
208
229
  @media (max-width: 650px) {
209
230
  .topbar { padding: 12px 18px; }
210
231
  .tabs { overflow-x: auto; padding: 10px 14px; }
@@ -428,13 +449,9 @@ body.schedule-detail-open { overflow: hidden; }
428
449
  .identity-panel { max-width: 480px; border: 1px solid var(--line); border-radius: 14px; margin-bottom: 24px; padding: 20px; color: var(--ink); }
429
450
  .identity-panel h3 { margin: 0; }
430
451
  .identity-panel > p { margin: 20px 0; }
431
- .identity-panel-actions, .login-actions { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 10px; }
452
+ .identity-panel-actions { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 10px; }
432
453
  #admin-selector { min-height: 48px; margin: 18px 0; }
433
454
  #admin-user-selection { margin-top: 20px; }
434
- .login-qr { display: block; width: min(240px, 100%); height: auto; margin: 18px auto; }
435
- .login-link { display: inline-block; margin-bottom: 14px; }
436
- .login-actions { justify-content: flex-start; margin: 18px 0; }
437
- #login-code { overflow-wrap: anywhere; }
438
455
  @media (max-width: 600px) {
439
456
  .topbar { align-items: flex-start; gap: 12px; flex-wrap: wrap; }
440
457
  .topbar-brand { flex-basis: 100%; }
@@ -2565,12 +2565,13 @@ function renderDefaultSettings() {
2565
2565
  const model = renderSessionSettingsFields(defaultsInput, settings, editor.models, editor.contextAvailable);
2566
2566
  defaultsInput("context-field").hidden = editor.kind === "chat"
2567
2567
  && chatTarget("defaults").kind() === "p2p" && settings.message_context_mode === "current-only";
2568
+ renderDefaultWorkspace();
2568
2569
  const notes = [editor.loading ? "正在读取配置…" : "", editor.sourceNote, editor.error, editor.catalogError];
2569
2570
  if (turn && !model) notes.push("已保存的模型当前不可用,选择保持不变;可显式选择其他模型。");
2570
2571
  if (editor.contextAvailable === false) notes.push("单聊不支持补齐未读上下文,请使用当前消息。");
2571
2572
  defaultsInput("note").textContent = notes.filter(Boolean).join(" ");
2572
2573
  defaultsInput("note").classList.toggle("error", Boolean(editor.error));
2573
- defaultsInput("save").disabled = defaultsBusy || editor.loading || !editor.action;
2574
+ defaultsInput("save").disabled = defaultsBusy || editor.loading || !editor.action || !defaultWorkspaceAvailable();
2574
2575
  defaultsInput("save").textContent = defaultsBusy ? "保存中…" : "保存";
2575
2576
  defaultsInput("editor").setAttribute("aria-busy", String(defaultsBusy || editor.loading));
2576
2577
  }
@@ -2595,6 +2596,77 @@ function renderDefaultProject(project) {
2595
2596
  ])], project);
2596
2597
  }
2597
2598
 
2599
+ function defaultWorkspaceAvailable() {
2600
+ const editor = defaultsEditor;
2601
+ if (!editor || editor.workspace.mode === "project") return true;
2602
+ const options = editor.workspaceOptions;
2603
+ if (!options?.isGit || options.projectAlias !== defaultsInput("project").value) return false;
2604
+ return editor.workspace.mode === "new"
2605
+ || options.items.some((item) => item.id === editor.workspace.worktree_id);
2606
+ }
2607
+
2608
+ function renderDefaultWorkspace() {
2609
+ const editor = defaultsEditor;
2610
+ const workspace = editor.workspace;
2611
+ const options = editor.workspaceOptions;
2612
+ const available = options?.isGit && options.projectAlias === defaultsInput("project").value;
2613
+ defaultsInput("workspace").value = workspace.mode;
2614
+ for (const option of defaultsInput("workspace").querySelectorAll("option")) {
2615
+ option.disabled = option.value !== "project" && (!available || option.value === "existing" && !options.items.length);
2616
+ }
2617
+ defaultsInput("worktree-field").hidden = workspace.mode !== "existing";
2618
+ defaultsInput("workspace-ref-field").hidden = workspace.mode !== "new";
2619
+ scheduleSelectOptions(defaultsInput("worktree"), [["", "选择受管 Worktree"],
2620
+ ...(available ? options.items : []).map((item) => [item.id, `${item.cwd} · ${item.branch || "detached"} · ${item.id}`])],
2621
+ workspace.worktree_id || "");
2622
+ defaultsInput("worktree").required = workspace.mode === "existing";
2623
+ defaultsInput("workspace-ref").value = workspace.ref || "";
2624
+ defaultsInput("workspace-refs").replaceChildren();
2625
+ for (const branch of options?.branches || []) {
2626
+ const option = document.createElement("option");
2627
+ option.value = branch.ref;
2628
+ option.textContent = branch.name;
2629
+ defaultsInput("workspace-refs").append(option);
2630
+ }
2631
+ const notes = [];
2632
+ if (editor.workspaceError) notes.push(editor.workspaceError);
2633
+ else if (options?.isGit === false) notes.push("该 Project 不是 Git 仓库,只支持项目目录。");
2634
+ else if (!defaultsInput("project").value) notes.push("先选择 Project,再选择工作区策略。");
2635
+ if (workspace.mode === "existing") {
2636
+ notes.push("新会话共享所选 Worktree 的文件和分支;删除或失效后保留此配置,自动创建会话时明确报错。");
2637
+ if (!editor.loading && !defaultWorkspaceAvailable()) notes.push("所选 Worktree 当前不可用,请重新选择;不会切回项目目录。");
2638
+ } else if (workspace.mode === "new") {
2639
+ notes.push("每次自动创建新会话时创建一次 detached Worktree;后续消息复用。留空使用创建时 Project 的 HEAD,保存配置不创建目录。");
2640
+ if (!editor.loading && !available) notes.push("当前无法使用新建 Worktree 策略,请检查 Project。");
2641
+ } else notes.push("新会话使用 Project 的项目目录。");
2642
+ defaultsInput("workspace-help").textContent = notes.join(" ");
2643
+ defaultsInput("workspace-help").classList.toggle("error", !editor.loading && !defaultWorkspaceAvailable());
2644
+ }
2645
+
2646
+ async function changeDefaultProject() {
2647
+ const editor = defaultsEditor;
2648
+ if (!editor || defaultsBusy) return;
2649
+ editor.workspace = { mode: "project" };
2650
+ editor.workspaceOptions = null;
2651
+ editor.workspaceError = "";
2652
+ await loadDefaultOptions();
2653
+ }
2654
+
2655
+ function changeDefaultWorkspace(field) {
2656
+ const editor = defaultsEditor;
2657
+ if (!editor || editor.loading || defaultsBusy) return;
2658
+ if (field === "workspace") {
2659
+ const mode = defaultsInput("workspace").value;
2660
+ editor.workspace = mode === "existing" ? { mode, worktree_id: "" } : { mode };
2661
+ } else if (field === "worktree") {
2662
+ editor.workspace = { mode: "existing", worktree_id: defaultsInput(field).value };
2663
+ } else {
2664
+ const ref = defaultsInput(field).value.trim();
2665
+ editor.workspace = { mode: "new", ...(ref ? { ref } : {}) };
2666
+ }
2667
+ renderDefaultSettings();
2668
+ }
2669
+
2598
2670
  async function loadDefaultOptions(resolveChat = false) {
2599
2671
  const editor = defaultsEditor;
2600
2672
  if (!editor) return;
@@ -2603,6 +2675,8 @@ async function loadDefaultOptions(resolveChat = false) {
2603
2675
  const chatId = editor.kind === "chat" ? defaultsInput("chat").value.trim() : "";
2604
2676
  editor.loading = true;
2605
2677
  editor.error = "";
2678
+ editor.workspaceOptions = null;
2679
+ editor.workspaceError = "";
2606
2680
  renderDefaultSettings();
2607
2681
  try {
2608
2682
  let view = null;
@@ -2614,10 +2688,12 @@ async function loadDefaultOptions(resolveChat = false) {
2614
2688
  const effective = view.exact || view.effective;
2615
2689
  if (effective) {
2616
2690
  editor.settings = structuredClone(effective.session_settings);
2691
+ editor.workspace = structuredClone(effective.workspace);
2617
2692
  renderDefaultProject(effective.project);
2618
2693
  editor.touched = true;
2619
2694
  } else {
2620
2695
  editor.settings = defaultScheduleSessionSettings();
2696
+ editor.workspace = { mode: "project" };
2621
2697
  renderDefaultProject("");
2622
2698
  editor.touched = false;
2623
2699
  }
@@ -2627,10 +2703,14 @@ async function loadDefaultOptions(resolveChat = false) {
2627
2703
  }
2628
2704
  const query = new URLSearchParams({ mode: "options" });
2629
2705
  if (chatId) query.set("chat_id", chatId);
2706
+ const project = defaultsInput("project").value;
2707
+ if (project) query.set("project", project);
2630
2708
  const data = await api(`/api/v1/defaults?${query}`);
2631
2709
  if (defaultsEditor !== editor || serial !== editor.serial) return;
2632
2710
  editor.models = data.models;
2633
2711
  editor.contextAvailable = data.context_mode_available;
2712
+ editor.workspaceOptions = data.workspace_options || null;
2713
+ editor.workspaceError = data.workspace_options_error || "";
2634
2714
  if (editor.kind === "chat") chatTarget("defaults").resolveKind(editor.contextAvailable);
2635
2715
  editor.catalogError = typeof data.model_catalog_error === "string"
2636
2716
  ? data.model_catalog_error : data.model_catalog_error?.message || "";
@@ -2657,6 +2737,7 @@ function openDefaultEditor(kind, record = null) {
2657
2737
  returnFocus: document.activeElement,
2658
2738
  settings: structuredClone(record?.session_settings || defaultScheduleSessionSettings()),
2659
2739
  models: [], serial: 0, loading: true, touched: false, contextAvailable: null,
2740
+ workspace: structuredClone(record ? record.workspace : { mode: "project" }), workspaceOptions: null, workspaceError: "",
2660
2741
  sourceNote: record ? "修改已保存的配置,只影响之后创建的会话。" : "保存后,在没有当前会话的位置收到消息时生效。",
2661
2742
  error: "", catalogError: "", lockedTarget: Boolean(record) };
2662
2743
  defaultsInput("editor").hidden = false;
@@ -2701,7 +2782,7 @@ function defaultEditorValues() {
2701
2782
  if (!editor) return null;
2702
2783
  return { kind: editor.kind, project: defaultsInput("project").value,
2703
2784
  target: defaultsInput(editor.kind === "chat" ? "chat" : "keyword").value,
2704
- settings: sessionSettingsFingerprint(editor.settings) };
2785
+ settings: sessionSettingsFingerprint(editor.settings), workspace: structuredClone(editor.workspace) };
2705
2786
  }
2706
2787
 
2707
2788
  function defaultEditorHasChanges() {
@@ -2752,7 +2833,7 @@ async function defaultMutation(mode, action, extra = {}) {
2752
2833
  async function saveDefault(event) {
2753
2834
  event.preventDefault();
2754
2835
  const editor = defaultsEditor;
2755
- if (!editor || editor.loading || !editor.action || defaultsBusy) return;
2836
+ if (!editor || editor.loading || !editor.action || defaultsBusy || !defaultWorkspaceAvailable()) return;
2756
2837
  if (editor.kind === "chat") {
2757
2838
  editor.loading = true;
2758
2839
  renderDefaultSettings();
@@ -2765,7 +2846,10 @@ async function saveDefault(event) {
2765
2846
  if (defaultsEditor !== editor) return;
2766
2847
  }
2767
2848
  const definition = { kind: editor.kind, project: defaultsInput("project").value,
2768
- session_settings: structuredClone(editor.settings) };
2849
+ session_settings: structuredClone(editor.settings), workspace: structuredClone(editor.workspace) };
2850
+ if (editor.workspaceOptions?.projectAlias === definition.project) {
2851
+ definition.expected_project_revision = editor.workspaceOptions.projectRevision;
2852
+ }
2769
2853
  if (editor.kind === "chat") definition.chat_id = defaultsInput("chat").value.trim();
2770
2854
  else definition.keyword = defaultsInput("keyword").value.trim();
2771
2855
  const action = editor.action;
@@ -2828,7 +2912,7 @@ function defaultChatCell(row, rule) {
2828
2912
  row.append(td);
2829
2913
  }
2830
2914
 
2831
- function defaultSettingsCell(row, settings) {
2915
+ function defaultSettingsCell(row, settings, workspace) {
2832
2916
  const td = document.createElement("td");
2833
2917
  const model = settings.turn_settings;
2834
2918
  const title = document.createElement("div");
@@ -2836,7 +2920,11 @@ function defaultSettingsCell(row, settings) {
2836
2920
  const detail = document.createElement("div");
2837
2921
  detail.className = "default-settings-detail";
2838
2922
  detail.textContent = scheduleSessionSummary(settings).split(" · ").slice(1).join(" · ");
2839
- td.append(title, detail);
2923
+ const location = document.createElement("div");
2924
+ location.className = "default-settings-detail";
2925
+ location.textContent = workspace.mode === "existing" ? `指定 Worktree:${workspace.worktree_id}`
2926
+ : workspace.mode === "new" ? `每个新会话创建 Worktree · ${workspace.ref || "创建时 Project HEAD"}` : "项目目录";
2927
+ td.append(title, detail, location);
2840
2928
  row.append(td);
2841
2929
  }
2842
2930
 
@@ -2863,7 +2951,7 @@ async function loadDefaults(offset = defaultsOffset, returnFocus = null) {
2863
2951
  cell(row, rule.keyword);
2864
2952
  } else defaultChatCell(row, rule);
2865
2953
  cell(row, rule.project);
2866
- defaultSettingsCell(row, rule.session_settings);
2954
+ defaultSettingsCell(row, rule.session_settings, rule.workspace);
2867
2955
  const actions = document.createElement("td");
2868
2956
  const buttons = document.createElement("div");
2869
2957
  buttons.className = "actions";
@@ -4418,6 +4506,11 @@ defaultsInput("drawer").addEventListener("cancel", (event) => {
4418
4506
  });
4419
4507
  defaultsInput("editor").addEventListener("submit", saveDefault);
4420
4508
  defaultsInput("chat").addEventListener("change", () => changeDefaultChat());
4509
+ defaultsInput("project").addEventListener("change", changeDefaultProject);
4510
+ for (const field of ["workspace", "worktree"]) {
4511
+ defaultsInput(field).addEventListener("change", () => changeDefaultWorkspace(field));
4512
+ }
4513
+ defaultsInput("workspace-ref").addEventListener("input", () => changeDefaultWorkspace("workspace-ref"));
4421
4514
  for (const field of ["model", "effort", "tier", "context", "reactions", "progress", "completion-mention"]) {
4422
4515
  defaultsInput(field).addEventListener("change", () => changeDefaultSettings(field));
4423
4516
  }
@@ -271,6 +271,12 @@
271
271
  <h4>会话配置</h4>
272
272
  <div class="schedule-form-grid">
273
273
  <label>Project<select id="defaults-project" required><option value="">选择 Project</option></select></label>
274
+ <label>工作区策略<select id="defaults-workspace"><option value="project">项目目录</option><option value="existing">指定已有 Worktree</option><option value="new">每个新会话创建 Worktree</option></select></label>
275
+ <label id="defaults-worktree-field" hidden>受管 Worktree<select id="defaults-worktree"></select></label>
276
+ <label id="defaults-workspace-ref-field" hidden>起点提交 / 分支<input id="defaults-workspace-ref" list="defaults-workspace-refs" maxlength="256" placeholder="留空使用创建时 Project 的 HEAD"><datalist id="defaults-workspace-refs"></datalist></label>
277
+ </div>
278
+ <p id="defaults-workspace-help" class="defaults-field-help" role="status"></p>
279
+ <div class="schedule-form-grid">
274
280
  <label>模型<select id="defaults-model"><option value="">继承 Codex</option></select></label>
275
281
  <label>推理强度<select id="defaults-effort" disabled></select></label>
276
282
  <label>速度<select id="defaults-tier" disabled></select></label>
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Netizen Admin 登录</title>
7
+ <link rel="stylesheet" href="/static/admin.css">
8
+ <script src="/static/login.js" defer></script>
9
+ </head>
10
+ <body class="login-body">
11
+ <main class="login-card">
12
+ <header class="login-heading">
13
+ <h1>Netizen Admin</h1>
14
+ <p>使用已获授权的飞书管理员账号登录</p>
15
+ </header>
16
+ <div class="login-instance">
17
+ <span>当前实例</span>
18
+ <p class="instance-root">__NETIZEN_INSTANCE_ROOT__</p>
19
+ </div>
20
+ <input id="login-nonce" type="hidden" value="__NETIZEN_LOGIN_NONCE__">
21
+ <input id="login-next" type="hidden" value="__NETIZEN_LOGIN_NEXT__">
22
+ <div id="login-authorization" hidden>
23
+ <a id="login-link" class="login-button login-primary" target="_blank" rel="noopener noreferrer">打开飞书授权页<span aria-hidden="true">↗</span></a>
24
+ <div class="login-scan">
25
+ <p class="login-divider">或使用飞书扫码</p>
26
+ <img id="login-qr" class="login-qr" alt="使用飞书扫描二维码完成本次登录" hidden>
27
+ </div>
28
+ <p class="login-user-code"><span>核对用户码</span><strong id="login-code"></strong></p>
29
+ <p id="login-deadline" class="login-deadline"></p>
30
+ </div>
31
+ <p id="login-status" class="status" role="status" aria-live="polite">正在检查登录状态…</p>
32
+ <div class="login-actions">
33
+ <button id="login-start" class="login-button login-primary" disabled>使用飞书登录</button>
34
+ <button id="login-check" class="login-button login-secondary" hidden>检查登录结果</button>
35
+ <button id="login-cancel" class="login-button login-cancel" hidden>取消登录</button>
36
+ </div>
37
+ <a id="login-retry" class="login-button login-primary" href="/login" hidden>重新开始登录</a>
38
+ <p class="login-safety">只确认你为上述实例主动发起的登录,不要替他人确认登录请求。</p>
39
+ </main>
40
+ </body>
41
+ </html>
@@ -764,7 +764,7 @@ class AdminWebApplication:
764
764
  allowed = {
765
765
  "list": {"mode", "kind", "offset", "limit"},
766
766
  "view": {"mode", "chat_id"},
767
- "options": {"mode", "chat_id"},
767
+ "options": {"mode", "chat_id", "project", "expected_project_revision"},
768
768
  }
769
769
  if mode not in allowed:
770
770
  raise AdminWebError(400, "invalid_query", "默认配置查询类型无效。")
@@ -773,9 +773,9 @@ class AdminWebApplication:
773
773
  for key in allowed[mode] - {"mode"}:
774
774
  value = _optional_text_query(context.query, key)
775
775
  if value is not None:
776
- if key in {"offset", "limit"}:
776
+ if key in {"offset", "limit", "expected_project_revision"}:
777
777
  if not value.isascii() or not value.isdigit():
778
- raise AdminWebError(400, "invalid_query", "分页参数无效。")
778
+ raise AdminWebError(400, "invalid_query", "分页或 Project 版本参数无效。")
779
779
  request[key] = int(value)
780
780
  else:
781
781
  request[key] = value
@@ -817,7 +817,7 @@ class AdminWebApplication:
817
817
  grant = self._redeem_parsed(context, payload, "defaults.save", target, expected_resource=target.resource)
818
818
  definition = payload.get("definition")
819
819
  if not isinstance(definition, dict) or set(definition) - {
820
- "kind", "chat_id", "keyword", "project", "session_settings",
820
+ "kind", "chat_id", "keyword", "project", "expected_project_revision", "workspace", "session_settings",
821
821
  }:
822
822
  raise AdminWebError(400, "invalid_input", "默认配置字段无效。")
823
823
  request: dict[str, object] = {**definition, "mode": "save", "expected_revision": None}
@@ -39,7 +39,7 @@ from .worktrees.models import WorktreeRecord
39
39
  from .worktrees.store import WorktreeStore, create_schema as create_worktree_schema
40
40
 
41
41
 
42
- SCHEMA_VERSION = 15
42
+ SCHEMA_VERSION = 16
43
43
 
44
44
  # Includes the running query and all submitted queries waiting for the reader.
45
45
  _ADMIN_QUERY_CAPACITY = 32
@@ -466,7 +466,7 @@ def _require_current_schema(connection: sqlite3.Connection) -> None:
466
466
  _require_project_metadata_schema(connection)
467
467
  require_schema(connection)
468
468
  require_defaults_schema(connection)
469
- from .migrations.v15 import validate
469
+ from .migrations.v16 import validate
470
470
  validate(connection)
471
471
 
472
472
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import re
7
- from collections.abc import Callable, Mapping
7
+ from collections.abc import Callable, Mapping, Sequence
8
8
  from dataclasses import dataclass
9
9
  from datetime import datetime
10
10
  from typing import Any
@@ -350,7 +350,7 @@ def _new_binding_form(
350
350
  ),
351
351
  None,
352
352
  )
353
- elements = [_new_form_section("项目", [
353
+ elements = [_form_section("项目", [
354
354
  _static_select(
355
355
  name="new_project",
356
356
  placeholder="选择项目",
@@ -393,31 +393,15 @@ def _new_binding_form(
393
393
  model_value_encoder=_new_model_reference,
394
394
  inherit_initial_when_unset=False,
395
395
  )
396
- _restore_new_settings_draft(model_fields, settings_draft)
397
- model_elements = [model_fields[1]]
398
- if len(model_fields) > 2:
399
- model_elements.append(_new_form_columns(
400
- (1, model_fields[2:4]), (1, model_fields[4:6]),
401
- ))
402
- elements.append(_new_form_section("模型配置", model_elements))
403
- if allow_context_mode:
404
- context = _context_mode_form_elements(prefix="new", initial_mode=message_context_mode)
405
- context[-1] = _form_hint(
406
- "机器人始终只响应 @。选择“自动带上”会把两次 @ 之间未 @ 机器人的群聊消息交给 Codex。"
407
- )
408
- _restore_new_settings_draft(context, settings_draft)
409
- elements.append(_new_form_section("消息上下文", context))
410
- feedback = _task_feedback_form_elements(prefix="new", initial=task_feedback)
411
- _restore_new_settings_draft(feedback, settings_draft)
412
- feedback_hints = (
413
- "执行时闪烁;关闭仍保留状态表情。部分移动端会显示为消息。",
414
- "展示执行状态与清单,结束后自动折叠。",
415
- "完成或失败时 @ 发起人;主动停止、定时自动首轮不提醒。",
396
+ context = (
397
+ _context_mode_form_elements(prefix="new", initial_mode=message_context_mode)
398
+ if allow_context_mode else []
416
399
  )
417
- elements.append(_new_form_section("任务反馈", [
418
- _new_form_columns((3, [label, _form_hint(hint)]), (1, [select]))
419
- for label, select, hint in zip(feedback[::3], feedback[1::3], feedback_hints, strict=True)
420
- ]))
400
+ feedback = _task_feedback_form_elements(prefix="new", initial=task_feedback)
401
+ _restore_settings_draft([*model_fields, *context, *feedback], settings_draft)
402
+ elements.extend(_session_settings_sections(
403
+ model_fields=model_fields, context_fields=context, feedback_fields=feedback,
404
+ ))
421
405
  if allow_workspace_selection:
422
406
  workspace_elements = [
423
407
  _form_hint("使用上方选择的项目加载目录选项;不会创建会话。切换项目后请重新加载。"),
@@ -434,7 +418,7 @@ def _new_binding_form(
434
418
  ))
435
419
  if workspace_options is not None and workspace_options.get("isGit") is True:
436
420
  workspace_elements.extend(_new_workspace_elements(workspace_options))
437
- elements.append(_new_form_section("工作区", workspace_elements))
421
+ elements.append(_form_section("工作区", workspace_elements))
438
422
  elements.append(
439
423
  _form_submit_button(name="new_binding_submit_v6", label="新建会话")
440
424
  )
@@ -446,7 +430,7 @@ def _new_binding_form(
446
430
  }
447
431
 
448
432
 
449
- def _restore_new_settings_draft(
433
+ def _restore_settings_draft(
450
434
  elements: list[dict[str, Any]], draft: Mapping[str, str] | None,
451
435
  ) -> None:
452
436
  """Retain this form submission only; never silently replace an unavailable choice."""
@@ -461,8 +445,8 @@ def _restore_new_settings_draft(
461
445
  element["initial_option"] = value
462
446
 
463
447
 
464
- def _new_form_columns(*columns: tuple[int, list[dict[str, Any]]]) -> dict[str, Any]:
465
- """Arrange existing /new controls without changing their submitted values."""
448
+ def _form_columns(*columns: tuple[int, list[dict[str, Any]]]) -> dict[str, Any]:
449
+ """Arrange existing controls without changing their submitted values."""
466
450
  rendered = []
467
451
  for weight, elements in columns:
468
452
  styled = []
@@ -482,8 +466,41 @@ def _new_form_columns(*columns: tuple[int, list[dict[str, Any]]]) -> dict[str, A
482
466
  }
483
467
 
484
468
 
485
- def _new_form_section(title: str, elements: list[dict[str, Any]]) -> dict[str, Any]:
486
- return _new_form_columns((1, [{"tag": "markdown", "content": f"**{title}**"}, *elements]))
469
+ def _form_section(title: str, elements: list[dict[str, Any]]) -> dict[str, Any]:
470
+ return _form_columns((1, [{"tag": "markdown", "content": f"**{title}**"}, *elements]))
471
+
472
+
473
+ def _session_settings_sections(
474
+ *,
475
+ model_fields: list[dict[str, Any]],
476
+ context_fields: list[dict[str, Any]],
477
+ feedback_fields: list[dict[str, Any]],
478
+ model_notices: Sequence[dict[str, Any]] = (),
479
+ ) -> list[dict[str, Any]]:
480
+ """Share presentation while each caller owns field values and validation."""
481
+ model_elements = [*model_notices, model_fields[1]]
482
+ if len(model_fields) > 2:
483
+ model_elements.append(_form_columns(
484
+ (1, model_fields[2:4]), (1, model_fields[4:6]),
485
+ ))
486
+ sections = [_form_section("模型配置", model_elements)]
487
+ if context_fields:
488
+ sections.append(_form_section("消息上下文", [
489
+ *context_fields[:-1],
490
+ _form_hint("机器人始终只响应 @。选择“自动带上”会把两次 @ 之间未 @ 机器人的群聊消息交给 Codex。"),
491
+ ]))
492
+ feedback_hints = (
493
+ "执行时闪烁;关闭仍保留状态表情。部分移动端会显示为消息。",
494
+ "展示执行状态与清单,结束后自动折叠。",
495
+ "完成或失败时 @ 发起人;主动停止、定时自动首轮不提醒。",
496
+ )
497
+ sections.append(_form_section("任务反馈", [
498
+ _form_columns((3, [label, _form_hint(hint)]), (1, [select]))
499
+ for label, select, hint in zip(
500
+ feedback_fields[::3], feedback_fields[1::3], feedback_hints, strict=True,
501
+ )
502
+ ]))
503
+ return sections
487
504
 
488
505
 
489
506
  def _workspace_reference(options: Mapping[str, Any], choice: str) -> str:
@@ -567,38 +584,31 @@ def _binding_config_form(
567
584
  model_id=model_id,
568
585
  )
569
586
 
570
- elements: list[dict[str, Any]] = []
571
587
  if catalog is None:
572
- elements.extend(
573
- [
574
- _form_label("Model"),
575
- _static_select(
576
- name="config_model",
577
- placeholder="选择 Model 来源",
578
- options=(("继承 Codex", model_reference(None)),),
579
- initial_option=model_reference(None),
580
- ),
581
- ]
582
- )
588
+ model_fields = [
589
+ _form_label("Model"),
590
+ _static_select(
591
+ name="config_model",
592
+ placeholder="选择 Model 来源",
593
+ options=(("继承 Codex", model_reference(None)),),
594
+ initial_option=model_reference(None),
595
+ ),
596
+ ]
583
597
  else:
584
- elements.extend(
585
- _model_settings_form_elements(
586
- prefix="config",
587
- catalog=catalog,
588
- turn_settings=turn_settings,
589
- model_value_encoder=model_reference,
590
- inherit_initial_when_unset=True,
591
- )
592
- )
593
- if allow_context_mode:
594
- elements.extend(
595
- _context_mode_form_elements(
596
- prefix="config",
597
- initial_mode=message_context_mode,
598
- )
598
+ model_fields = _model_settings_form_elements(
599
+ prefix="config",
600
+ catalog=catalog,
601
+ turn_settings=turn_settings,
602
+ model_value_encoder=model_reference,
603
+ inherit_initial_when_unset=True,
599
604
  )
600
- elements.extend(
601
- _task_feedback_form_elements(prefix="config", initial=task_feedback)
605
+ elements = _session_settings_sections(
606
+ model_fields=model_fields,
607
+ context_fields=(
608
+ _context_mode_form_elements(prefix="config", initial_mode=message_context_mode)
609
+ if allow_context_mode else []
610
+ ),
611
+ feedback_fields=_task_feedback_form_elements(prefix="config", initial=task_feedback),
602
612
  )
603
613
  elements.append(
604
614
  _form_submit_button(
@@ -609,6 +619,7 @@ def _binding_config_form(
609
619
  return {
610
620
  "tag": "form",
611
621
  "name": "binding_config_v6",
622
+ "vertical_spacing": "12px",
612
623
  "elements": elements,
613
624
  }
614
625
 
@@ -778,6 +789,8 @@ def session_settings_form_elements(
778
789
  catalog: ModelCatalog | None,
779
790
  catalog_error: str | None = None,
780
791
  allow_context_mode: bool,
792
+ grouped: bool = False,
793
+ settings_draft: Mapping[str, str] | None = None,
781
794
  ) -> list[dict[str, Any]]:
782
795
  """Use ordinary controls while preserving unavailable explicit intent."""
783
796
  usable_catalog = catalog
@@ -787,34 +800,43 @@ def session_settings_form_elements(
787
800
  except ValueError:
788
801
  usable_catalog = None
789
802
  catalog_error = "已有 Model / Effort / Speed 暂不可用,当前选择完整保留。"
790
- elements: list[dict[str, Any]] = []
803
+ model_notices: list[dict[str, Any]] = []
791
804
  if usable_catalog is not None:
792
- elements.extend(_model_settings_form_elements(
805
+ model_fields = _model_settings_form_elements(
793
806
  prefix=prefix, catalog=usable_catalog, turn_settings=settings.turn_settings,
794
807
  model_value_encoder=_new_model_reference, inherit_initial_when_unset=True,
795
- ))
808
+ )
796
809
  else:
797
- elements.append(_notice(catalog_error or "模型目录暂不可用;可以保留原配置或明确选择继承 Codex。"))
810
+ model_notices.append(_notice(catalog_error or "模型目录暂不可用;可以保留原配置或明确选择继承 Codex。"))
798
811
  options = [("继承 Codex", _new_model_reference(None))]
799
812
  turn = settings.turn_settings
800
813
  if turn is not None:
801
814
  options.append(("保留已有显式配置", _new_model_reference(turn.model_id)))
802
- elements.extend([
815
+ model_fields = [
803
816
  _form_label("Model"),
804
817
  _static_select(name=f"{prefix}_model", placeholder="选择 Model 来源", options=tuple(options),
805
818
  initial_option=_new_model_reference(turn.model_id if turn else None)),
806
- ])
819
+ ]
807
820
  if turn is not None:
808
821
  for field, label, value in (("effort", "Effort", turn.effort_id), ("speed", "Speed", turn.service_tier_id)):
809
- elements.extend([
822
+ model_fields.extend([
810
823
  _form_label(label),
811
824
  _static_select(name=f"{prefix}_{field}", placeholder=label,
812
825
  options=((value, value),), initial_option=value),
813
826
  ])
814
- if allow_context_mode:
815
- elements.extend(_context_mode_form_elements(prefix=prefix, initial_mode=settings.message_context_mode))
816
- elements.extend(_task_feedback_form_elements(prefix=prefix, initial=settings.task_feedback))
817
- return elements
827
+ context = (
828
+ _context_mode_form_elements(prefix=prefix, initial_mode=settings.message_context_mode)
829
+ if allow_context_mode else []
830
+ )
831
+ feedback = _task_feedback_form_elements(prefix=prefix, initial=settings.task_feedback)
832
+ fields = [*model_fields, *context, *feedback]
833
+ _restore_settings_draft(fields, settings_draft)
834
+ if grouped:
835
+ return _session_settings_sections(
836
+ model_fields=model_fields, context_fields=context, feedback_fields=feedback,
837
+ model_notices=model_notices,
838
+ )
839
+ return [*model_notices, *fields]
818
840
 
819
841
 
820
842
  def session_settings_summary(settings: SessionSettings, *, allow_context_mode: bool = True) -> str: