netizen-cli 0.13.0__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 (314) hide show
  1. {netizen_cli-0.13.0/netizen_cli.egg-info → netizen_cli-0.13.2}/PKG-INFO +1 -1
  2. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/__init__.py +1 -1
  3. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/admin.css +69 -18
  4. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/admin.js +316 -22
  5. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/identity.js +18 -5
  6. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/index.html +114 -67
  7. netizen_cli-0.13.2/netizen_cli/admin/static/login.html +41 -0
  8. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/session-access.js +6 -1
  9. netizen_cli-0.13.2/netizen_cli/admin/static/worktrees.js +365 -0
  10. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/web.py +4 -4
  11. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/bindings.py +2 -2
  12. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/controls.py +170 -112
  13. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/defaults.py +166 -14
  14. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel_app.py +56 -13
  15. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/database_migrations.py +5 -1
  16. netizen_cli-0.13.2/netizen_cli/defaults/models.py +85 -0
  17. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/defaults/service.py +76 -9
  18. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/defaults/store.py +32 -4
  19. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/domain.py +1 -0
  20. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/management/service.py +68 -1
  21. netizen_cli-0.13.2/netizen_cli/migrations/v16.py +104 -0
  22. {netizen_cli-0.13.0 → netizen_cli-0.13.2/netizen_cli.egg-info}/PKG-INFO +1 -1
  23. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli.egg-info/SOURCES.txt +3 -0
  24. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/pyproject.toml +1 -1
  25. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/admin.md +14 -2
  26. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/sessions.md +28 -9
  27. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/user-guide.md +2 -0
  28. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cards.py +41 -5
  29. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_channel_app.py +1 -1
  30. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_data.py +8 -6
  31. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_database_migrations.py +4 -4
  32. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_defaults_cards.py +163 -5
  33. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_defaults_channel.py +120 -0
  34. netizen_cli-0.13.2/tests/test_defaults_worktree_channel.py +374 -0
  35. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_migration_schema_validation.py +1 -1
  36. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_defaults.py +153 -0
  37. netizen_cli-0.13.2/tests/test_session_defaults_migration.py +106 -0
  38. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_worktree_new.py +121 -18
  39. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_worktree_storage.py +1 -1
  40. netizen_cli-0.13.0/netizen_cli/admin/static/login.html +0 -30
  41. netizen_cli-0.13.0/netizen_cli/admin/static/worktrees.js +0 -227
  42. netizen_cli-0.13.0/netizen_cli/defaults/models.py +0 -35
  43. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/MANIFEST.in +0 -0
  44. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/README.md +0 -0
  45. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/_build.py +0 -0
  46. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/config.example.yaml +0 -0
  47. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/__main__.py +0 -0
  48. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/account_rate_limits.py +0 -0
  49. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/__init__.py +0 -0
  50. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/auth.py +0 -0
  51. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/credentials.py +0 -0
  52. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/errors.py +0 -0
  53. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/feishu.py +0 -0
  54. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/login.py +0 -0
  55. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/port_config.py +0 -0
  56. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/presentation.py +0 -0
  57. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/queries.py +0 -0
  58. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/chat-picker.css +0 -0
  59. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/chat-picker.js +0 -0
  60. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/static/login.js +0 -0
  61. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/transport.py +0 -0
  62. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/admin/users.py +0 -0
  63. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/builtin_skills.py +0 -0
  64. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/__init__.py +0 -0
  65. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/callbacks.py +0 -0
  66. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/chat_target.py +0 -0
  67. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/fork.py +0 -0
  68. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/model_info.py +0 -0
  69. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/pagination.py +0 -0
  70. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/questions.py +0 -0
  71. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/reply.py +0 -0
  72. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/scheduled.py +0 -0
  73. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/session_ids.py +0 -0
  74. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/status.py +0 -0
  75. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cards/usage.py +0 -0
  76. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/__init__.py +0 -0
  77. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/completion_mentions.py +0 -0
  78. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/input_preparation.py +0 -0
  79. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/messages.py +0 -0
  80. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/ports.py +0 -0
  81. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/question_inputs.py +0 -0
  82. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/reactions.py +0 -0
  83. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/reply_presenter.py +0 -0
  84. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/session_pages.py +0 -0
  85. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/channel/topics.py +0 -0
  86. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/chat_avatars.py +0 -0
  87. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/chat_targets.py +0 -0
  88. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli.py +0 -0
  89. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli_data.py +0 -0
  90. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli_packages.py +0 -0
  91. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli_services.py +0 -0
  92. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli_setup.py +0 -0
  93. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli_update.py +0 -0
  94. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli_update_restore.py +0 -0
  95. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/cli_update_worker.py +0 -0
  96. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/codex_runtime.py +0 -0
  97. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/completion_mention.py +0 -0
  98. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/defaults/__init__.py +0 -0
  99. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/deployment/__init__.py +0 -0
  100. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/deployment/restart_worker.py +0 -0
  101. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/deployment/update_executor.py +0 -0
  102. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/deployment/update_protocol.py +0 -0
  103. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/error_messages.py +0 -0
  104. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/experience.py +0 -0
  105. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/feishu_app_onboarding.py +0 -0
  106. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/feishu_app_permissions.py +0 -0
  107. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/feishu_links.py +0 -0
  108. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/git_status.py +0 -0
  109. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/image_inputs.py +0 -0
  110. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/instance.py +0 -0
  111. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/lark_app.py +0 -0
  112. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/main.py +0 -0
  113. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/management/__init__.py +0 -0
  114. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/management/blocking_io.py +0 -0
  115. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/management/chat_directory.py +0 -0
  116. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/management/chat_labels.py +0 -0
  117. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/management/coordination.py +0 -0
  118. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/management/updates.py +0 -0
  119. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/markdown_images.py +0 -0
  120. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/mcp_transport.py +0 -0
  121. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/message_content.py +0 -0
  122. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/message_history.py +0 -0
  123. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/message_preparation.py +0 -0
  124. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/message_projection.py +0 -0
  125. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/migrations/__init__.py +0 -0
  126. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/migrations/schema.py +0 -0
  127. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/migrations/v14.py +0 -0
  128. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/migrations/v15.py +0 -0
  129. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/model_settings.py +0 -0
  130. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/package_resources.py +0 -0
  131. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/pagination.py +0 -0
  132. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/partial_answers.py +0 -0
  133. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/projects.py +0 -0
  134. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/prompt_projection.py +0 -0
  135. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/quoted_context.py +0 -0
  136. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/result_images.py +0 -0
  137. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/runtime/__init__.py +0 -0
  138. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/runtime/contracts.py +0 -0
  139. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/runtime/name_writes.py +0 -0
  140. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/runtime/thread_naming.py +0 -0
  141. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/schedules/__init__.py +0 -0
  142. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/schedules/mcp.py +0 -0
  143. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/schedules/models.py +0 -0
  144. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/schedules/scheduler.py +0 -0
  145. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/schedules/service.py +0 -0
  146. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/schedules/store.py +0 -0
  147. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sdk_gap_adapter.py +0 -0
  148. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/service_launcher.py +0 -0
  149. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/session_inventory.py +0 -0
  150. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/session_settings.py +0 -0
  151. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/__init__.py +0 -0
  152. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/delegation.py +0 -0
  153. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/errors.py +0 -0
  154. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/mcp.py +0 -0
  155. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/policy.py +0 -0
  156. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/presentation.py +0 -0
  157. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/schema.py +0 -0
  158. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/sessions/service.py +0 -0
  159. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/settings.py +0 -0
  160. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/skill_references.py +0 -0
  161. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/task_text.py +0 -0
  162. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/terminal_cleanup.py +0 -0
  163. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/thread_history.py +0 -0
  164. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/turn_activity.py +0 -0
  165. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/turn_files.py +0 -0
  166. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/turn_patch_children.py +0 -0
  167. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/turn_plan_observer.py +0 -0
  168. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/user_questions.py +0 -0
  169. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/worktrees/errors.py +0 -0
  170. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/worktrees/git.py +0 -0
  171. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/worktrees/models.py +0 -0
  172. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/worktrees/service.py +0 -0
  173. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/worktrees/settings.py +0 -0
  174. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli/worktrees/store.py +0 -0
  175. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli.egg-info/dependency_links.txt +0 -0
  176. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli.egg-info/entry_points.txt +0 -0
  177. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli.egg-info/requires.txt +0 -0
  178. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/netizen_cli.egg-info/top_level.txt +0 -0
  179. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/setup.cfg +0 -0
  180. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-lark/SKILL.md +0 -0
  181. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/SKILL.md +0 -0
  182. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/commands.md +0 -0
  183. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/concepts.md +0 -0
  184. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/faq.md +0 -0
  185. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/feedback-and-files.md +0 -0
  186. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/fork-side-goal.md +0 -0
  187. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/messages.md +0 -0
  188. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/native-capabilities.md +0 -0
  189. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/scheduled-tasks.md +0 -0
  190. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/session-collaboration.md +0 -0
  191. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/skills/netizen-user-guide/references/status-and-control.md +0 -0
  192. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_account_rate_limits.py +0 -0
  193. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_admin_update_executor_probe.py +0 -0
  194. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_binding_scheduled_channel.py +0 -0
  195. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_bindings.py +0 -0
  196. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_channel_fixtures.py +0 -0
  197. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_channel_input_preparation.py +0 -0
  198. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_chat_avatars.py +0 -0
  199. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_chat_target_cards.py +0 -0
  200. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_check_sdk.py +0 -0
  201. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli.py +0 -0
  202. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_distribution.py +0 -0
  203. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_integration.py +0 -0
  204. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_packages.py +0 -0
  205. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_packaging.py +0 -0
  206. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_services.py +0 -0
  207. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_setup.py +0 -0
  208. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cli_update.py +0 -0
  209. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_codex_runtime.py +0 -0
  210. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_codex_sdk_capabilities.py +0 -0
  211. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_codex_sdk_upgrade_0161.py +0 -0
  212. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_codex_sdk_upgrade_0163.py +0 -0
  213. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_completion_mention_cards.py +0 -0
  214. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_completion_mentions.py +0 -0
  215. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_completion_topic_delivery.py +0 -0
  216. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_cron_chat_targets.py +0 -0
  217. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_defaults_runtime.py +0 -0
  218. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_deployment.py +0 -0
  219. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_documentation_links.py +0 -0
  220. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_domain_experience.py +0 -0
  221. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_error_messages.py +0 -0
  222. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_feishu_app_onboarding.py +0 -0
  223. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_feishu_app_permissions.py +0 -0
  224. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_file_pagination.py +0 -0
  225. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_fork_cards.py +0 -0
  226. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_git_status.py +0 -0
  227. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_goal_reply_separation.py +0 -0
  228. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_image_inputs.py +0 -0
  229. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_lark_app.py +0 -0
  230. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_lark_sdk_contract.py +0 -0
  231. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_main.py +0 -0
  232. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_markdown_images.py +0 -0
  233. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_message_content.py +0 -0
  234. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_message_history.py +0 -0
  235. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_message_material_inputs.py +0 -0
  236. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_message_preparation.py +0 -0
  237. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_model_info_cards.py +0 -0
  238. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_model_settings.py +0 -0
  239. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_name_writes.py +0 -0
  240. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_pagination.py +0 -0
  241. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_partial_answer_delivery.py +0 -0
  242. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_partial_answers.py +0 -0
  243. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_persistent_fork_bindings.py +0 -0
  244. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_persistent_fork_channel.py +0 -0
  245. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_persistent_fork_runtime.py +0 -0
  246. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_child_files.py +0 -0
  247. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_feishu_completion_mention.py +0 -0
  248. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_feishu_partial_answers.py +0 -0
  249. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_feishu_turn_file_card.py +0 -0
  250. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_persistent_fork.py +0 -0
  251. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_python_sdk.py +0 -0
  252. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_scheduled_tasks.py +0 -0
  253. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_sdk_completion_race.py +0 -0
  254. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_sdk_response_recovery.py +0 -0
  255. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_skill_execution.py +0 -0
  256. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_probe_thread_naming.py +0 -0
  257. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_project_delete_channel.py +0 -0
  258. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_project_delete_runtime.py +0 -0
  259. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_project_deletion_storage.py +0 -0
  260. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_prompt_projection.py +0 -0
  261. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_question_cards.py +0 -0
  262. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_question_channel.py +0 -0
  263. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_question_target_parity.py +0 -0
  264. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_quoted_context.py +0 -0
  265. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_release_script.py +0 -0
  266. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_repository_hygiene.py +0 -0
  267. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_result_images.py +0 -0
  268. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_resume_channel.py +0 -0
  269. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_retired_deployment.py +0 -0
  270. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_runtime_resume_activation.py +0 -0
  271. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_runtime_thread_naming.py +0 -0
  272. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_schedule_card_retry.py +0 -0
  273. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_schedule_cards.py +0 -0
  274. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_schedule_mcp.py +0 -0
  275. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_schedule_run_now.py +0 -0
  276. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_schedule_scope_matrix.py +0 -0
  277. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_schedule_service.py +0 -0
  278. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_scheduled_channel.py +0 -0
  279. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_scheduled_runtime.py +0 -0
  280. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_scheduler.py +0 -0
  281. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_scheduler_presenter.py +0 -0
  282. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_schedules_storage.py +0 -0
  283. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_sdk_gap_adapter.py +0 -0
  284. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_service_launcher.py +0 -0
  285. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_card_pages.py +0 -0
  286. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_collaboration_app.py +0 -0
  287. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_external_runtime.py +0 -0
  288. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_id_cards.py +0 -0
  289. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_policy.py +0 -0
  290. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_settings.py +0 -0
  291. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_session_tools.py +0 -0
  292. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_settings_projects.py +0 -0
  293. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_status_cards.py +0 -0
  294. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_status_commands.py +0 -0
  295. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_task_text.py +0 -0
  296. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_terminal_cleanup.py +0 -0
  297. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_thread_history.py +0 -0
  298. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_thread_naming.py +0 -0
  299. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_turn_activity.py +0 -0
  300. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_turn_files.py +0 -0
  301. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_turn_patch_children.py +0 -0
  302. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_turn_patch_statistics.py +0 -0
  303. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_turn_plan_observer.py +0 -0
  304. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_update_executor.py +0 -0
  305. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_usage_card.py +0 -0
  306. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_usage_control.py +0 -0
  307. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_user_guide_skill.py +0 -0
  308. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_user_questions.py +0 -0
  309. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_worktree_channel_cwd.py +0 -0
  310. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_worktree_git.py +0 -0
  311. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_worktree_runtime_cwd.py +0 -0
  312. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_worktree_service.py +0 -0
  313. {netizen_cli-0.13.0 → netizen_cli-0.13.2}/tests/test_worktree_settings.py +0 -0
  314. {netizen_cli-0.13.0 → 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.0
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.0"
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; }
@@ -220,7 +241,7 @@ button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px s
220
241
  .time-range-fields { grid-template-columns: 1fr; }
