windcode 0.1.0__tar.gz → 0.1.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 (164) hide show
  1. {windcode-0.1.0 → windcode-0.1.2}/.gitignore +6 -0
  2. {windcode-0.1.0 → windcode-0.1.2}/.windcode/config.toml.example +2 -2
  3. {windcode-0.1.0 → windcode-0.1.2}/AGENTS.md +3 -2
  4. {windcode-0.1.0 → windcode-0.1.2}/PKG-INFO +6 -5
  5. {windcode-0.1.0 → windcode-0.1.2}/README.md +5 -4
  6. {windcode-0.1.0 → windcode-0.1.2}/examples/README.md +1 -1
  7. {windcode-0.1.0 → windcode-0.1.2}/pyproject.toml +1 -1
  8. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/__init__.py +1 -1
  9. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/cli.py +4 -3
  10. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/config/__init__.py +8 -1
  11. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/config/loader.py +27 -1
  12. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/service.py +13 -2
  13. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sdk.py +6 -43
  14. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/app.py +18 -7
  15. {windcode-0.1.0 → windcode-0.1.2}/uv.lock +1 -1
  16. {windcode-0.1.0 → windcode-0.1.2}/.github/workflows/publish.yml +0 -0
  17. {windcode-0.1.0 → windcode-0.1.2}/.python-version +0 -0
  18. {windcode-0.1.0 → windcode-0.1.2}/.windcode/skills/code-review/SKILL.md +0 -0
  19. {windcode-0.1.0 → windcode-0.1.2}/.windcode/skills/code-review/agents/openai.yaml +0 -0
  20. {windcode-0.1.0 → windcode-0.1.2}/LICENSE +0 -0
  21. {windcode-0.1.0 → windcode-0.1.2}/assets/image.png +0 -0
  22. {windcode-0.1.0 → windcode-0.1.2}/assets/image1.png +0 -0
  23. {windcode-0.1.0 → windcode-0.1.2}/assets/image2.png +0 -0
  24. {windcode-0.1.0 → windcode-0.1.2}/assets/image3.png +0 -0
  25. {windcode-0.1.0 → windcode-0.1.2}/assets/image4.png +0 -0
  26. {windcode-0.1.0 → windcode-0.1.2}/examples/01_stateful_chat.py +0 -0
  27. {windcode-0.1.0 → windcode-0.1.2}/examples/02_custom_tool.py +0 -0
  28. {windcode-0.1.0 → windcode-0.1.2}/examples/03_multi_agent.py +0 -0
  29. {windcode-0.1.0 → windcode-0.1.2}/examples/04_mcp_tools.py +0 -0
  30. {windcode-0.1.0 → windcode-0.1.2}/examples/05_skill.py +0 -0
  31. {windcode-0.1.0 → windcode-0.1.2}/examples/06_stream_and_cancel.py +0 -0
  32. {windcode-0.1.0 → windcode-0.1.2}/examples/07_bash_approval.py +0 -0
  33. {windcode-0.1.0 → windcode-0.1.2}/examples/skills/release_notes/SKILL.md +0 -0
  34. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/__main__.py +0 -0
  35. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/auth/__init__.py +0 -0
  36. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/auth/store.py +0 -0
  37. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/config/models.py +0 -0
  38. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/config/writer.py +0 -0
  39. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/context/__init__.py +0 -0
  40. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/context/compactor.py +0 -0
  41. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/context/estimator.py +0 -0
  42. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/context/truncation.py +0 -0
  43. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/domain/__init__.py +0 -0
  44. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/domain/errors.py +0 -0
  45. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/domain/events.py +0 -0
  46. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/domain/messages.py +0 -0
  47. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/domain/models.py +0 -0
  48. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/domain/subagents.py +0 -0
  49. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/domain/tools.py +0 -0
  50. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/__init__.py +0 -0
  51. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/commands.py +0 -0
  52. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/discovery.py +0 -0
  53. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/events.py +0 -0
  54. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/hooks/__init__.py +0 -0
  55. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/hooks/dispatcher.py +0 -0
  56. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/hooks/executor.py +0 -0
  57. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/hooks/loader.py +0 -0
  58. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/hooks/models.py +0 -0
  59. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/mcp/__init__.py +0 -0
  60. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/mcp/adapter.py +0 -0
  61. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/mcp/catalog.py +0 -0
  62. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/mcp/client.py +0 -0
  63. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/mcp/runtime.py +0 -0
  64. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/mcp/tools.py +0 -0
  65. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/models.py +0 -0
  66. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/paths.py +0 -0
  67. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/plugins/__init__.py +0 -0
  68. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/plugins/installer.py +0 -0
  69. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/plugins/manifest.py +0 -0
  70. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/runtime.py +0 -0
  71. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/skills/__init__.py +0 -0
  72. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/skills/loader.py +0 -0
  73. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/skills/parser.py +0 -0
  74. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/skills/tools.py +0 -0
  75. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/snapshot.py +0 -0
  76. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/extensions/state.py +0 -0
  77. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/instructions/__init__.py +0 -0
  78. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/instructions/loader.py +0 -0
  79. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/memory/__init__.py +0 -0
  80. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/memory/extraction.py +0 -0
  81. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/memory/models.py +0 -0
  82. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/memory/refiner.py +0 -0
  83. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/memory/security.py +0 -0
  84. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/memory/service.py +0 -0
  85. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/memory/store.py +0 -0
  86. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/observability/__init__.py +0 -0
  87. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/observability/redaction.py +0 -0
  88. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/observability/trace.py +0 -0
  89. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/policy/__init__.py +0 -0
  90. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/policy/engine.py +0 -0
  91. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/policy/models.py +0 -0
  92. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/__init__.py +0 -0
  93. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/_utils.py +0 -0
  94. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/anthropic.py +0 -0
  95. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/base.py +0 -0
  96. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/catalog.py +0 -0
  97. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/errors.py +0 -0
  98. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/openai_compat.py +0 -0
  99. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/openai_responses.py +0 -0
  100. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/providers/registry.py +0 -0
  101. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/__init__.py +0 -0
  102. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/control.py +0 -0
  103. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/event_bus.py +0 -0
  104. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/loop.py +0 -0
  105. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/prompts.py +0 -0
  106. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/report.py +0 -0
  107. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/retry.py +0 -0
  108. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/scheduler.py +0 -0
  109. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/subagents/__init__.py +0 -0
  110. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/subagents/approvals.py +0 -0
  111. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/subagents/budgets.py +0 -0
  112. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/subagents/coordinator.py +0 -0
  113. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/subagents/factory.py +0 -0
  114. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/subagents/roles.py +0 -0
  115. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/runtime/subagents/verification.py +0 -0
  116. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sandbox/__init__.py +0 -0
  117. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sandbox/bwrap.py +0 -0
  118. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sessions/__init__.py +0 -0
  119. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sessions/artifacts.py +0 -0
  120. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sessions/models.py +0 -0
  121. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sessions/store.py +0 -0
  122. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/sessions/tree.py +0 -0
  123. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/__init__.py +0 -0
  124. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/apply_patch.py +0 -0
  125. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/ask_user.py +0 -0
  126. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/builtins.py +0 -0
  127. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/edit_file.py +0 -0
  128. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/filesystem.py +0 -0
  129. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/glob.py +0 -0
  130. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/grep.py +0 -0
  131. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/memory.py +0 -0
  132. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/read_file.py +0 -0
  133. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/registry.py +0 -0
  134. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/shell.py +0 -0
  135. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/subagents/__init__.py +0 -0
  136. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/subagents/cancel.py +0 -0
  137. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/subagents/integrate.py +0 -0
  138. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/subagents/list.py +0 -0
  139. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/subagents/spawn.py +0 -0
  140. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/subagents/wait.py +0 -0
  141. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tools/write_file.py +0 -0
  142. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/__init__.py +0 -0
  143. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/commands.py +0 -0
  144. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/permission_display.py +0 -0
  145. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/styles.tcss +0 -0
  146. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/__init__.py +0 -0
  147. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/approval.py +0 -0
  148. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/command_menu.py +0 -0
  149. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/extensions.py +0 -0
  150. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/input.py +0 -0
  151. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/memory.py +0 -0
  152. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/messages.py +0 -0
  153. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/models.py +0 -0
  154. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/question.py +0 -0
  155. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/sessions.py +0 -0
  156. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/status.py +0 -0
  157. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/subagents.py +0 -0
  158. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/tools.py +0 -0
  159. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/tui/widgets/welcome.py +0 -0
  160. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/types.py +0 -0
  161. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/worktrees/__init__.py +0 -0
  162. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/worktrees/git.py +0 -0
  163. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/worktrees/manager.py +0 -0
  164. {windcode-0.1.0 → windcode-0.1.2}/src/windcode/worktrees/models.py +0 -0
