windcode 0.1.0__py3-none-any.whl

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 (143) hide show
  1. windcode/__init__.py +6 -0
  2. windcode/__main__.py +4 -0
  3. windcode/auth/__init__.py +11 -0
  4. windcode/auth/store.py +90 -0
  5. windcode/cli.py +181 -0
  6. windcode/config/__init__.py +41 -0
  7. windcode/config/loader.py +117 -0
  8. windcode/config/models.py +203 -0
  9. windcode/config/writer.py +74 -0
  10. windcode/context/__init__.py +18 -0
  11. windcode/context/compactor.py +72 -0
  12. windcode/context/estimator.py +89 -0
  13. windcode/context/truncation.py +87 -0
  14. windcode/domain/__init__.py +1 -0
  15. windcode/domain/errors.py +33 -0
  16. windcode/domain/events.py +597 -0
  17. windcode/domain/messages.py +226 -0
  18. windcode/domain/models.py +73 -0
  19. windcode/domain/subagents.py +263 -0
  20. windcode/domain/tools.py +55 -0
  21. windcode/extensions/__init__.py +27 -0
  22. windcode/extensions/commands.py +25 -0
  23. windcode/extensions/discovery.py +139 -0
  24. windcode/extensions/events.py +58 -0
  25. windcode/extensions/hooks/__init__.py +4 -0
  26. windcode/extensions/hooks/dispatcher.py +107 -0
  27. windcode/extensions/hooks/executor.py +49 -0
  28. windcode/extensions/hooks/loader.py +101 -0
  29. windcode/extensions/hooks/models.py +109 -0
  30. windcode/extensions/mcp/__init__.py +10 -0
  31. windcode/extensions/mcp/adapter.py +101 -0
  32. windcode/extensions/mcp/catalog.py +153 -0
  33. windcode/extensions/mcp/client.py +273 -0
  34. windcode/extensions/mcp/runtime.py +144 -0
  35. windcode/extensions/mcp/tools.py +570 -0
  36. windcode/extensions/models.py +168 -0
  37. windcode/extensions/paths.py +71 -0
  38. windcode/extensions/plugins/__init__.py +3 -0
  39. windcode/extensions/plugins/installer.py +93 -0
  40. windcode/extensions/plugins/manifest.py +166 -0
  41. windcode/extensions/runtime.py +366 -0
  42. windcode/extensions/service.py +437 -0
  43. windcode/extensions/skills/__init__.py +3 -0
  44. windcode/extensions/skills/loader.py +67 -0
  45. windcode/extensions/skills/parser.py +57 -0
  46. windcode/extensions/skills/tools.py +213 -0
  47. windcode/extensions/snapshot.py +57 -0
  48. windcode/extensions/state.py +158 -0
  49. windcode/instructions/__init__.py +8 -0
  50. windcode/instructions/loader.py +50 -0
  51. windcode/memory/__init__.py +57 -0
  52. windcode/memory/extraction.py +83 -0
  53. windcode/memory/models.py +218 -0
  54. windcode/memory/refiner.py +189 -0
  55. windcode/memory/security.py +23 -0
  56. windcode/memory/service.py +179 -0
  57. windcode/memory/store.py +362 -0
  58. windcode/observability/__init__.py +4 -0
  59. windcode/observability/redaction.py +95 -0
  60. windcode/observability/trace.py +115 -0
  61. windcode/policy/__init__.py +22 -0
  62. windcode/policy/engine.py +137 -0
  63. windcode/policy/models.py +69 -0
  64. windcode/providers/__init__.py +29 -0
  65. windcode/providers/_utils.py +19 -0
  66. windcode/providers/anthropic.py +215 -0
  67. windcode/providers/base.py +46 -0
  68. windcode/providers/catalog.py +119 -0
  69. windcode/providers/errors.py +96 -0
  70. windcode/providers/openai_compat.py +231 -0
  71. windcode/providers/openai_responses.py +189 -0
  72. windcode/providers/registry.py +105 -0
  73. windcode/runtime/__init__.py +15 -0
  74. windcode/runtime/control.py +89 -0
  75. windcode/runtime/event_bus.py +67 -0
  76. windcode/runtime/loop.py +510 -0
  77. windcode/runtime/prompts.py +132 -0
  78. windcode/runtime/report.py +64 -0
  79. windcode/runtime/retry.py +73 -0
  80. windcode/runtime/scheduler.py +194 -0
  81. windcode/runtime/subagents/__init__.py +28 -0
  82. windcode/runtime/subagents/approvals.py +88 -0
  83. windcode/runtime/subagents/budgets.py +79 -0
  84. windcode/runtime/subagents/coordinator.py +714 -0
  85. windcode/runtime/subagents/factory.py +311 -0
  86. windcode/runtime/subagents/roles.py +74 -0
  87. windcode/runtime/subagents/verification.py +53 -0
  88. windcode/sandbox/__init__.py +3 -0
  89. windcode/sandbox/bwrap.py +79 -0
  90. windcode/sdk.py +1259 -0
  91. windcode/sessions/__init__.py +23 -0
  92. windcode/sessions/artifacts.py +69 -0
  93. windcode/sessions/models.py +104 -0
  94. windcode/sessions/store.py +167 -0
  95. windcode/sessions/tree.py +35 -0
  96. windcode/tools/__init__.py +10 -0
  97. windcode/tools/apply_patch.py +191 -0
  98. windcode/tools/ask_user.py +58 -0
  99. windcode/tools/builtins.py +44 -0
  100. windcode/tools/edit_file.py +54 -0
  101. windcode/tools/filesystem.py +77 -0
  102. windcode/tools/glob.py +35 -0
  103. windcode/tools/grep.py +66 -0
  104. windcode/tools/memory.py +400 -0
  105. windcode/tools/read_file.py +54 -0
  106. windcode/tools/registry.py +120 -0
  107. windcode/tools/shell.py +159 -0
  108. windcode/tools/subagents/__init__.py +31 -0
  109. windcode/tools/subagents/cancel.py +35 -0
  110. windcode/tools/subagents/integrate.py +54 -0
  111. windcode/tools/subagents/list.py +46 -0
  112. windcode/tools/subagents/spawn.py +86 -0
  113. windcode/tools/subagents/wait.py +74 -0
  114. windcode/tools/write_file.py +61 -0
  115. windcode/tui/__init__.py +3 -0
  116. windcode/tui/app.py +1015 -0
  117. windcode/tui/commands.py +90 -0
  118. windcode/tui/permission_display.py +24 -0
  119. windcode/tui/styles.tcss +591 -0
  120. windcode/tui/widgets/__init__.py +31 -0
  121. windcode/tui/widgets/approval.py +98 -0
  122. windcode/tui/widgets/command_menu.py +90 -0
  123. windcode/tui/widgets/extensions.py +48 -0
  124. windcode/tui/widgets/input.py +110 -0
  125. windcode/tui/widgets/memory.py +143 -0
  126. windcode/tui/widgets/messages.py +299 -0
  127. windcode/tui/widgets/models.py +565 -0
  128. windcode/tui/widgets/question.py +46 -0
  129. windcode/tui/widgets/sessions.py +68 -0
  130. windcode/tui/widgets/status.py +46 -0
  131. windcode/tui/widgets/subagents.py +195 -0
  132. windcode/tui/widgets/tools.py +66 -0
  133. windcode/tui/widgets/welcome.py +149 -0
  134. windcode/types.py +80 -0
  135. windcode/worktrees/__init__.py +24 -0
  136. windcode/worktrees/git.py +92 -0
  137. windcode/worktrees/manager.py +265 -0
  138. windcode/worktrees/models.py +62 -0
  139. windcode-0.1.0.dist-info/METADATA +207 -0
  140. windcode-0.1.0.dist-info/RECORD +143 -0
  141. windcode-0.1.0.dist-info/WHEEL +4 -0
  142. windcode-0.1.0.dist-info/entry_points.txt +2 -0
  143. windcode-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,299 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from time import monotonic
