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,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rich.text import Text as RichText
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.containers import Horizontal
|
|
6
|
+
from textual.widgets import Static
|
|
7
|
+
|
|
8
|
+
from windcode.tui.permission_display import permission_label, permission_style
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StatusBar(Horizontal):
|
|
12
|
+
def compose(self) -> ComposeResult:
|
|
13
|
+
yield Static("", id="mode-label")
|
|
14
|
+
yield Static("", id="sandbox-label")
|
|
15
|
+
yield Static("", id="model-label")
|
|
16
|
+
|
|
17
|
+
def set_state(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
model: str | None,
|
|
21
|
+
permission: str,
|
|
22
|
+
sandbox: bool,
|
|
23
|
+
state: str,
|
|
24
|
+
delegation: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
states = {
|
|
27
|
+
"idle": "空闲",
|
|
28
|
+
"running": "运行中",
|
|
29
|
+
"completed": "已完成",
|
|
30
|
+
"unverified": "已完成 · 未验证",
|
|
31
|
+
"failed": "失败",
|
|
32
|
+
"cancelled": "已取消",
|
|
33
|
+
}
|
|
34
|
+
mode_content = RichText()
|
|
35
|
+
mode_content.append(f" {states.get(state, state)}", style="#5fa8e8")
|
|
36
|
+
mode_content.append(" · ", style="#88939b")
|
|
37
|
+
mode_content.append(permission_label(permission), style=permission_style(permission))
|
|
38
|
+
self.query_one("#mode-label", Static).update(mode_content)
|
|
39
|
+
delegation_label = {"explicit": "显式", "proactive": "主动"}.get(
|
|
40
|
+
delegation or "", delegation or ""
|
|
41
|
+
)
|
|
42
|
+
suffix = f" · 委派: {delegation_label}" if delegation_label else ""
|
|
43
|
+
self.query_one("#sandbox-label", Static).update(
|
|
44
|
+
f"沙箱: {'开启' if sandbox else '关闭'}{suffix}"
|
|
45
|
+
)
|
|
46
|
+
self.query_one("#model-label", Static).update(model or "按配置")
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding, BindingType
|
|
9
|
+
from textual.containers import Vertical
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
|
|
12
|
+
from windcode.domain.events import (
|
|
13
|
+
SubagentBlocked,
|
|
14
|
+
SubagentCancelled,
|
|
15
|
+
SubagentCleanup,
|
|
16
|
+
SubagentCompleted,
|
|
17
|
+
SubagentConflict,
|
|
18
|
+
SubagentEvent,
|
|
19
|
+
SubagentFailed,
|
|
20
|
+
SubagentIntegrated,
|
|
21
|
+
SubagentProgress,
|
|
22
|
+
SubagentQueued,
|
|
23
|
+
SubagentStarted,
|
|
24
|
+
)
|
|
25
|
+
from windcode.domain.models import Usage
|
|
26
|
+
|
|
27
|
+
STATUS_LABELS = {
|
|
28
|
+
"queued": "排队",
|
|
29
|
+
"running": "运行",
|
|
30
|
+
"blocked": "阻塞",
|
|
31
|
+
"completed": "完成",
|
|
32
|
+
"failed": "失败",
|
|
33
|
+
"cancelled": "取消",
|
|
34
|
+
"conflict": "冲突",
|
|
35
|
+
"integrated": "已集成",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class SubagentViewState:
|
|
41
|
+
subagent_id: str
|
|
42
|
+
task_index: int
|
|
43
|
+
role: str
|
|
44
|
+
task_name: str
|
|
45
|
+
status: str = "queued"
|
|
46
|
+
summary: str = ""
|
|
47
|
+
activity: str = "等待调度"
|
|
48
|
+
usage: Usage = field(default_factory=Usage)
|
|
49
|
+
created_at: datetime | None = None
|
|
50
|
+
updated_at: datetime | None = None
|
|
51
|
+
commit: str | None = None
|
|
52
|
+
verification: tuple[str, ...] = ()
|
|
53
|
+
retained_path: str | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def elapsed_seconds(self) -> int:
|
|
57
|
+
if self.created_at is None or self.updated_at is None:
|
|
58
|
+
return 0
|
|
59
|
+
return max(0, int((self.updated_at - self.created_at).total_seconds()))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SubagentRow(Vertical, can_focus=True):
|
|
63
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
64
|
+
Binding("enter", "toggle_details", "展开详情", priority=True),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def __init__(self, state: SubagentViewState) -> None:
|
|
68
|
+
super().__init__(classes="subagent-row")
|
|
69
|
+
self.state = state
|
|
70
|
+
self.expanded = False
|
|
71
|
+
|
|
72
|
+
def compose(self) -> ComposeResult:
|
|
73
|
+
yield Static(classes="subagent-summary")
|
|
74
|
+
yield Static(classes="subagent-details")
|
|
75
|
+
|
|
76
|
+
def on_mount(self) -> None:
|
|
77
|
+
self.refresh_state()
|
|
78
|
+
|
|
79
|
+
def refresh_state(self) -> None:
|
|
80
|
+
status = STATUS_LABELS.get(self.state.status, self.state.status)
|
|
81
|
+
tokens = self.state.usage.input_tokens + self.state.usage.output_tokens
|
|
82
|
+
self.set_classes(f"subagent-row subagent-status-{self.state.status}")
|
|
83
|
+
self.query_one(".subagent-summary", Static).update(
|
|
84
|
+
f"[{status}] {self.state.role} · {self.state.task_name}\n"
|
|
85
|
+
f"{self.state.elapsed_seconds}s · {tokens} tokens · {self.state.activity}"
|
|
86
|
+
)
|
|
87
|
+
details = self.query_one(".subagent-details", Static)
|
|
88
|
+
detail_lines = [self.state.summary] if self.state.summary else []
|
|
89
|
+
if self.state.commit:
|
|
90
|
+
detail_lines.append(f"提交: {self.state.commit}")
|
|
91
|
+
if self.state.verification:
|
|
92
|
+
detail_lines.append("验证: " + "; ".join(self.state.verification))
|
|
93
|
+
if self.state.retained_path:
|
|
94
|
+
detail_lines.append(f"保留 Worktree: {self.state.retained_path}")
|
|
95
|
+
details.update("\n".join(detail_lines))
|
|
96
|
+
details.display = self.expanded and bool(detail_lines)
|
|
97
|
+
|
|
98
|
+
def action_toggle_details(self) -> None:
|
|
99
|
+
self.expanded = not self.expanded
|
|
100
|
+
self.refresh_state()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SubagentGroup(Vertical):
|
|
104
|
+
"""One stable, incrementally updated group for a root run's child tasks."""
|
|
105
|
+
|
|
106
|
+
def __init__(self) -> None:
|
|
107
|
+
super().__init__(classes="subagent-group")
|
|
108
|
+
self.states: dict[str, SubagentViewState] = {}
|
|
109
|
+
self.rows: dict[str, SubagentRow] = {}
|
|
110
|
+
|
|
111
|
+
def compose(self) -> ComposeResult:
|
|
112
|
+
yield Static("子智能体", classes="subagent-group-title")
|
|
113
|
+
|
|
114
|
+
async def apply_event(self, event: SubagentEvent) -> None:
|
|
115
|
+
state = self.states.get(event.subagent_id)
|
|
116
|
+
if state is None:
|
|
117
|
+
state = SubagentViewState(
|
|
118
|
+
subagent_id=event.subagent_id,
|
|
119
|
+
task_index=event.task_index,
|
|
120
|
+
role=event.role,
|
|
121
|
+
task_name=event.task_name,
|
|
122
|
+
summary=event.summary,
|
|
123
|
+
created_at=event.created_at,
|
|
124
|
+
updated_at=event.created_at,
|
|
125
|
+
)
|
|
126
|
+
self.states[event.subagent_id] = state
|
|
127
|
+
state.updated_at = event.created_at
|
|
128
|
+
state.summary = event.summary or state.summary
|
|
129
|
+
self._apply_details(state, event)
|
|
130
|
+
|
|
131
|
+
row = self.rows.get(event.subagent_id)
|
|
132
|
+
if row is None:
|
|
133
|
+
row = SubagentRow(state)
|
|
134
|
+
self.rows[event.subagent_id] = row
|
|
135
|
+
before = next(
|
|
136
|
+
(
|
|
137
|
+
existing
|
|
138
|
+
for child_id, existing in self.rows.items()
|
|
139
|
+
if child_id != event.subagent_id
|
|
140
|
+
and self.states[child_id].task_index > state.task_index
|
|
141
|
+
and existing.is_attached
|
|
142
|
+
),
|
|
143
|
+
None,
|
|
144
|
+
)
|
|
145
|
+
await self.mount(row, before=before)
|
|
146
|
+
row.refresh_state()
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _apply_details(state: SubagentViewState, event: SubagentEvent) -> None:
|
|
150
|
+
if isinstance(event, SubagentQueued):
|
|
151
|
+
state.status = "queued"
|
|
152
|
+
state.activity = "等待调度"
|
|
153
|
+
elif isinstance(event, SubagentStarted):
|
|
154
|
+
state.status = "running"
|
|
155
|
+
state.activity = "已启动"
|
|
156
|
+
elif isinstance(event, SubagentProgress):
|
|
157
|
+
state.status = "running"
|
|
158
|
+
state.activity = event.activity or "运行中"
|
|
159
|
+
if event.usage != Usage():
|
|
160
|
+
state.usage = event.usage
|
|
161
|
+
elif isinstance(event, SubagentBlocked):
|
|
162
|
+
state.status = "blocked"
|
|
163
|
+
state.activity = event.reason or "需要父智能体处理"
|
|
164
|
+
elif isinstance(event, SubagentCompleted):
|
|
165
|
+
state.status = "completed"
|
|
166
|
+
state.activity = "等待检查或集成"
|
|
167
|
+
state.commit = event.commit
|
|
168
|
+
state.verification = event.verification
|
|
169
|
+
state.usage = event.usage
|
|
170
|
+
elif isinstance(event, SubagentFailed):
|
|
171
|
+
state.status = "failed"
|
|
172
|
+
state.activity = event.message or event.category
|
|
173
|
+
if event.usage != Usage():
|
|
174
|
+
state.usage = event.usage
|
|
175
|
+
elif isinstance(event, SubagentCancelled):
|
|
176
|
+
state.status = "cancelled"
|
|
177
|
+
state.activity = event.reason
|
|
178
|
+
if event.usage != Usage():
|
|
179
|
+
state.usage = event.usage
|
|
180
|
+
elif isinstance(event, SubagentIntegrated):
|
|
181
|
+
state.status = "integrated"
|
|
182
|
+
state.activity = "提交已集成"
|
|
183
|
+
state.commit = event.commit
|
|
184
|
+
state.verification = event.verification
|
|
185
|
+
elif isinstance(event, SubagentConflict):
|
|
186
|
+
state.status = "conflict"
|
|
187
|
+
state.activity = event.message or "集成冲突"
|
|
188
|
+
if event.conflict_files:
|
|
189
|
+
state.verification = ("冲突文件: " + ", ".join(event.conflict_files),)
|
|
190
|
+
elif isinstance(event, SubagentCleanup):
|
|
191
|
+
state.retained_path = event.retained_path
|
|
192
|
+
if event.removed:
|
|
193
|
+
state.activity = "Worktree 已清理"
|
|
194
|
+
elif event.reason:
|
|
195
|
+
state.activity = event.reason
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rich.text import Text as RichText
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
|
|
6
|
+
from windcode.domain.events import ToolFinished, ToolProgress, ToolStarted
|
|
7
|
+
|
|
8
|
+
TOOL_LABELS = {
|
|
9
|
+
"read_file": "读取文件",
|
|
10
|
+
"write_file": "写入文件",
|
|
11
|
+
"edit_file": "编辑文件",
|
|
12
|
+
"apply_patch": "应用补丁",
|
|
13
|
+
"glob": "查找文件",
|
|
14
|
+
"grep": "搜索文本",
|
|
15
|
+
"shell": "执行命令",
|
|
16
|
+
"ask_user": "询问用户",
|
|
17
|
+
"memory_search": "检索长期记忆",
|
|
18
|
+
"memory_list": "列出长期记忆",
|
|
19
|
+
"memory_get": "读取长期记忆",
|
|
20
|
+
"memory_write": "写入长期记忆",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_duration(seconds: float) -> str:
|
|
25
|
+
if seconds < 0.01:
|
|
26
|
+
return "<0.01 秒"
|
|
27
|
+
if seconds < 1:
|
|
28
|
+
return f"{seconds:.2f} 秒"
|
|
29
|
+
return f"{seconds:.1f} 秒"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ToolBlock(Static, can_focus=True):
|
|
33
|
+
def __init__(self, event: ToolStarted) -> None:
|
|
34
|
+
self.call_id = event.call_id
|
|
35
|
+
self.tool_name = event.tool_name
|
|
36
|
+
command = event.arguments.get("command")
|
|
37
|
+
self.command = str(command) if command is not None else None
|
|
38
|
+
self.title = TOOL_LABELS.get(event.tool_name, event.tool_name)
|
|
39
|
+
super().__init__(
|
|
40
|
+
self._content(f"● {self.title} ..."),
|
|
41
|
+
classes="tool-block tool-block-loading",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _content(self, status: str) -> RichText:
|
|
45
|
+
content = RichText(f" {status}")
|
|
46
|
+
if self.tool_name == "shell" and self.command:
|
|
47
|
+
content.append("\n ")
|
|
48
|
+
content.append("bash:", style="bold cyan")
|
|
49
|
+
content.append(f" {self.command}")
|
|
50
|
+
return content
|
|
51
|
+
|
|
52
|
+
def progress(self, event: ToolProgress) -> None:
|
|
53
|
+
self.update(self._content(f"● {self.title} ... {event.message}"))
|
|
54
|
+
|
|
55
|
+
def finish(self, event: ToolFinished) -> None:
|
|
56
|
+
self.remove_class("tool-block-loading")
|
|
57
|
+
marker = "✗" if event.result.is_error else "✓"
|
|
58
|
+
if event.result.is_error:
|
|
59
|
+
self.add_class("tool-block-error")
|
|
60
|
+
exit_code = event.result.data.get("exit_code")
|
|
61
|
+
detail = f" · 退出码 {exit_code}" if exit_code is not None else ""
|
|
62
|
+
self.title = (
|
|
63
|
+
f"{TOOL_LABELS.get(self.tool_name, self.tool_name)}{detail}"
|
|
64
|
+
f" ({format_duration(event.result.elapsed_seconds)})"
|
|
65
|
+
)
|
|
66
|
+
self.update(self._content(f"{marker} {self.title}"))
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rich.text import Text as RichText
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Vertical
|
|
8
|
+
from textual.events import Resize
|
|
9
|
+
from textual.timer import Timer
|
|
10
|
+
from textual.widgets import Static
|
|
11
|
+
|
|
12
|
+
from windcode.tui.permission_display import permission_label, permission_style
|
|
13
|
+
|
|
14
|
+
WIDE_LOGO = r"""
|
|
15
|
+
_ _
|
|
16
|
+
__ _____(_)_ __ __| | ___ ___ ___ ___
|
|
17
|
+
\ \ /\ / / _ \ | '_ \ / _` |/ __/ _ \ / _ \/ _ \
|
|
18
|
+
\ V V / __/ | | | | (_| | (_| (_) | __/ __/
|
|
19
|
+
\_/\_/ \___|_|_| |_|\__,_|\___\___/ \___|\___|
|
|
20
|
+
""".strip("\n")
|
|
21
|
+
|
|
22
|
+
COMPACT_LOGO = "[ windcode ]"
|
|
23
|
+
MCP_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
|
|
24
|
+
LOGO_PALETTE = ("#59c7d6", "#5fa8e8", "#8b8fe8", "#d9a557", "#63c28d")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WelcomeView(Vertical):
|
|
28
|
+
"""Brand-focused empty state for a new Windcode session."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
model: str,
|
|
34
|
+
permission: str,
|
|
35
|
+
sandbox: bool,
|
|
36
|
+
workspace: Path,
|
|
37
|
+
id: str | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(id=id)
|
|
40
|
+
self._model = model
|
|
41
|
+
self._permission = permission
|
|
42
|
+
self._sandbox = sandbox
|
|
43
|
+
self._workspace = workspace
|
|
44
|
+
self._mcp_spinner_timer: Timer | None = None
|
|
45
|
+
self._mcp_spinner_index = 0
|
|
46
|
+
self._logo_timer: Timer | None = None
|
|
47
|
+
self._logo_frame = 0
|
|
48
|
+
|
|
49
|
+
def compose(self) -> ComposeResult:
|
|
50
|
+
yield Static(self._logo(), id="welcome-logo")
|
|
51
|
+
yield Static("本地编码 Agent", id="welcome-subtitle")
|
|
52
|
+
yield Static(self._context_content(), id="welcome-context")
|
|
53
|
+
yield Static("", id="welcome-notice")
|
|
54
|
+
|
|
55
|
+
def on_mount(self) -> None:
|
|
56
|
+
self._logo_timer = self.set_interval(0.12, self._tick_logo)
|
|
57
|
+
|
|
58
|
+
def on_unmount(self) -> None:
|
|
59
|
+
if self._logo_timer is not None:
|
|
60
|
+
self._logo_timer.stop()
|
|
61
|
+
self._logo_timer = None
|
|
62
|
+
self.stop_mcp_loading()
|
|
63
|
+
|
|
64
|
+
def _tick_logo(self) -> None:
|
|
65
|
+
self._logo_frame = (self._logo_frame + 1) % 10_000
|
|
66
|
+
self.query_one("#welcome-logo", Static).update(self._logo())
|
|
67
|
+
|
|
68
|
+
def set_context(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
model: str,
|
|
72
|
+
permission: str,
|
|
73
|
+
sandbox: bool,
|
|
74
|
+
workspace: Path,
|
|
75
|
+
) -> None:
|
|
76
|
+
self._model = model
|
|
77
|
+
self._permission = permission
|
|
78
|
+
self._sandbox = sandbox
|
|
79
|
+
self._workspace = workspace
|
|
80
|
+
if self.is_mounted:
|
|
81
|
+
self.query_one("#welcome-logo", Static).update(self._logo())
|
|
82
|
+
self.query_one("#welcome-context", Static).update(self._context_content())
|
|
83
|
+
|
|
84
|
+
def show_notice(self, text: str, *, error: bool = False) -> None:
|
|
85
|
+
notice = self.query_one("#welcome-notice", Static)
|
|
86
|
+
notice.set_class(error, "welcome-notice-error")
|
|
87
|
+
notice.update(f"{'错误' if error else '状态'} · {text}")
|
|
88
|
+
|
|
89
|
+
def clear_notice(self) -> None:
|
|
90
|
+
if self.is_mounted:
|
|
91
|
+
notice = self.query_one("#welcome-notice", Static)
|
|
92
|
+
notice.remove_class("welcome-notice-error")
|
|
93
|
+
notice.update("")
|
|
94
|
+
|
|
95
|
+
def start_mcp_loading(self) -> None:
|
|
96
|
+
self.stop_mcp_loading()
|
|
97
|
+
self._mcp_spinner_index = 0
|
|
98
|
+
self._render_mcp_loading()
|
|
99
|
+
self._mcp_spinner_timer = self.set_interval(0.08, self._tick_mcp_loading)
|
|
100
|
+
|
|
101
|
+
def stop_mcp_loading(self) -> None:
|
|
102
|
+
if self._mcp_spinner_timer is not None:
|
|
103
|
+
self._mcp_spinner_timer.stop()
|
|
104
|
+
self._mcp_spinner_timer = None
|
|
105
|
+
|
|
106
|
+
def _tick_mcp_loading(self) -> None:
|
|
107
|
+
self._mcp_spinner_index = (self._mcp_spinner_index + 1) % len(MCP_SPINNER_FRAMES)
|
|
108
|
+
self._render_mcp_loading()
|
|
109
|
+
|
|
110
|
+
def _render_mcp_loading(self) -> None:
|
|
111
|
+
if not self.is_mounted:
|
|
112
|
+
return
|
|
113
|
+
frame = MCP_SPINNER_FRAMES[self._mcp_spinner_index]
|
|
114
|
+
notice = self.query_one("#welcome-notice", Static)
|
|
115
|
+
notice.remove_class("welcome-notice-error")
|
|
116
|
+
notice.update(f"{frame} 正在加载 MCP 服务...")
|
|
117
|
+
|
|
118
|
+
def on_resize(self, event: Resize) -> None:
|
|
119
|
+
del event
|
|
120
|
+
self.query_one("#welcome-logo", Static).update(self._logo())
|
|
121
|
+
|
|
122
|
+
def _logo(self) -> RichText:
|
|
123
|
+
logo = COMPACT_LOGO if self.size.width and self.size.width < 64 else WIDE_LOGO
|
|
124
|
+
output = RichText(justify="center")
|
|
125
|
+
lines = logo.splitlines()
|
|
126
|
+
for row, line in enumerate(lines):
|
|
127
|
+
for column, char in enumerate(line):
|
|
128
|
+
if char.isspace():
|
|
129
|
+
output.append(char)
|
|
130
|
+
continue
|
|
131
|
+
wave = (column + row * 2 + self._logo_frame) // 3
|
|
132
|
+
color = LOGO_PALETTE[wave % len(LOGO_PALETTE)]
|
|
133
|
+
highlight = (column + row + self._logo_frame) % 17 in {0, 1}
|
|
134
|
+
style = f"bold {color}" + (" on #26343d" if highlight else "")
|
|
135
|
+
output.append(char, style=style)
|
|
136
|
+
if row < len(lines) - 1:
|
|
137
|
+
output.append("\n")
|
|
138
|
+
return output
|
|
139
|
+
|
|
140
|
+
def _context_content(self) -> RichText:
|
|
141
|
+
context = RichText(justify="center")
|
|
142
|
+
context.append(self._model, style="bold color(252)")
|
|
143
|
+
context.append(" · ", style="color(240)")
|
|
144
|
+
context.append(permission_label(self._permission), style=permission_style(self._permission))
|
|
145
|
+
context.append(" · ", style="color(240)")
|
|
146
|
+
context.append(f"沙箱{'开启' if self._sandbox else '关闭'}", style="color(246)")
|
|
147
|
+
context.append("\n")
|
|
148
|
+
context.append(str(self._workspace), style="color(242)")
|
|
149
|
+
return context
|
windcode/types.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Stable public types for embedding Windcode."""
|
|
2
|
+
|
|
3
|
+
from windcode.domain.events import (
|
|
4
|
+
AgentEventType,
|
|
5
|
+
ApprovalRequested,
|
|
6
|
+
ApprovalResponse,
|
|
7
|
+
RunRequest,
|
|
8
|
+
RunResponse,
|
|
9
|
+
RunResult,
|
|
10
|
+
SubagentBlocked,
|
|
11
|
+
SubagentCancelled,
|
|
12
|
+
SubagentCleanup,
|
|
13
|
+
SubagentCompleted,
|
|
14
|
+
SubagentConflict,
|
|
15
|
+
SubagentFailed,
|
|
16
|
+
SubagentIntegrated,
|
|
17
|
+
SubagentProgress,
|
|
18
|
+
SubagentQueued,
|
|
19
|
+
SubagentStarted,
|
|
20
|
+
UserInputRequested,
|
|
21
|
+
UserResponse,
|
|
22
|
+
)
|
|
23
|
+
from windcode.domain.subagents import (
|
|
24
|
+
SubagentRecord,
|
|
25
|
+
SubagentResult,
|
|
26
|
+
SubagentRole,
|
|
27
|
+
SubagentStatus,
|
|
28
|
+
SubagentTaskKind,
|
|
29
|
+
SubagentTaskSpec,
|
|
30
|
+
VerificationResult,
|
|
31
|
+
)
|
|
32
|
+
from windcode.domain.tools import Tool, ToolContext, ToolEffect, ToolResult
|
|
33
|
+
from windcode.extensions.models import (
|
|
34
|
+
CapabilityRecord,
|
|
35
|
+
Diagnostic,
|
|
36
|
+
ExtensionSnapshot,
|
|
37
|
+
ManagementResult,
|
|
38
|
+
)
|
|
39
|
+
from windcode.extensions.plugins.installer import InstallResult
|
|
40
|
+
from windcode.extensions.state import ManagementAuditRecord
|
|
41
|
+
|
|
42
|
+
AgentEvent = AgentEventType
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"AgentEvent",
|
|
46
|
+
"ApprovalRequested",
|
|
47
|
+
"ApprovalResponse",
|
|
48
|
+
"CapabilityRecord",
|
|
49
|
+
"Diagnostic",
|
|
50
|
+
"ExtensionSnapshot",
|
|
51
|
+
"InstallResult",
|
|
52
|
+
"ManagementAuditRecord",
|
|
53
|
+
"ManagementResult",
|
|
54
|
+
"RunRequest",
|
|
55
|
+
"RunResponse",
|
|
56
|
+
"RunResult",
|
|
57
|
+
"SubagentBlocked",
|
|
58
|
+
"SubagentCancelled",
|
|
59
|
+
"SubagentCleanup",
|
|
60
|
+
"SubagentCompleted",
|
|
61
|
+
"SubagentConflict",
|
|
62
|
+
"SubagentFailed",
|
|
63
|
+
"SubagentIntegrated",
|
|
64
|
+
"SubagentProgress",
|
|
65
|
+
"SubagentQueued",
|
|
66
|
+
"SubagentRecord",
|
|
67
|
+
"SubagentResult",
|
|
68
|
+
"SubagentRole",
|
|
69
|
+
"SubagentStarted",
|
|
70
|
+
"SubagentStatus",
|
|
71
|
+
"SubagentTaskKind",
|
|
72
|
+
"SubagentTaskSpec",
|
|
73
|
+
"Tool",
|
|
74
|
+
"ToolContext",
|
|
75
|
+
"ToolEffect",
|
|
76
|
+
"ToolResult",
|
|
77
|
+
"UserInputRequested",
|
|
78
|
+
"UserResponse",
|
|
79
|
+
"VerificationResult",
|
|
80
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from windcode.worktrees.git import GitCommandResult, GitRunner
|
|
2
|
+
from windcode.worktrees.manager import WorktreeManager
|
|
3
|
+
from windcode.worktrees.models import (
|
|
4
|
+
CleanupResult,
|
|
5
|
+
GitBaseline,
|
|
6
|
+
GitErrorCategory,
|
|
7
|
+
IntegrationResult,
|
|
8
|
+
WorktreeError,
|
|
9
|
+
WorktreeLease,
|
|
10
|
+
WorktreeResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CleanupResult",
|
|
15
|
+
"GitBaseline",
|
|
16
|
+
"GitCommandResult",
|
|
17
|
+
"GitErrorCategory",
|
|
18
|
+
"GitRunner",
|
|
19
|
+
"IntegrationResult",
|
|
20
|
+
"WorktreeError",
|
|
21
|
+
"WorktreeLease",
|
|
22
|
+
"WorktreeManager",
|
|
23
|
+
"WorktreeResult",
|
|
24
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from windcode.worktrees.models import GitErrorCategory, WorktreeError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _resolve(path: Path) -> Path:
|
|
13
|
+
return path.expanduser().resolve()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class GitCommandResult:
|
|
18
|
+
arguments: tuple[str, ...]
|
|
19
|
+
cwd: Path
|
|
20
|
+
returncode: int
|
|
21
|
+
stdout: str
|
|
22
|
+
stderr: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GitRunner:
|
|
26
|
+
def __init__(self, *, timeout_seconds: float = 30.0) -> None:
|
|
27
|
+
self.timeout_seconds = timeout_seconds
|
|
28
|
+
|
|
29
|
+
async def run(
|
|
30
|
+
self,
|
|
31
|
+
arguments: Sequence[str],
|
|
32
|
+
*,
|
|
33
|
+
cwd: Path,
|
|
34
|
+
check: bool = True,
|
|
35
|
+
timeout_seconds: float | None = None,
|
|
36
|
+
cancelled: Callable[[], bool] | None = None,
|
|
37
|
+
) -> GitCommandResult:
|
|
38
|
+
if cancelled is not None and cancelled():
|
|
39
|
+
raise WorktreeError(GitErrorCategory.CANCELLED, "Git operation was cancelled")
|
|
40
|
+
resolved_cwd = _resolve(cwd)
|
|
41
|
+
environment = os.environ.copy()
|
|
42
|
+
environment.update(
|
|
43
|
+
{
|
|
44
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
45
|
+
"GCM_INTERACTIVE": "never",
|
|
46
|
+
"GIT_ASKPASS": "/bin/false",
|
|
47
|
+
"SSH_ASKPASS": "/bin/false",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
process = await asyncio.create_subprocess_exec(
|
|
51
|
+
"git",
|
|
52
|
+
*arguments,
|
|
53
|
+
cwd=resolved_cwd,
|
|
54
|
+
env=environment,
|
|
55
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
56
|
+
stdout=asyncio.subprocess.PIPE,
|
|
57
|
+
stderr=asyncio.subprocess.PIPE,
|
|
58
|
+
start_new_session=True,
|
|
59
|
+
)
|
|
60
|
+
timeout = self.timeout_seconds if timeout_seconds is None else timeout_seconds
|
|
61
|
+
try:
|
|
62
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(process.communicate(), timeout)
|
|
63
|
+
except TimeoutError as exc:
|
|
64
|
+
process.kill()
|
|
65
|
+
await process.wait()
|
|
66
|
+
raise WorktreeError(
|
|
67
|
+
GitErrorCategory.TIMEOUT,
|
|
68
|
+
f"Git command timed out after {timeout:g} seconds",
|
|
69
|
+
) from exc
|
|
70
|
+
except asyncio.CancelledError:
|
|
71
|
+
process.kill()
|
|
72
|
+
await process.wait()
|
|
73
|
+
raise
|
|
74
|
+
result = GitCommandResult(
|
|
75
|
+
arguments=tuple(arguments),
|
|
76
|
+
cwd=resolved_cwd,
|
|
77
|
+
returncode=process.returncode or 0,
|
|
78
|
+
stdout=stdout_bytes.decode("utf-8", errors="replace"),
|
|
79
|
+
stderr=stderr_bytes.decode("utf-8", errors="replace"),
|
|
80
|
+
)
|
|
81
|
+
if check and result.returncode != 0:
|
|
82
|
+
message = result.stderr.strip() or result.stdout.strip() or "Git command failed"
|
|
83
|
+
lowered = message.lower()
|
|
84
|
+
category = (
|
|
85
|
+
GitErrorCategory.NOT_REPOSITORY
|
|
86
|
+
if "not a git repository" in lowered
|
|
87
|
+
else GitErrorCategory.COMMAND_FAILED
|
|
88
|
+
)
|
|
89
|
+
raise WorktreeError(category, message)
|
|
90
|
+
if cancelled is not None and cancelled():
|
|
91
|
+
raise WorktreeError(GitErrorCategory.CANCELLED, "Git operation was cancelled")
|
|
92
|
+
return result
|