@@ -14,6 +14,12 @@ tests/
14
14
 
15
15
  .windcode/config.toml
16
16
  .windcode/state/
17
+ .windcode/skill/
18
+ .windcode/memory/
19
+ .windcode/sessions/
20
+ .windcode/traces/
21
+ .windcode/extensions/
22
+ .windcode/worktrees/
17
23
 
18
24
 
19
25
  spec/
@@ -54,8 +54,8 @@ url =
54
54
  required = true
55
55
 
56
56
  [storage]
57
- project_state_root = ".windcode/state"
58
- user_storage_root = "~/.local/state/windcode/state"
57
+ project_state_root = ".windcode"
58
+ user_storage_root = "~/.local/state/windcode"
59
59
 
60
60
  [memory]
61
61
  enabled = true
@@ -56,8 +56,9 @@ explicit SDK state_root
56
56
  -> user_storage_root
57
57
  ```
58
58
 
59
- The root contains `memory/`, `sessions/`, `traces/`, `extensions/`, and `worktrees/`.
60
- `.windcode/config.toml`, `.windcode/state/`, `tests/`, and `spec/` are local-only ignored paths.
59
+ The root contains `skill/`, `memory/`, `sessions/`, `traces/`, `extensions/`, and `worktrees/`.
60
+ `.windcode/config.toml`, runtime directories under `.windcode/`, `tests/`, and `spec/` are local-only
61
+ ignored paths.
61
62
  Never remove, rewrite, force-add, or commit them without an explicit user request.
62
63
 
63
64
  For MCP servers, `enable` controls discovery and runtime visibility; `required` only controls startup
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: windcode
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A safe, extensible terminal coding agent
5
5
  Project-URL: Homepage, https://github.com/tingfeng347/windcode
6
6
  Project-URL: Repository, https://github.com/tingfeng347/windcode
@@ -149,12 +149,13 @@ Windcode 将记忆、会话、trace、扩展状态和 Worktree 统一存放在
149
149
 
150
150
  ```toml