5
+
6
+ from rich.text import Text as RichText
7
+ from textual.containers import Horizontal, Vertical, VerticalScroll
8
+ from textual.timer import Timer
9
+ from textual.widget import Widget
10
+ from textual.widgets import Markdown, Static
11
+
12
+ from windcode.domain.events import (
13
+ AgentEventType,
14
+ ModelFallback,
15
+ ModelRetrying,
16
+ ModelStarted,
17
+ ReasoningStatus,
18
+ RunCancelled,
19
+ RunCompleted,
20
+ RunFailed,
21
+ TextDeltaEvent,
22
+ ToolStarted,
23
+ )
24
+ from windcode.domain.messages import Message, Role, TextBlock
25
+
26
+ SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
27
+
28
+
29
+ class MessageStream(VerticalScroll):
30
+ """MewCode-style chat stream with one mutable row per model response."""
31
+
32
+ def __init__(
33
+ self,
34
+ *children: Widget,
35
+ name: str | None = None,
36
+ id: str | None = None,
37
+ classes: str | None = None,
38
+ disabled: bool = False,
39
+ clock: Callable[[], float] = monotonic,
40
+ ) -> None:
41
+ super().__init__(*children, name=name, id=id, classes=classes, disabled=disabled)
42
+ self._clock = clock
43
+ self._ai_row: Vertical | None = None
44
+ self._streaming_label: Static | None = None
45
+ self._accumulated_text = ""
46
+ self._reasoning_text = ""
47
+ self._spinner_label: Static | None = None
48
+ self._spinner_timer: Timer | None = None
49
+ self._spinner_index = 0
50
+ self._started_at = 0.0
51
+ self._thinking_paused_at: float | None = None
52
+ self._thinking_paused_seconds = 0.0
53
+ self._thinking_pause_keys: set[str] = set()
54
+ self._thinking_active = False
55
+ self._finished_thinking_seconds = 0.0
56
+ self._waiting_for_next_model = False
57
+ self._last_ai_text = ""
58
+
59
+ async def _mount_if_attached(self, widget: Widget) -> None:
60
+ if self.is_attached:
61
+ await self.mount(widget)
62
+
63
+ async def add_user_message(self, text: str) -> None:
64
+ content = RichText()
65
+ content.append("❯ ", style="bold color(80)") # noqa: RUF001
66
+ content.append(text, style="bold color(255)")
67
+ row = Vertical(Static(content, classes="message user-message"), classes="user-row")
68
+ await self._mount_if_attached(row)
69
+ if self.is_attached:
70
+ self.call_after_refresh(self.scroll_end, animate=False)
71
+
72
+ async def add_ai_message(self, text: str) -> None:
73
+ segment = Horizontal(
74
+ Static(RichText("●", style="bold color(99)"), classes="ai-prefix"),
75
+ Markdown(text, classes="message ai-message"),
76
+ classes="ai-segment",
77
+ )
78
+ row = Vertical(
79
+ segment,
80
+ classes="ai-row",
81
+ )
82
+ await self._mount_if_attached(row)
83
+ if self.is_attached:
84
+ self.call_after_refresh(self.scroll_end, animate=False)
85
+
86
+ async def load_history(self, messages: tuple[Message, ...]) -> None:
87
+ await self.clear()
88
+ for message in messages:
89
+ text = "".join(
90
+ block.text for block in message.content if isinstance(block, TextBlock)
91
+ ).strip()
92
+ if not text:
93
+ continue
94
+ if message.role is Role.USER:
95
+ await self.add_user_message(text)
96
+ elif message.role is Role.ASSISTANT:
97
+ await self.add_ai_message(text)
98
+
99
+ async def add_system_message(self, text: str, *, error: bool = False) -> None:
100
+ prefix = "✖ " if error else " "
101
+ classes = "message error-message" if error else "message system-message"
102
+ await self._mount_if_attached(Static(f"{prefix}{text}", classes=classes))
103
+ if self.is_attached:
104
+ self.call_after_refresh(self.scroll_end, animate=False)
105
+
106
+ async def begin_run(self) -> None:
107
+ self._ai_row = None
108
+ self._streaming_label = None
109
+ self._accumulated_text = ""
110
+ self._reasoning_text = ""
111
+ self._waiting_for_next_model = False
112
+ await self._new_ai_row()
113
+ self._started_at = self._clock()
114
+ self._thinking_paused_at = None
115
+ self._thinking_paused_seconds = 0.0
116
+ self._thinking_pause_keys.clear()
117
+ self._thinking_active = True
118
+ self._finished_thinking_seconds = 0.0
119
+ self._spinner_index = 0
120
+ self._spinner_label = Static(" ⠋ 处理中...", id="spinner-live")
121
+ await self._place_spinner_at_end()
122
+ if self.is_attached:
123
+ self._spinner_timer = self.set_interval(0.08, self._tick_spinner)
124
+ self.call_after_refresh(self.scroll_end, animate=False)
125
+
126
+ async def _new_ai_row(self) -> None:
127
+ self._streaming_label = Static("", classes="message ai-message")
128
+ self._ai_row = Vertical(classes="ai-row")
129
+ await self._mount_if_attached(self._ai_row)
130
+ if self._ai_row.is_attached:
131
+ await self._ai_row.mount(self._streaming_label)
132
+ await self._place_spinner_at_end()
133
+ self._accumulated_text = ""
134
+
135
+ async def _place_spinner_at_end(self) -> None:
136
+ """Keep the run activity status after the newest response content."""
137
+ if self._ai_row is None or not self._ai_row.is_attached or self._spinner_label is None:
138
+ return
139
+ if self._spinner_label.parent is not self._ai_row:
140
+ if self._spinner_label.is_attached:
141
+ await self._spinner_label.remove()
142
+ await self._ai_row.mount(self._spinner_label)
143
+ return
144
+ if self._ai_row.children[-1] is not self._spinner_label:
145
+ self._ai_row.move_child(self._spinner_label, after=self._ai_row.children[-1])
146
+
147
+ async def _ensure_streaming_label(self) -> Static | None:
148
+ if self._ai_row is None:
149
+ await self._new_ai_row()
150
+ if self._streaming_label is None and self._ai_row is not None:
151
+ self._streaming_label = Static("", classes="message ai-message")
152
+ if self._ai_row.is_attached:
153
+ await self._ai_row.mount(self._streaming_label)
154
+ await self._place_spinner_at_end()
155
+ return self._streaming_label
156
+
157
+ async def _append_text(self, text: str) -> None:
158
+ label = await self._ensure_streaming_label()
159
+ self._accumulated_text += text
160
+ if label is not None:
161
+ content = RichText()
162
+ content.append("● ", style="bold color(99)")
163
+ content.append(self._accumulated_text)
164
+ label.update(content)
165
+ await self._place_spinner_at_end()
166
+ if self.is_attached:
167
+ self.call_after_refresh(self.scroll_end, animate=False)
168
+
169
+ async def _finalize_text(self) -> None:
170
+ """Replace the inexpensive streaming label with rich Markdown output."""
171
+ if not self._accumulated_text or self._ai_row is None:
172
+ return
173
+ if self._streaming_label is not None and self._streaming_label.is_attached:
174
+ await self._streaming_label.remove()
175
+ self._last_ai_text = self._accumulated_text
176
+ if self._ai_row.is_attached:
177
+ await self._ai_row.mount(
178
+ Horizontal(
179
+ Static(RichText("●", style="bold color(99)"), classes="ai-prefix"),
180
+ Markdown(self._accumulated_text, classes="message ai-message"),
181
+ classes="ai-segment",
182
+ )
183
+ )
184
+ self._streaming_label = None
185
+ self._accumulated_text = ""
186
+ await self._place_spinner_at_end()
187
+
188
+ async def begin_block(self) -> None:
189
+ await self._finalize_text()
190
+ if self._streaming_label is not None and not self._accumulated_text:
191
+ await self._streaming_label.remove()
192
+ self._streaming_label = None
193
+ self._accumulated_text = ""
194
+
195
+ async def mount_in_ai_row(self, widget: Widget) -> None:
196
+ if self._ai_row is None:
197
+ await self._new_ai_row()
198
+ if self._ai_row is not None:
199
+ await self._ai_row.mount(widget)
200
+ await self._place_spinner_at_end()
201
+
202
+ def _tick_spinner(self) -> None:
203
+ self._spinner_index += 1
204
+ frame = SPINNER_FRAMES[self._spinner_index % len(SPINNER_FRAMES)]
205
+ if self._spinner_label is not None:
206
+ if self._thinking_pause_keys:
207
+ self._spinner_label.update(" 等待审批...")
208
+ else:
209
+ self._spinner_label.update(f" {frame} 处理中... ({self.thinking_seconds:.0f}s)")
210
+
211
+ @property
212
+ def thinking_seconds(self) -> float:
213
+ if not self._thinking_active:
214
+ return self._finished_thinking_seconds
215
+ now = self._clock()
216
+ paused = self._thinking_paused_seconds
217
+ if self._thinking_paused_at is not None:
218
+ paused += now - self._thinking_paused_at
219
+ return max(0.0, now - self._started_at - paused)
220
+
221
+ @property
222
+ def last_ai_text(self) -> str:
223
+ return self._last_ai_text
224
+
225
+ def pause_thinking(self, key: str) -> None:
226
+ if not self._thinking_active or key in self._thinking_pause_keys:
227
+ return
228
+ if not self._thinking_pause_keys:
229
+ self._thinking_paused_at = self._clock()
230
+ self._thinking_pause_keys.add(key)
231
+
232
+ def resume_thinking(self, key: str) -> None:
233
+ if key not in self._thinking_pause_keys:
234
+ return
235
+ self._thinking_pause_keys.remove(key)
236
+ if not self._thinking_pause_keys and self._thinking_paused_at is not None:
237
+ self._thinking_paused_seconds += self._clock() - self._thinking_paused_at
238
+ self._thinking_paused_at = None
239
+
240
+ async def finish_run(self) -> None:
241
+ await self._finalize_text()
242
+ if self._streaming_label is not None and not self._accumulated_text:
243
+ await self._streaming_label.remove()
244
+ self._streaming_label = None
245
+ if self._spinner_timer is not None:
246
+ self._spinner_timer.stop()
247
+ self._spinner_timer = None
248
+ if self._spinner_label is not None:
249
+ await self._spinner_label.remove()
250
+ self._spinner_label = None
251
+ elapsed = self.thinking_seconds
252
+ self._finished_thinking_seconds = elapsed
253
+ self._thinking_active = False
254
+ self._thinking_paused_at = None
255
+ self._thinking_pause_keys.clear()
256
+ if self._ai_row is not None and self._ai_row.is_attached:
257
+ await self._ai_row.mount(
258
+ Static(
259
+ f"✻ 本轮耗时 {elapsed:.1f} 秒",
260
+ classes="message thinking-done",
261
+ )
262
+ )
263
+
264
+ async def clear(self) -> None:
265
+ await self.finish_run()
266
+ self._ai_row = None
267
+ self._streaming_label = None
268
+ self._accumulated_text = ""
269
+ self._reasoning_text = ""
270
+ if self.is_attached:
271
+ await self.remove_children()
272
+
273
+ async def apply_event(self, event: AgentEventType) -> None:
274
+ if isinstance(event, TextDeltaEvent):
275
+ await self._append_text(event.text)
276
+ elif isinstance(event, ReasoningStatus):
277
+ # Provider reasoning arrives as deltas. MewCode keeps it out of chat history.
278
+ self._reasoning_text += event.status
279
+ elif isinstance(event, ModelStarted):
280
+ if self._waiting_for_next_model:
281
+ await self._new_ai_row()
282
+ self._waiting_for_next_model = False
283
+ elif isinstance(event, ModelRetrying):
284
+ await self.add_system_message(f"正在重试: {event.reason}")
285
+ elif isinstance(event, ModelFallback):
286
+ await self.add_system_message(
287
+ f"模型已切换: {event.from_model} -> {event.to_model}; 原因: {event.reason}"
288
+ )
289
+ elif isinstance(event, ToolStarted):
290
+ await self.begin_block()
291
+ self._waiting_for_next_model = True
292
+ elif isinstance(event, RunCompleted):
293
+ await self.finish_run()
294
+ elif isinstance(event, RunFailed):
295
+ await self.finish_run()
296
+ await self.add_system_message(f"{event.category}: {event.message}", error=True)
297
+ elif isinstance(event, RunCancelled):
298
+ await self.finish_run()
299
+ await self.add_system_message("操作已取消")