221
242
  }
222
243
  .schedule-help { margin-top: 18px; color: var(--muted); }
223
- body.schedule-drawer-open, body.defaults-drawer-open, body.session-settings-drawer-open { overflow: hidden; }
244
+ body.schedule-drawer-open, body.defaults-drawer-open, body.session-settings-drawer-open, body.workspace-drawer-open { overflow: hidden; }
224
245
  .editor-drawer { position: fixed; inset: 0 0 0 auto; margin: 0; padding: 0; width: min(720px, 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh; border: 0; color: var(--ink); background: var(--paper); box-shadow: -12px 0 40px rgb(23 39 68 / 16%); }
225
246
  .editor-drawer::backdrop { background: rgb(16 33 63 / 35%); }
226
247
  #schedule-editor { height: 100%; display: flex; flex-direction: column; border: 0; border-radius: 0; padding: 0; margin: 0; }
@@ -329,6 +350,19 @@ body.schedule-detail-open { overflow: hidden; }
329
350
  .drawer-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 24px; border-top: 1px solid var(--line); }
330
351
  .drawer-form .drawer-cancel { color: var(--ink); background: white; border: 1px solid var(--line); }
331
352
  .drawer-form input { margin: 0; }
353
+ .workspace-form[hidden], .workspace-form [hidden] { display: none; }
354
+ .workspace-fields { display: grid; gap: 18px; }
355
+ .workspace-fields .hint { margin: -10px 0 0; }
356
+ .workspace-field-label { margin: 0 0 8px; color: var(--muted); font-size: 13px; }
357
+ .workspace-modes { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px; border-radius: 9px; background: var(--wash); }
358
+ .workspace-form .workspace-modes button { flex: 1; background: transparent; color: var(--muted); }
359
+ .workspace-form .workspace-modes button[aria-pressed="true"] { background: var(--paper); color: var(--blue); box-shadow: 0 1px 4px rgb(23 39 68 / 12%); font-weight: 600; }
360
+ .workspace-form .workspace-modes button:focus-visible { outline: 3px solid #8cb4ff; outline-offset: 2px; }
361
+ .workspace-form .drawer-body > .hint { margin-top: 12px; }
362
+ .workspace-form .status { margin-top: 24px; white-space: pre-line; overflow-wrap: anywhere; }
363
+ .workspace-form .cascade-choice input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; }
364
+ .workspace-form .target-summary, #worktree-effective-root { overflow-wrap: anywhere; }
365
+ #projects .section-head, #worktrees .section-head { flex-wrap: wrap; }
332
366
  #defaults [hidden], #defaults-editor[hidden], #defaults-editor [hidden] { display: none; }