151
151
  [storage]
152
- project_state_root = ".windcode/state"
153
- user_storage_root = "~/.local/state/windcode/state"
152
+ project_state_root = ".windcode"
153
+ user_storage_root = "~/.local/state/windcode"
154
154
  ```
155
155
 
156
- 配置项目状态根时优先使用项目目录;未配置时使用用户状态目录。`.windcode/config.toml`
157
- `.windcode/state/` 都不应提交到 Git。
156
+ 配置项目状态根时优先使用项目目录;未配置时使用用户状态目录。Skill 会同时扫描两边的
157
+ `skill/`,同名时项目级覆盖用户级。`.windcode/config.toml` 和 `.windcode/` 下的运行状态都
158
+ 不应提交到 Git。
158
159
 
159
160
  ## 开发
160
161
 
@@ -114,12 +114,13 @@ Windcode 将记忆、会话、trace、扩展状态和 Worktree 统一存放在
114
114
 
115
115
  ```toml
116
116
  [storage]
117
- project_state_root = ".windcode/state"
118
- user_storage_root = "~/.local/state/windcode/state"
117
+ project_state_root = ".windcode"
118
+ user_storage_root = "~/.local/state/windcode"
119
119
  ```
120
120
 
121
- 配置项目状态根时优先使用项目目录;未配置时使用用户状态目录。`.windcode/config.toml`
122
- `.windcode/state/` 都不应提交到 Git。
121
+ 配置项目状态根时优先使用项目目录;未配置时使用用户状态目录。Skill 会同时扫描两边的
122
+ `skill/`,同名时项目级覆盖用户级。`.windcode/config.toml` 和 `.windcode/` 下的运行状态都
123
+ 不应提交到 Git。
123
124
 
124
125
  ## 开发
125
126
 
@@ -61,7 +61,7 @@ uv run python examples/07_bash_approval.py
61
61
 
62
62
  ```toml
63
63
  [storage]
