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,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