333
367
  .defaults-help, .defaults-order-help, .defaults-field-help { color: var(--muted); font-size: 13px; line-height: 1.6; }
334
368
  .defaults-help { margin: 12px 0 24px; }
@@ -382,25 +416,42 @@ body.schedule-detail-open { overflow: hidden; }
382
416
  .person-avatar[hidden], .person-placeholder[hidden] { display: none; }
383
417
  .auth-expired { display: flex; align-items: center; justify-content: center; gap: 18px; padding: 18px; background: #fff2df; color: #7b4400; }
384
418
  .auth-expired[hidden], .person[hidden], .identity-panel[hidden], .identity-panel [hidden], .login-card [hidden] { display: none; }
385
- .system-tabs { width: fit-content; margin: 22px 0; }
419
+ .system-tabs { overflow-x: auto; }
420
+ .system-tabs .session-tab { flex: 0 0 auto; min-height: 44px; }
421
+ .system-tab-panel { padding-top: 20px; }
422
+ .tab-unsaved { display: inline-block; margin-left: 6px; padding: 2px 6px; border-radius: 4px; background: #fff3d6; color: #875800; font-size: 12px; font-weight: 400; }
423
+ .tab-unsaved[hidden] { display: none; }
386
424
  .admin-users { padding: 0; margin: 0; list-style: none; }
387
425
  .admin-users > li { display: flex; justify-content: space-between; align-items: center; gap: 16px; border-top: 1px solid var(--line); padding: 16px 0; }
388
- .session-access { margin-top: 32px; padding-top: 18px; border-top: 1px solid var(--line); }
389
- .session-access-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: 24px; }
390
- .session-access-grid h4 { margin: 0 0 8px; }
391
- .session-access-grid p { min-height: 48px; color: var(--muted); line-height: 1.6; }
392
- .session-access-grid textarea { width: 100%; resize: vertical; font-family: monospace; }
393
- .session-access-grid [hidden] { display: none; }
426
+ .session-access-form { border: 0; padding: 0; }
427
+ #session-access-fields { min-width: 0; margin: 0; border: 0; padding: 0; }
428
+ .session-access-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); align-items: start; gap: 16px; }
429
+ .session-access-rule { min-width: 0; border: 1px solid var(--line); border-radius: 10px; padding: 20px; }
430
+ .session-access-rule h4 { margin: 0; font-size: 16px; }
431
+ .session-access-description { margin: 6px 0 20px; color: var(--muted); font-size: 13px; line-height: 1.6; }
432
+ .session-access-targets { display: grid; gap: 16px; margin-top: 16px; }
433
+ .session-access-rule-note { margin: 16px 0 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
434
+ .session-access-rule textarea { width: 100%; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; line-height: 1.6; }
435
+ .session-access-rule .chat-picker-popup { width: 100%; max-width: 100%; }
436
+ .session-access-rule [hidden] { display: none; }
437
+ .session-access-help { margin-top: 16px; padding: 14px 16px; border-radius: 8px; background: var(--wash); color: var(--muted); font-size: 13px; line-height: 1.6; }
438
+ .session-access-help h4 { margin: 0 0 6px; color: var(--ink); font-size: inherit; font-weight: 600; }
439
+ .session-access-help p { margin: 4px 0 0; }
440
+ .session-access-footer { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line); }
441
+ .session-access-footer .defaults-field-help { flex: 1 1 240px; margin: 0; }
442
+ .session-access-footer .actions { flex: 0 0 auto; gap: 8px; margin-left: auto; }
443
+ .session-access-footer button { min-width: 96px; min-height: 40px; border: 1px solid var(--blue); padding: 9px 14px; font-size: var(--control-font-size); line-height: var(--control-line-height); font-weight: 500; }
444
+ .session-access-footer .refresh { border-color: var(--line); background: var(--paper); color: var(--ink); }
445
+ @media (max-width: 650px) {
446
+ .session-access-rule { padding: 16px; }
447
+ .session-access-footer button { min-height: 44px; }
448
+ }
394
449
  .identity-panel { max-width: 480px; border: 1px solid var(--line); border-radius: 14px; margin-bottom: 24px; padding: 20px; color: var(--ink); }