64
- project_state_root = ".windcode/state"
64
+ project_state_root = ".windcode"
65
65
  ```
66
66
 
67
67
  ## MCP 示例配置
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "windcode"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "A safe, extensible terminal coding agent"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -3,4 +3,4 @@
3
3
  from windcode.sdk import Windcode
4
4
 
5
5
  __all__ = ["Windcode"]
6
- __version__ = "0.1.0"
6
+ __version__ = "0.1.2"
@@ -8,7 +8,7 @@ from collections.abc import Sequence
8
8
  from dataclasses import dataclass
9
9
  from pathlib import Path
10
10
 
11
- from windcode.config import AppConfig, ConfigError, PermissionMode, load_config
11
+ from windcode.config import AppConfig, ConfigError, PermissionMode, ensure_user_config, load_config
12
12
 
13
13
 
14
14
  @dataclass(frozen=True, slots=True)
@@ -156,8 +156,9 @@ def run(argv: Sequence[str] | None = None) -> int:
156
156
  options = parse_options(arguments)
157
157
  if not options.workspace.is_dir():
158
158
  raise ConfigError(options.workspace, "workspace is not a directory")
159
+ write_config_file = options.config_file or ensure_user_config()
159
160
  config = resolve_config(options)
160
- except ConfigError as exc:
161
+ except (ConfigError, OSError) as exc:
161
162
  print(f"windcode: {exc}", file=sys.stderr)
162
163
  return 2
163
164
 
@@ -171,7 +172,7 @@ def run(argv: Sequence[str] | None = None) -> int:
171
172
  permission_mode=(
172
173
  options.permission_mode.value if options.permission_mode is not None else None
173
174
  ),
174
- config_file=options.config_file,
175
+ config_file=write_config_file,
175
176
  )
176
177
  app.run()
177
178
  return 0
@@ -1,4 +1,9 @@
1
- from windcode.config.loader import ConfigError, load_config
1
+ from windcode.config.loader import (
2
+ ConfigError,
3
+ default_user_config_path,
4
+ ensure_user_config,
5
+ load_config,
6
+ )
2
7
  from windcode.config.models import (
3
8
  HARD_MAX_CONCURRENT_SUBAGENTS,
4
9
  HARD_MAX_SUBAGENT_TASKS,
@@ -35,6 +40,8 @@ __all__ = [
35
40
  "StorageConfig",
36
41
  "SubagentConfig",
37
42
  "TraceConfig",
43
+ "default_user_config_path",
44
+ "ensure_user_config",
38
45
  "load_config",
39
46
  "save_memory_config",
40
47
  "save_model_config",
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
+ import os
4
5
  import tomllib
5
6
  from collections.abc import Mapping
6
7
  from pathlib import Path
7
8
  from typing import Any, cast
8
9
 
10
+ import tomli_w
9
11
  from platformdirs import user_config_path
10
12
  from pydantic import ValidationError
11
13
 
@@ -18,6 +20,30 @@ class ConfigError(ValueError):
18
20
  super().__init__(f"{source}: {message}")
19
21
 
20
22
 
23
+ def default_user_config_path() -> Path:
24
+ return user_config_path("windcode") / "config.toml"
25
+
26
+
27
+ def ensure_user_config(path: Path | None = None) -> Path:
28
+ """Create the default user configuration once without overwriting an existing file."""
29
+ target = (path or default_user_config_path()).expanduser().resolve()
30
+ if target.exists():
31
+ return target
32
+
33
+ content = tomli_w.dumps(AppConfig().model_dump(mode="json", exclude_none=True)).encode()
34
+ target.parent.mkdir(parents=True, exist_ok=True)
35
+ try:
36
+ descriptor = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
37
+ except FileExistsError:
38
+ return target
39
+
40
+ with os.fdopen(descriptor, "wb") as stream:
41
+ stream.write(content)
42
+ stream.flush()
43
+ os.fsync(stream.fileno())
44
+ return target
45
+
46
+
21
47
  def _deep_merge(base: dict[str, Any], overlay: Mapping[str, Any]) -> dict[str, Any]:
22
48
  merged = copy.deepcopy(base)
23
49
  for key, value in overlay.items():
@@ -52,7 +78,7 @@ def load_config(
52
78
  overrides: Mapping[str, Any] | None = None,
53
79
  ) -> AppConfig:
54
80
  workspace = workspace.expanduser().resolve()
55
- default_user = user_config_path("windcode") / "config.toml"
81
+ default_user = default_user_config_path()
56
82
  default_project = workspace / ".windcode" / "config.toml"
57
83
  layers: list[tuple[Path | str, dict[str, Any]]] = [
58
84
  (
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import cast
9
9
  from uuid import uuid4
10
10
 
11
+ from platformdirs import user_state_path
11
12
  from pydantic import TypeAdapter, ValidationError
12
13
 
13
14
  from windcode.config.models import (
@@ -57,13 +58,23 @@ class ExtensionService:
57
58
  workspace: Path,
58
59
  state_store: ExtensionStateStore,
59
60
  plugins_root: Path,
61
+ *,
62
+ user_skill_root: Path | None = None,
60
63
  ) -> None:
61
64
  self.config = config
62
65
  self.workspace = workspace.expanduser().resolve()
63
66
  self.state_store = state_store
64
67
  self.plugins_root = plugins_root
65
- self._user_skill_roots = tuple(Path(path).expanduser() for path in config.skill_roots)
66
- self._project_skill_root = self.workspace / ".windcode" / "skills"
68
+ default_user_skill_root = user_skill_root or user_state_path("windcode") / "skill"
69
+ self._user_skill_roots = tuple(
70
+ dict.fromkeys(
71
+ (
72
+ default_user_skill_root.expanduser().resolve(),
73
+ *(Path(path).expanduser().resolve() for path in config.skill_roots),
74
+ )
75
+ )
76
+ )
77
+ self._project_skill_root = self.workspace / ".windcode" / "skill"
67
78
  loaded = state_store.load()
68
79
  self._state = loaded.state
69
80
  self._state_diagnostics = loaded.diagnostics
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import shutil
5
4
  from collections.abc import AsyncIterator, Mapping
6
5
  from dataclasses import replace
7
6
  from pathlib import Path
@@ -226,21 +225,13 @@ class Windcode:
226
225
  def _resolve_state_root(self, explicit_root: Path | None) -> Path:
227
226
  if explicit_root is not None:
228
227
  return explicit_root.expanduser().resolve()
229
- legacy_root = user_state_path("windcode").expanduser().resolve()
228
+ configured_project_root = self.config.storage.project_state_root
229
+ if configured_project_root is not None:
230
+ return self._configured_state_path(configured_project_root)
230
231
  configured_user_root = self.config.storage.user_storage_root
231
- user_root = (
232
- self._configured_state_path(configured_user_root)
233
- if configured_user_root is not None
234
- else legacy_root / "state"
235
- )
236
- source_root = user_root if user_root.exists() else legacy_root
237
- configured = self.config.storage.project_state_root
238
- if configured is None:
239
- self._migrate_state_root(source_root, user_root)
240
- return user_root
241
- project_root = self._configured_state_path(configured)
242
- self._migrate_state_root(source_root, project_root)
243
- return project_root
232
+ if configured_user_root is not None:
233
+ return self._configured_state_path(configured_user_root)
234
+ return user_state_path("windcode").expanduser().resolve()
244
235
 
245
236
  def _configured_state_path(self, value: str) -> Path:
246
237
  project_root = Path(value).expanduser()
@@ -248,34 +239,6 @@ class Windcode:
248
239
  project_root = self.workspace / project_root
249
240
  return project_root.resolve()
250
241
 
251
- @staticmethod
252
- def _state_manifest(root: Path) -> dict[str, int]:
253
- return {
254
- str(path.relative_to(root)): path.stat().st_size
255
- for path in root.rglob("*")
256
- if path.is_file() and not path.is_symlink()
257
- }
258
-
259
- @classmethod
260
- def _migrate_state_root(cls, source: Path, target: Path) -> None:
261
- """Copy the complete legacy state once, validate it, then atomically install it."""
262
- if source == target or target.exists():
263
- return
264
- target.parent.mkdir(parents=True, exist_ok=True)
265
- temporary_parent = source.parent if target.is_relative_to(source) else target.parent
266
- temporary = temporary_parent / f".{target.name}.migrate-{uuid4().hex}"
267
- try:
268
- if source.exists():
269
- shutil.copytree(source, temporary, copy_function=shutil.copy2)
270
- if cls._state_manifest(source) != cls._state_manifest(temporary):
271
- raise OSError("project state migration validation failed")
272
- else:
273
- temporary.mkdir(parents=True)
274
- temporary.replace(target)
275
- finally:
276
- if temporary.exists():
277
- shutil.rmtree(temporary)
278
-
279
242
  @classmethod
280
243
  def open(
281
244
  cls,
@@ -16,7 +16,7 @@ from textual.theme import Theme
16
16
  from textual.widgets import Static
17
17
 
18
18
  from windcode.auth import CredentialStore, CredentialStoreError
19
- from windcode.config import AppConfig, PermissionMode, ProviderConfig
19
+ from windcode.config import AppConfig, PermissionMode, ProviderConfig, default_user_config_path
20
20
  from windcode.domain.events import (
21
21
  ApprovalRequested,
22
22
  ApprovalResponse,
@@ -86,9 +86,7 @@ class WindcodeApp(App[None]):
86
86
  super().__init__()
87
87
  self.config = config
88
88
  self.workspace = workspace
89
- self.config_file = (
90
- (config_file or self.workspace / ".windcode" / "config.toml").expanduser().resolve()
91
- )
89
+ self.config_file = (config_file or default_user_config_path()).expanduser().resolve()
92
90
  self.model = model
93
91
  self.session_id = session_id
94
92
  self.permission_mode = permission_mode or config.permission.mode.value
@@ -122,6 +120,13 @@ class WindcodeApp(App[None]):
122
120
  return provider.model
123
121
  return "按配置"
124
122
 
123
+ def _model_setup_message(self) -> str | None:
124
+ if self.client.transport_registry.aliases:
125
+ return None
126
+ if self.config.providers:
127
+ return "模型 Provider 尚未连接, 请检查 API Key 或重新配置 Provider"
128
+ return "尚未配置模型 Provider, 请先连接模型后再开始任务"
129
+
125
130
  def _make_banner(self) -> RichText:
126
131
  banner = RichText()
127
132
  banner.append("▣ ", style="bold color(75)")
@@ -172,6 +177,8 @@ class WindcodeApp(App[None]):
172
177
  self.set_class(self.size.width < 60, "narrow")
173
178
  self.query_one("#chat-input", ChatInput).focus()
174
179
  self._update_status("loading MCP" if self.client.required_mcp_loading else "idle")
180
+ if setup_message := self._model_setup_message():
181
+ self.query_one("#welcome-view", WelcomeView).show_notice(setup_message)
175
182
  if self.client.required_mcp_loading:
176
183
  self.query_one("#welcome-view", WelcomeView).start_mcp_loading()
177
184
  self.run_worker(self._load_required_mcp(), group="mcp-startup", exclusive=True)
@@ -188,7 +195,7 @@ class WindcodeApp(App[None]):
188
195
  return
189
196
  self.query_one("#welcome-view", WelcomeView).stop_mcp_loading()
190
197
  self._update_status("idle")
191
- await self._show_system_message("MCP 服务已加载")
198
+ await self._show_system_message(self._model_setup_message() or "MCP 服务已加载")
192
199
 
193
200
  def on_resize(self, event: events.Resize) -> None:
194
201
  self.set_class(event.size.width < 60, "narrow")
@@ -230,7 +237,7 @@ class WindcodeApp(App[None]):
230
237
  await self._show_system_message("没有可复制的内容", error=True)
231
238
  return
232
239
  try:
233
- await self.copy_to_clipboard(text)
240
+ self.copy_to_clipboard(text)
234
241
  preview = text.replace("\n", " ")[:60]
235
242
  await self._show_system_message(
236
243
  f"已复制到剪贴板: {preview}{'...' if len(text) > 60 else ''}"
@@ -458,6 +465,10 @@ class WindcodeApp(App[None]):
458
465
  await self._start_prompt(value)
459
466
 
460
467
  async def _start_prompt(self, value: str) -> None:
468
+ if setup_message := self._model_setup_message():
469
+ await self._show_system_message(setup_message, error=True)
470
+ await self._open_provider_manager()
471
+ return
461
472
  if self.handle is not None and not self.handle.done:
462
473
  self.prompt_queue.append(value)
463
474
  self._update_status(f"运行中 · 队列 {len(self.prompt_queue)}")
@@ -1006,7 +1017,7 @@ class WindcodeApp(App[None]):
1006
1017
  # If the user has selected text, copy it first instead of canceling.
1007
1018
  selected = self.screen.get_selected_text()
1008
1019
  if selected:
1009
- await self.copy_to_clipboard(selected)
1020
+ self.copy_to_clipboard(selected)
1010
1021
  self.screen.clear_selection()
1011
1022
  return
1012
1023
  if self.handle is not None and not self.handle.done:
@@ -1498,7 +1498,7 @@ wheels = [
1498
1498
 
1499
1499
  [[package]]
1500
1500
  name = "windcode"
1501
- version = "0.1.0"
1501
+ version = "0.1.2"
1502
1502
  source = { editable = "." }
1503
1503
  dependencies = [
1504
1504
  { name = "aiohttp" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes