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.
- windcode/__init__.py +6 -0
- windcode/__main__.py +4 -0
- windcode/auth/__init__.py +11 -0
- windcode/auth/store.py +90 -0
- windcode/cli.py +181 -0
- windcode/config/__init__.py +41 -0
- windcode/config/loader.py +117 -0
- windcode/config/models.py +203 -0
- windcode/config/writer.py +74 -0
- windcode/context/__init__.py +18 -0
- windcode/context/compactor.py +72 -0
- windcode/context/estimator.py +89 -0
- windcode/context/truncation.py +87 -0
- windcode/domain/__init__.py +1 -0
- windcode/domain/errors.py +33 -0
- windcode/domain/events.py +597 -0
- windcode/domain/messages.py +226 -0
- windcode/domain/models.py +73 -0
- windcode/domain/subagents.py +263 -0
- windcode/domain/tools.py +55 -0
- windcode/extensions/__init__.py +27 -0
- windcode/extensions/commands.py +25 -0
- windcode/extensions/discovery.py +139 -0
- windcode/extensions/events.py +58 -0
- windcode/extensions/hooks/__init__.py +4 -0
- windcode/extensions/hooks/dispatcher.py +107 -0
- windcode/extensions/hooks/executor.py +49 -0
- windcode/extensions/hooks/loader.py +101 -0
- windcode/extensions/hooks/models.py +109 -0
- windcode/extensions/mcp/__init__.py +10 -0
- windcode/extensions/mcp/adapter.py +101 -0
- windcode/extensions/mcp/catalog.py +153 -0
- windcode/extensions/mcp/client.py +273 -0
- windcode/extensions/mcp/runtime.py +144 -0
- windcode/extensions/mcp/tools.py +570 -0
- windcode/extensions/models.py +168 -0
- windcode/extensions/paths.py +71 -0
- windcode/extensions/plugins/__init__.py +3 -0
- windcode/extensions/plugins/installer.py +93 -0
- windcode/extensions/plugins/manifest.py +166 -0
- windcode/extensions/runtime.py +366 -0
- windcode/extensions/service.py +437 -0
- windcode/extensions/skills/__init__.py +3 -0
- windcode/extensions/skills/loader.py +67 -0
- windcode/extensions/skills/parser.py +57 -0
- windcode/extensions/skills/tools.py +213 -0
- windcode/extensions/snapshot.py +57 -0
- windcode/extensions/state.py +158 -0
- windcode/instructions/__init__.py +8 -0
- windcode/instructions/loader.py +50 -0
- windcode/memory/__init__.py +57 -0
- windcode/memory/extraction.py +83 -0
- windcode/memory/models.py +218 -0
- windcode/memory/refiner.py +189 -0
- windcode/memory/security.py +23 -0
- windcode/memory/service.py +179 -0
- windcode/memory/store.py +362 -0
- windcode/observability/__init__.py +4 -0
- windcode/observability/redaction.py +95 -0
- windcode/observability/trace.py +115 -0
- windcode/policy/__init__.py +22 -0
- windcode/policy/engine.py +137 -0
- windcode/policy/models.py +69 -0
- windcode/providers/__init__.py +29 -0
- windcode/providers/_utils.py +19 -0
- windcode/providers/anthropic.py +215 -0
- windcode/providers/base.py +46 -0
- windcode/providers/catalog.py +119 -0
- windcode/providers/errors.py +96 -0
- windcode/providers/openai_compat.py +231 -0
- windcode/providers/openai_responses.py +189 -0
- windcode/providers/registry.py +105 -0
- windcode/runtime/__init__.py +15 -0
- windcode/runtime/control.py +89 -0
- windcode/runtime/event_bus.py +67 -0
- windcode/runtime/loop.py +510 -0
- windcode/runtime/prompts.py +132 -0
- windcode/runtime/report.py +64 -0
- windcode/runtime/retry.py +73 -0
- windcode/runtime/scheduler.py +194 -0
- windcode/runtime/subagents/__init__.py +28 -0
- windcode/runtime/subagents/approvals.py +88 -0
- windcode/runtime/subagents/budgets.py +79 -0
- windcode/runtime/subagents/coordinator.py +714 -0
- windcode/runtime/subagents/factory.py +311 -0
- windcode/runtime/subagents/roles.py +74 -0
- windcode/runtime/subagents/verification.py +53 -0
- windcode/sandbox/__init__.py +3 -0
- windcode/sandbox/bwrap.py +79 -0
- windcode/sdk.py +1259 -0
- windcode/sessions/__init__.py +23 -0
- windcode/sessions/artifacts.py +69 -0
- windcode/sessions/models.py +104 -0
- windcode/sessions/store.py +167 -0
- windcode/sessions/tree.py +35 -0
- windcode/tools/__init__.py +10 -0
- windcode/tools/apply_patch.py +191 -0
- windcode/tools/ask_user.py +58 -0
- windcode/tools/builtins.py +44 -0
- windcode/tools/edit_file.py +54 -0
- windcode/tools/filesystem.py +77 -0
- windcode/tools/glob.py +35 -0
- windcode/tools/grep.py +66 -0
- windcode/tools/memory.py +400 -0
- windcode/tools/read_file.py +54 -0
- windcode/tools/registry.py +120 -0
- windcode/tools/shell.py +159 -0
- windcode/tools/subagents/__init__.py +31 -0
- windcode/tools/subagents/cancel.py +35 -0
- windcode/tools/subagents/integrate.py +54 -0
- windcode/tools/subagents/list.py +46 -0
- windcode/tools/subagents/spawn.py +86 -0
- windcode/tools/subagents/wait.py +74 -0
- windcode/tools/write_file.py +61 -0
- windcode/tui/__init__.py +3 -0
- windcode/tui/app.py +1015 -0
- windcode/tui/commands.py +90 -0
- windcode/tui/permission_display.py +24 -0
- windcode/tui/styles.tcss +591 -0
- windcode/tui/widgets/__init__.py +31 -0
- windcode/tui/widgets/approval.py +98 -0
- windcode/tui/widgets/command_menu.py +90 -0
- windcode/tui/widgets/extensions.py +48 -0
- windcode/tui/widgets/input.py +110 -0
- windcode/tui/widgets/memory.py +143 -0
- windcode/tui/widgets/messages.py +299 -0
- windcode/tui/widgets/models.py +565 -0
- windcode/tui/widgets/question.py +46 -0
- windcode/tui/widgets/sessions.py +68 -0
- windcode/tui/widgets/status.py +46 -0
- windcode/tui/widgets/subagents.py +195 -0
- windcode/tui/widgets/tools.py +66 -0
- windcode/tui/widgets/welcome.py +149 -0
- windcode/types.py +80 -0
- windcode/worktrees/__init__.py +24 -0
- windcode/worktrees/git.py +92 -0
- windcode/worktrees/manager.py +265 -0
- windcode/worktrees/models.py +62 -0
- windcode-0.1.0.dist-info/METADATA +207 -0
- windcode-0.1.0.dist-info/RECORD +143 -0
- windcode-0.1.0.dist-info/WHEEL +4 -0
- windcode-0.1.0.dist-info/entry_points.txt +2 -0
- 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("操作已取消")
|