395
450
  .identity-panel h3 { margin: 0; }
396
451
  .identity-panel > p { margin: 20px 0; }
397
- .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; }
398
453
  #admin-selector { min-height: 48px; margin: 18px 0; }
399
454
  #admin-user-selection { margin-top: 20px; }
400
- .login-qr { display: block; width: min(240px, 100%); height: auto; margin: 18px auto; }
401
- .login-link { display: inline-block; margin-bottom: 14px; }
402
- .login-actions { justify-content: flex-start; margin: 18px 0; }
403
- #login-code { overflow-wrap: anywhere; }
404
455
  @media (max-width: 600px) {
405
456
  .topbar { align-items: flex-start; gap: 12px; flex-wrap: wrap; }
406
457
  .topbar-brand { flex-basis: 100%; }
@@ -3,6 +3,7 @@
3
3
  const state = {
4
4
  tab: "projects",
5
5
  sessionTab: "ordinary",
6
+ systemTab: "admins",
6
7
  projects: null,
7
8
  sessions: null,
8
9
  sides: null,
@@ -56,6 +57,12 @@ function expireAdminSession() {
56
57
  state.authExpired = true;
57
58
  stopUpdatePolling();
58
59
  window.NetizenSelect?.close();
60
+ // Release these modal drawers so the page's login link remains reachable.
61
+ // Inputs stay in memory, but expired actions can never be submitted again.
62
+ for (const dialog of document.querySelectorAll("#project-drawer, #worktree-create-drawer, #worktree-settings-drawer")) {
63
+ if (dialog.open) dialog.close();
64
+ }
65
+ document.body.classList.toggle("workspace-drawer-open", false);
59
66
  for (const element of document.querySelectorAll?.(".panel, dialog, .tabs") || []) element.inert = true;
60
67
  for (const button of document.querySelectorAll?.("button") || []) button.disabled = true;
61
68
  const notice = document.querySelector("#auth-expired");
@@ -403,6 +410,163 @@ function actionsCell(row) {
403
410
  return wrap;
404
411
  }
405
412
 
413
+ let projectEditor = null;
414
+ const projectInput = (name) => document.querySelector(`#project-${name}`);
415
+
416
+ function projectEditorValues() {
417
+ if (!projectEditor) return null;
418
+ return { mode: projectEditor.mode, alias: projectInput("alias").value,
419
+ paths: { ...projectEditor.paths, [projectEditor.mode]: projectInput("path").value } };
420
+ }
421
+
422
+ function projectEditorHasChanges() {
423
+ return Boolean(projectEditor && JSON.stringify(projectEditorValues()) !== projectEditor.discardBase);
424
+ }
425
+
426
+ function renderProjectEditorControls() {
427
+ const busy = state.authExpired || projectEditor?.saving;
428
+ projectInput("editor").setAttribute("aria-busy", String(Boolean(projectEditor?.saving || projectEditor?.checking)));
429
+ projectInput("new").disabled = Boolean(busy
430
+ || !(state.projects?.actions?.register || state.projects?.actions?.createDirectory));
431
+ for (const button of document.querySelectorAll("[data-project-add]")) button.disabled = projectInput("new").disabled;
432
+ for (const field of projectInput("editor").querySelectorAll("input, button")) field.disabled = Boolean(busy);
433
+ if (!projectEditor) {
434
+ projectInput("submit").disabled = true;
435
+ return;
436
+ }
437
+ const registering = projectEditor.mode === "register";
438
+ projectInput("path").required = registering;
439
+ projectInput("path-label").textContent = registering ? "绝对路径" : "绝对路径(可留空)";
440
+ projectInput("path-help").textContent = registering
441
+ ? "登记一个已存在的目录作为 Project。"
442
+ : "留空时在实例项目根目录下按 Alias 创建空目录;自定义路径也须位于该根目录内。";
443
+ for (const button of document.querySelectorAll("[data-project-mode]")) {
444
+ button.setAttribute("aria-pressed", String(button.dataset.projectMode === projectEditor.mode));
445
+ button.disabled = Boolean(busy || projectEditor.attempted);
446
+ }
447
+ projectInput("submit").textContent = projectEditor.saving
448
+ ? registering ? "正在登记…" : "正在创建…" : registering ? "登记 Project" : "创建并登记";
449
+ projectInput("submit").disabled = Boolean(busy || projectEditor.attempted || !projectEditor.actions[projectEditor.mode]);
450
+ projectInput("check").disabled = Boolean(busy || projectEditor.checking);
451
+ }
452
+
453
+ function openProjectEditor() {
454
+ if (state.authExpired || projectEditor?.saving
455
+ || !(state.projects?.actions?.register || state.projects?.actions?.createDirectory)) return;
456
+ if (projectEditor && !closeProjectEditor()) return;
457
+ projectEditor = { mode: "register", paths: { register: "", createDirectory: "" },
458
+ actions: { ...state.projects.actions }, returnFocus: document.activeElement, saving: false, attempted: false };
459
+ projectInput("alias").value = "";
460
+ projectInput("path").value = "";
461
+ projectInput("note").textContent = "";
462
+ projectInput("note").classList.toggle("error", false);
463
+ projectInput("check").hidden = true;
464
+ projectInput("check").textContent = "刷新列表核对";
465
+ projectEditor.discardBase = JSON.stringify(projectEditorValues());
466
+ projectInput("editor").hidden = false;
467
+ renderProjectEditorControls();
468
+ projectInput("drawer").showModal();
469
+ document.body.classList.toggle("workspace-drawer-open", true);
470
+ projectInput("drawer").querySelector(".drawer-body").scrollTop = 0;
471
+ projectInput("alias").focus();
472
+ }
473
+
474
+ function closeProjectEditor({ saved = false, confirmed = false } = {}) {
475
+ if (!projectEditor) return true;
476
+ if (projectEditor.saving) return false;
477
+ if (!saved && !confirmed && !confirmDiscardChanges(projectEditorHasChanges())) return false;
478
+ const returnFocus = projectEditor.returnFocus;
479
+ projectEditor = null;
480
+ projectInput("editor").hidden = true;
481
+ projectInput("drawer").close();
482
+ document.body.classList.toggle("workspace-drawer-open", false);
483
+ renderProjectEditorControls();
484
+ (returnFocus?.isConnected === false ? projectInput("new") : returnFocus)?.focus();
485
+ return true;
486
+ }
487
+
488
+ function selectProjectMode(mode) {
489
+ if (!projectEditor || projectEditor.saving || projectEditor.attempted || state.authExpired) return;
490
+ projectEditor.paths[projectEditor.mode] = projectInput("path").value;
491
+ projectEditor.mode = mode;
492
+ projectInput("path").value = projectEditor.paths[mode];
493
+ renderProjectEditorControls();
494
+ }
495
+
496
+ async function checkProjectResult() {
497
+ const editor = projectEditor;
498
+ if (!editor || editor.saving || editor.checking || state.authExpired) return;
499
+ editor.checking = true;
500
+ renderProjectEditorControls();
501
+ try {
502
+ const refreshed = await loadProjects();
503
+ if (projectEditor !== editor || state.authExpired) return;
504
+ if (refreshed !== false && editor.attempted && !editor.uncertain) {
505
+ editor.actions = { ...state.projects.actions };
506
+ editor.attempted = false;
507
+ projectInput("check").hidden = true;
508
+ projectInput("note").classList.toggle("error", false);
509
+ projectInput("note").textContent = "已刷新,当前输入已保留。可修改后重新提交。";
510
+ } else {
511
+ projectInput("note").textContent = refreshed === false
512
+ ? "列表未更新,请再次刷新核对。"
513
+ : "已刷新 Projects 列表;请关闭表单核对 Alias 与路径。未重复提交操作。";
514
+ }
515
+ } catch (error) {
516
+ if (projectEditor === editor && !state.authExpired) projectInput("note").textContent = `列表读取失败:${error.message}`;
517
+ } finally {
518
+ editor.checking = false;
519
+ if (projectEditor === editor) renderProjectEditorControls();
520
+ }
521
+ }
522
+
523
+ async function saveProject(event) {
524
+ event.preventDefault();
525
+ const editor = projectEditor;
526
+ if (!editor || editor.saving || editor.attempted || state.authExpired || !editor.actions[editor.mode]) return;
527
+ if (!projectInput("editor").reportValidity()) return;
528
+ const envelope = editor.actions[editor.mode];
529
+ editor.actions[editor.mode] = null;
530
+ if (state.projects?.actions) state.projects.actions[editor.mode] = null;
531
+ editor.saving = true;
532
+ editor.attempted = true;
533
+ projectInput("note").textContent = editor.mode === "register" ? "正在登记 Project…" : "正在创建 Project…";
534
+ renderProjectEditorControls();
535
+ let result;
536
+ try {
537
+ result = await api(editor.mode === "register" ? "/api/v1/projects/register" : "/api/v1/projects/create-directory", {
538
+ method: "POST", headers: { "Content-Type": "application/json" },
539
+ body: JSON.stringify(actionPayload(envelope, { alias: projectInput("alias").value,
540
+ path: projectInput("path").value || null })),
541
+ });
542
+ } catch (error) {
543
+ if (state.authExpired) return;
544
+ editor.uncertain = !error.status || error.status >= 500;
545
+ projectInput("note").textContent = !editor.uncertain
546
+ ? `${error.message} 输入已保留,请刷新后修改并重新提交。`
547
+ : "操作结果未确认,请刷新 Projects 列表核对;不要重复提交。";
548
+ projectInput("note").classList.toggle("error", true);
549
+ projectInput("check").hidden = false;
550
+ projectInput("check").textContent = editor.uncertain ? "刷新列表核对" : "刷新后继续填写";
551
+ setStatus(projectInput("note").textContent, true);
552
+ return;
553
+ } finally {
554
+ editor.saving = false;
555
+ renderProjectEditorControls();
556
+ }
557
+ const alias = projectInput("alias").value;
558
+ closeProjectEditor({ saved: true });
559
+ const message = `Project「${result?.alias || alias}」已${editor.mode === "register" ? "登记" : "创建并登记"}。`
560
+ + (result?.cwd ? `目录:${result.cwd}` : "");
561
+ try {
562
+ const refreshed = await loadProjects();
563
+ if (!state.authExpired && state.tab === "projects") setStatus(
564
+ refreshed === false ? `${message} 列表未更新,请手动刷新。` : message);
565
+ } catch {
566
+ if (!state.authExpired && state.tab === "projects") setStatus(`${message} 列表刷新失败,请手动刷新查看。`, true);
567
+ }
568
+ }
569
+
406
570
  function confirmMaterializedDelete(session) {
407
571
  const name = session.nativeTitle || "未命名 Session";
408
572
  return window.confirm(
@@ -574,6 +738,19 @@ async function loadProjects(cursor = undefined) {
574
738
  }
575
739
  body.append(row);
576
740
  }
741
+ if (!data.items.length) {
742
+ const row = document.createElement("tr");
743
+ const empty = document.createElement("td");
744
+ empty.colSpan = 6;
745
+ const message = document.createElement("p");
746
+ message.textContent = "本页没有 Project。可登记已有目录,或创建空目录。";
747
+ const add = actionButton("添加 Project", openProjectEditor);
748
+ add.setAttribute("data-project-add", "");
749
+ empty.append(message, add);
750
+ row.append(empty);
751
+ body.append(row);
752
+ }
753
+ renderProjectEditorControls();
577
754
  renderCursorPagination("projects", state.projectPage, data.items.length);
578
755
  return true;
579
756
  }
@@ -2388,12 +2565,13 @@ function renderDefaultSettings() {
2388
2565
  const model = renderSessionSettingsFields(defaultsInput, settings, editor.models, editor.contextAvailable);
2389
2566
  defaultsInput("context-field").hidden = editor.kind === "chat"
2390
2567
  && chatTarget("defaults").kind() === "p2p" && settings.message_context_mode === "current-only";
2568
+ renderDefaultWorkspace();
2391
2569
  const notes = [editor.loading ? "正在读取配置…" : "", editor.sourceNote, editor.error, editor.catalogError];
2392
2570
  if (turn && !model) notes.push("已保存的模型当前不可用,选择保持不变;可显式选择其他模型。");
2393
2571
  if (editor.contextAvailable === false) notes.push("单聊不支持补齐未读上下文,请使用当前消息。");
2394
2572
  defaultsInput("note").textContent = notes.filter(Boolean).join(" ");
2395
2573
  defaultsInput("note").classList.toggle("error", Boolean(editor.error));
2396
- defaultsInput("save").disabled = defaultsBusy || editor.loading || !editor.action;
2574
+ defaultsInput("save").disabled = defaultsBusy || editor.loading || !editor.action || !defaultWorkspaceAvailable();
2397
2575
  defaultsInput("save").textContent = defaultsBusy ? "保存中…" : "保存";
2398
2576
  defaultsInput("editor").setAttribute("aria-busy", String(defaultsBusy || editor.loading));
2399
2577
  }
@@ -2418,6 +2596,77 @@ function renderDefaultProject(project) {
2418
2596
  ])], project);
2419
2597
  }
2420
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
+
2421
2670
  async function loadDefaultOptions(resolveChat = false) {
2422
2671
  const editor = defaultsEditor;
2423
2672
  if (!editor) return;
@@ -2426,6 +2675,8 @@ async function loadDefaultOptions(resolveChat = false) {
2426
2675
  const chatId = editor.kind === "chat" ? defaultsInput("chat").value.trim() : "";
2427
2676
  editor.loading = true;
2428
2677
  editor.error = "";
2678
+ editor.workspaceOptions = null;
2679
+ editor.workspaceError = "";
2429
2680
  renderDefaultSettings();
2430
2681
  try {
2431
2682
  let view = null;
@@ -2437,10 +2688,12 @@ async function loadDefaultOptions(resolveChat = false) {
2437
2688
  const effective = view.exact || view.effective;
2438
2689
  if (effective) {
2439
2690
  editor.settings = structuredClone(effective.session_settings);
2691
+ editor.workspace = structuredClone(effective.workspace);
2440
2692
  renderDefaultProject(effective.project);
2441
2693
  editor.touched = true;
2442
2694
  } else {
2443
2695
  editor.settings = defaultScheduleSessionSettings();
2696
+ editor.workspace = { mode: "project" };
2444
2697
  renderDefaultProject("");
2445
2698
  editor.touched = false;
2446
2699
  }
@@ -2450,10 +2703,14 @@ async function loadDefaultOptions(resolveChat = false) {
2450
2703
  }
2451
2704
  const query = new URLSearchParams({ mode: "options" });
2452
2705
  if (chatId) query.set("chat_id", chatId);
2706
+ const project = defaultsInput("project").value;
2707
+ if (project) query.set("project", project);
2453
2708
  const data = await api(`/api/v1/defaults?${query}`);
2454
2709
  if (defaultsEditor !== editor || serial !== editor.serial) return;
2455
2710
  editor.models = data.models;
2456
2711
  editor.contextAvailable = data.context_mode_available;
2712
+ editor.workspaceOptions = data.workspace_options || null;
2713
+ editor.workspaceError = data.workspace_options_error || "";
2457
2714
  if (editor.kind === "chat") chatTarget("defaults").resolveKind(editor.contextAvailable);
2458
2715
  editor.catalogError = typeof data.model_catalog_error === "string"
2459
2716
  ? data.model_catalog_error : data.model_catalog_error?.message || "";
@@ -2480,6 +2737,7 @@ function openDefaultEditor(kind, record = null) {
2480
2737
  returnFocus: document.activeElement,
2481
2738
  settings: structuredClone(record?.session_settings || defaultScheduleSessionSettings()),
2482
2739
  models: [], serial: 0, loading: true, touched: false, contextAvailable: null,
2740
+ workspace: structuredClone(record ? record.workspace : { mode: "project" }), workspaceOptions: null, workspaceError: "",
2483
2741
  sourceNote: record ? "修改已保存的配置,只影响之后创建的会话。" : "保存后,在没有当前会话的位置收到消息时生效。",
2484
2742
  error: "", catalogError: "", lockedTarget: Boolean(record) };
2485
2743
  defaultsInput("editor").hidden = false;
@@ -2524,7 +2782,7 @@ function defaultEditorValues() {
2524
2782
  if (!editor) return null;
2525
2783
  return { kind: editor.kind, project: defaultsInput("project").value,
2526
2784
  target: defaultsInput(editor.kind === "chat" ? "chat" : "keyword").value,
2527
- settings: sessionSettingsFingerprint(editor.settings) };
2785
+ settings: sessionSettingsFingerprint(editor.settings), workspace: structuredClone(editor.workspace) };
2528
2786
  }
2529
2787
 
2530
2788
  function defaultEditorHasChanges() {
@@ -2575,7 +2833,7 @@ async function defaultMutation(mode, action, extra = {}) {
2575
2833
  async function saveDefault(event) {
2576
2834
  event.preventDefault();
2577
2835
  const editor = defaultsEditor;
2578
- if (!editor || editor.loading || !editor.action || defaultsBusy) return;
2836
+ if (!editor || editor.loading || !editor.action || defaultsBusy || !defaultWorkspaceAvailable()) return;
2579
2837
  if (editor.kind === "chat") {
2580
2838
  editor.loading = true;
2581
2839
  renderDefaultSettings();
@@ -2588,7 +2846,10 @@ async function saveDefault(event) {
2588
2846
  if (defaultsEditor !== editor) return;
2589
2847
  }
2590
2848
  const definition = { kind: editor.kind, project: defaultsInput("project").value,
2591
- 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
+ }
2592
2853
  if (editor.kind === "chat") definition.chat_id = defaultsInput("chat").value.trim();
2593
2854
  else definition.keyword = defaultsInput("keyword").value.trim();
2594
2855
  const action = editor.action;
@@ -2651,7 +2912,7 @@ function defaultChatCell(row, rule) {
2651
2912
  row.append(td);
2652
2913
  }
2653
2914
 
2654
- function defaultSettingsCell(row, settings) {
2915
+ function defaultSettingsCell(row, settings, workspace) {
2655
2916
  const td = document.createElement("td");
2656
2917
  const model = settings.turn_settings;
2657
2918
  const title = document.createElement("div");
@@ -2659,7 +2920,11 @@ function defaultSettingsCell(row, settings) {
2659
2920
  const detail = document.createElement("div");
2660
2921
  detail.className = "default-settings-detail";
2661
2922
  detail.textContent = scheduleSessionSummary(settings).split(" · ").slice(1).join(" · ");
2662
- 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);
2663
2928
  row.append(td);
2664
2929
  }
2665
2930
 
@@ -2686,7 +2951,7 @@ async function loadDefaults(offset = defaultsOffset, returnFocus = null) {
2686
2951
  cell(row, rule.keyword);
2687
2952
  } else defaultChatCell(row, rule);
2688
2953
  cell(row, rule.project);
2689
- defaultSettingsCell(row, rule.session_settings);
2954
+ defaultSettingsCell(row, rule.session_settings, rule.workspace);
2690
2955
  const actions = document.createElement("td");
2691
2956
  const buttons = document.createElement("div");
2692
2957
  buttons.className = "actions";
@@ -2740,7 +3005,7 @@ function confirmDiscardChanges(changed) {
2740
3005
  }
2741
3006
 
2742
3007
  function hasUnsavedEditors() {
2743
- return sessionSettingsHasChanges() || defaultEditorHasChanges() || scheduleEditorHasChanges()
3008
+ return projectEditorHasChanges() || sessionSettingsHasChanges() || defaultEditorHasChanges() || scheduleEditorHasChanges()
2744
3009
  || Boolean(pendingScheduleDraft?.dirty) || Boolean(window.NetizenSessionAccess?.dirty())
2745
3010
  || Boolean(window.NetizenWorktrees?.dirty());
2746
3011
  }
@@ -2748,9 +3013,10 @@ function hasUnsavedEditors() {
2748
3013
  // Navigation asks once before closing any editor. Busy submissions retain the
2749
3014
  // existing fail-closed boundary; this helper never cancels or repeats a POST.
2750
3015
  function confirmDiscardEditors() {
2751
- if (sessionSettingsEditor?.saving || defaultsBusy || (scheduleEditor && scheduleInput("fields").disabled)
3016
+ if (projectEditor?.saving || sessionSettingsEditor?.saving || defaultsBusy || (scheduleEditor && scheduleInput("fields").disabled)
2752
3017
  || window.NetizenSessionAccess?.busy() || window.NetizenWorktrees?.busy()) return false;
2753
3018
  if (!confirmDiscardChanges(hasUnsavedEditors())) return false;
3019
+ closeProjectEditor({ confirmed: true });
2754
3020
  closeSessionSettingsEditor({ confirmed: true });
2755
3021
  closeDefaultEditor({ confirmed: true });
2756
3022
  if (scheduleEditor) closeScheduleEditor({ confirmed: true });
@@ -4089,6 +4355,20 @@ function selectSessionTab(name) {
4089
4355
  return refresh("sessions");
4090
4356
  }
4091
4357
 
4358
+ function selectSystemTab(name) {
4359
+ if (state.authExpired || state.systemTab === name) return;
4360
+ window.NetizenSelect?.close();
4361
+ if (state.systemTab === "admins") window.NetizenIdentity?.suspendSelector();
4362
+ else window.NetizenSessionAccess?.closePickers();
4363
+ state.systemTab = name;
4364
+ for (const button of document.querySelectorAll("[data-system-tab]")) {
4365
+ const selected = button.dataset.systemTab === name;
4366
+ button.setAttribute("aria-selected", String(selected));
4367
+ button.setAttribute("tabindex", selected ? "0" : "-1");
4368
+ document.querySelector(`#system-${button.dataset.systemTab}`).hidden = !selected;
4369
+ }
4370
+ }
4371
+
4092
4372
  function selectTab(name, sessionTab = state.sessionTab || "ordinary", { preserveEditors = false, replace = false } = {}) {
4093
4373
  if (state.authExpired) return;
4094
4374
  if (!preserveEditors && !confirmDiscardEditors()) return;
@@ -4141,6 +4421,18 @@ for (const tab of document.querySelectorAll("[data-session-tab]")) {
4141
4421
  });
4142
4422
  }
4143
4423
 
4424
+ for (const tab of document.querySelectorAll("[data-system-tab]")) {
4425
+ tab.addEventListener("click", () => selectSystemTab(tab.dataset.systemTab));
4426
+ tab.addEventListener("keydown", (event) => {
4427
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
4428
+ event.preventDefault();
4429
+ const name = event.key === "Home" ? "admins" : event.key === "End" ? "access"
4430
+ : tab.dataset.systemTab === "admins" ? "access" : "admins";
4431
+ selectSystemTab(name);
4432
+ document.querySelector(`#system-tab-${name}`).focus();
4433
+ });
4434
+ }
4435
+
4144
4436
  for (const button of document.querySelectorAll("[data-refresh]")) {
4145
4437
  button.addEventListener("click", () => refresh(button.dataset.refresh));
4146
4438
  }
@@ -4149,21 +4441,18 @@ for (const button of document.querySelectorAll("[data-session-sort]")) {
4149
4441
  button.addEventListener("click", () => sortSessionPage(button.dataset.sessionSort, button.dataset.sortBy));
4150
4442
  }
4151
4443
 
4152
- document.querySelector("#register-project").addEventListener("submit", async (event) => {
4444
+ projectInput("new").addEventListener("click", openProjectEditor);
4445
+ projectInput("cancel").addEventListener("click", closeProjectEditor);
4446
+ projectInput("close").addEventListener("click", closeProjectEditor);
4447
+ projectInput("drawer").addEventListener("cancel", (event) => {
4153
4448
  event.preventDefault();
4154
- const formElement = event.currentTarget;
4155
- const form = new FormData(formElement);
4156
- await mutate("/api/v1/projects/register", state.projects.actions.register, { alias: form.get("alias"), path: form.get("path") });
4157
- formElement.reset();
4158
- });
4159
-
4160
- document.querySelector("#create-project").addEventListener("submit", async (event) => {
4161
- event.preventDefault();
4162
- const formElement = event.currentTarget;
4163
- const form = new FormData(formElement);
4164
- await mutate("/api/v1/projects/create-directory", state.projects.actions.createDirectory, { alias: form.get("alias"), path: form.get("path") || null });
4165
- formElement.reset();
4449
+ closeProjectEditor();
4166
4450
  });
4451
+ projectInput("editor").addEventListener("submit", saveProject);
4452
+ projectInput("check").addEventListener("click", checkProjectResult);
4453
+ for (const button of document.querySelectorAll("[data-project-mode]")) {
4454
+ button.addEventListener("click", () => selectProjectMode(button.dataset.projectMode));
4455
+ }
4167
4456
 
4168
4457
  document.querySelector("#session-inventory-state").addEventListener("change", updateSessionDateFilters);
4169
4458
  document.querySelector("#session-filter").addEventListener("submit", (event) => {
@@ -4217,6 +4506,11 @@ defaultsInput("drawer").addEventListener("cancel", (event) => {
4217
4506
  });
4218
4507
  defaultsInput("editor").addEventListener("submit", saveDefault);
4219
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"));
4220
4514
  for (const field of ["model", "effort", "tier", "context", "reactions", "progress", "completion-mention"]) {
4221
4515
  defaultsInput(field).addEventListener("change", () => changeDefaultSettings(field));
4222
4516
  }