chcode 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.
- chcode/__init__.py +0 -0
- chcode/__main__.py +5 -0
- chcode/agent_setup.py +395 -0
- chcode/agents/__init__.py +0 -0
- chcode/agents/definitions.py +158 -0
- chcode/agents/loader.py +104 -0
- chcode/agents/runner.py +159 -0
- chcode/chat.py +1630 -0
- chcode/cli.py +142 -0
- chcode/config.py +571 -0
- chcode/display.py +325 -0
- chcode/prompts.py +640 -0
- chcode/session.py +149 -0
- chcode/skill_manager.py +165 -0
- chcode/utils/__init__.py +3 -0
- chcode/utils/enhanced_chat_openai.py +368 -0
- chcode/utils/git_checker.py +38 -0
- chcode/utils/git_manager.py +261 -0
- chcode/utils/modelscope_ratelimit.py +65 -0
- chcode/utils/multimodal.py +268 -0
- chcode/utils/shell/__init__.py +17 -0
- chcode/utils/shell/output.py +63 -0
- chcode/utils/shell/provider.py +128 -0
- chcode/utils/shell/result.py +14 -0
- chcode/utils/shell/semantics.py +55 -0
- chcode/utils/shell/session.py +159 -0
- chcode/utils/skill_loader.py +565 -0
- chcode/utils/text_utils.py +14 -0
- chcode/utils/tool_result_pipeline.py +244 -0
- chcode/utils/tools.py +1724 -0
- chcode/vision_config.py +371 -0
- chcode-0.1.0.dist-info/METADATA +275 -0
- chcode-0.1.0.dist-info/RECORD +36 -0
- chcode-0.1.0.dist-info/WHEEL +4 -0
- chcode-0.1.0.dist-info/entry_points.txt +2 -0
- chcode-0.1.0.dist-info/licenses/LICENSE +21 -0
chcode/display.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich 输出渲染 — Markdown、流式输出、状态栏、消息样式
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.markdown import Markdown
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from rich.rule import Rule
|
|
14
|
+
from rich.live import Live
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import contextvars
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
_subagent_count = 0
|
|
22
|
+
_subagent_count_lock = threading.Lock()
|
|
23
|
+
_subagent_parallel = False
|
|
24
|
+
|
|
25
|
+
_current_agent_tag: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
26
|
+
"_current_agent_tag", default=None
|
|
27
|
+
)
|
|
28
|
+
_agent_progress: dict[str, dict] = {}
|
|
29
|
+
_agent_progress_lock = threading.Lock()
|
|
30
|
+
_progress_live: Live | None = None
|
|
31
|
+
_progress_task: asyncio.Task | None = None
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
38
|
+
# ─── 消息渲染 ──────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render_human(message: str) -> None:
|
|
42
|
+
"""渲染用户消息"""
|
|
43
|
+
console.print(
|
|
44
|
+
Panel(
|
|
45
|
+
Markdown(message),
|
|
46
|
+
border_style="blue",
|
|
47
|
+
title="You",
|
|
48
|
+
title_align="right",
|
|
49
|
+
padding=(0, 1),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_ai_chunk(content: str) -> None:
|
|
55
|
+
"""渲染 AI 回复片段(流式)"""
|
|
56
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
57
|
+
return
|
|
58
|
+
console.print(content, end="", style="white")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def render_ai_start():
|
|
62
|
+
"""AI 回复开始"""
|
|
63
|
+
global _subagent_parallel
|
|
64
|
+
# 先完成并清理之前的进度显示
|
|
65
|
+
_finalize_progress()
|
|
66
|
+
_subagent_parallel = False
|
|
67
|
+
with _agent_progress_lock:
|
|
68
|
+
_agent_progress.clear()
|
|
69
|
+
if _subagent_count > 0:
|
|
70
|
+
return
|
|
71
|
+
console.print()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def render_ai_end() -> None:
|
|
75
|
+
"""AI 回复结束"""
|
|
76
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
77
|
+
return
|
|
78
|
+
console.print()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render_reasoning(reasoning: str) -> None:
|
|
82
|
+
"""渲染推理/思考内容(灰色斜体,折叠)"""
|
|
83
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
84
|
+
return
|
|
85
|
+
console.print(
|
|
86
|
+
Panel(
|
|
87
|
+
Text(reasoning, style="dim italic"),
|
|
88
|
+
border_style="dim",
|
|
89
|
+
title="Thinking",
|
|
90
|
+
title_align="left",
|
|
91
|
+
padding=(0, 1),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _start_progress():
|
|
97
|
+
global _progress_live
|
|
98
|
+
if _progress_live is None:
|
|
99
|
+
_progress_live = Live("", transient=True, console=console, refresh_per_second=1)
|
|
100
|
+
_progress_live.start()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _update_progress():
|
|
104
|
+
if not _progress_live:
|
|
105
|
+
return
|
|
106
|
+
with _agent_progress_lock:
|
|
107
|
+
if not _agent_progress:
|
|
108
|
+
_progress_live.update("")
|
|
109
|
+
return
|
|
110
|
+
now = time.time()
|
|
111
|
+
lines = []
|
|
112
|
+
for tag, info in _agent_progress.items():
|
|
113
|
+
elapsed = int(now - info["start"])
|
|
114
|
+
timeout = info.get("timeout", 300)
|
|
115
|
+
|
|
116
|
+
if info.get("failed"):
|
|
117
|
+
lines.append(f" [red]\u274c {tag}: \u8d85\u65f6 ({timeout}s)[/red]")
|
|
118
|
+
elif info.get("done"):
|
|
119
|
+
lines.append(f" [green]\u2705 {tag}: done ({elapsed}s)[/green]")
|
|
120
|
+
else:
|
|
121
|
+
remaining = max(0, timeout - elapsed)
|
|
122
|
+
pct = min(elapsed / timeout, 1.0) if timeout > 0 else 1.0
|
|
123
|
+
bar_len = int(pct * 16)
|
|
124
|
+
bar = "\u2588" * bar_len + "\u2591" * (16 - bar_len)
|
|
125
|
+
lines.append(f" {tag}: {remaining}s \u5269\u4f59 [{bar}]")
|
|
126
|
+
_progress_live.update("\n".join(lines))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def _progress_updater():
|
|
130
|
+
"""定期更新进度显示的后台任务"""
|
|
131
|
+
try:
|
|
132
|
+
while True:
|
|
133
|
+
await asyncio.sleep(1)
|
|
134
|
+
if _progress_live is None:
|
|
135
|
+
break
|
|
136
|
+
_update_progress()
|
|
137
|
+
except asyncio.CancelledError:
|
|
138
|
+
pass # 优雅处理取消
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _finalize_progress():
|
|
142
|
+
"""停止进度显示,打印最终状态并清理资源"""
|
|
143
|
+
global _progress_live, _progress_task
|
|
144
|
+
|
|
145
|
+
# 先取消更新任务
|
|
146
|
+
if _progress_task is not None and not _progress_task.done():
|
|
147
|
+
_progress_task.cancel()
|
|
148
|
+
_progress_task = None
|
|
149
|
+
|
|
150
|
+
# 停止 Live(transient=True 会自动清除显示内容)
|
|
151
|
+
if _progress_live is not None:
|
|
152
|
+
_progress_live.stop()
|
|
153
|
+
_progress_live = None
|
|
154
|
+
|
|
155
|
+
# 清空进度数据
|
|
156
|
+
with _agent_progress_lock:
|
|
157
|
+
_agent_progress.clear()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def render_tool_call(name: str, summary: str) -> None:
|
|
161
|
+
tag = _current_agent_tag.get()
|
|
162
|
+
if tag:
|
|
163
|
+
with _agent_progress_lock:
|
|
164
|
+
if tag in _agent_progress:
|
|
165
|
+
_agent_progress[tag]["calls"] += 1
|
|
166
|
+
if len(summary) > 120:
|
|
167
|
+
summary = summary[:117] + "..."
|
|
168
|
+
if name == "agent":
|
|
169
|
+
console.print(Text(f"\n[{name}] {summary}", style="bold cyan"))
|
|
170
|
+
return
|
|
171
|
+
if _subagent_parallel or _subagent_count >= 2:
|
|
172
|
+
return
|
|
173
|
+
if _subagent_count == 1:
|
|
174
|
+
console.print(Text(f" [{name}] {summary}", style="dim cyan"))
|
|
175
|
+
return
|
|
176
|
+
console.print(Text(f"\n[{name}] {summary}", style="bold cyan"))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def render_tool(name: str, content: str) -> None:
|
|
180
|
+
"""渲染工具调用结果"""
|
|
181
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
182
|
+
return
|
|
183
|
+
# 截断过长内容
|
|
184
|
+
lines = content.split("\n")
|
|
185
|
+
if len(lines) > 50:
|
|
186
|
+
content = "\n".join(lines[:50]) + f"\n... ({len(lines) - 50} more lines)"
|
|
187
|
+
console.print(
|
|
188
|
+
Panel(
|
|
189
|
+
Text(content, style="yellow"),
|
|
190
|
+
border_style="yellow",
|
|
191
|
+
title=f"Tool: {name}",
|
|
192
|
+
title_align="left",
|
|
193
|
+
padding=(0, 1),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def render_error(message: str) -> None:
|
|
199
|
+
"""渲染错误信息"""
|
|
200
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
201
|
+
return
|
|
202
|
+
console.print(Text("Error: ", style="red bold"), Text(message, style="red bold"))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def render_info(message: str) -> None:
|
|
206
|
+
"""渲染信息"""
|
|
207
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
208
|
+
return
|
|
209
|
+
console.print(f"[cyan]{message}[/cyan]")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def render_success(message: str) -> None:
|
|
213
|
+
"""渲染成功信息"""
|
|
214
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
215
|
+
return
|
|
216
|
+
console.print(f"[green]{message}[/green]")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def render_warning(message: str) -> None:
|
|
220
|
+
"""渲染警告信息"""
|
|
221
|
+
if _subagent_parallel or _subagent_count > 0:
|
|
222
|
+
return
|
|
223
|
+
console.print(f"[yellow]{message}[/yellow]")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def render_separator() -> None:
|
|
227
|
+
"""渲染分隔线"""
|
|
228
|
+
console.print(Rule(style="dim"))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def render_welcome() -> None:
|
|
232
|
+
"""渲染欢迎信息"""
|
|
233
|
+
console.print()
|
|
234
|
+
console.print(
|
|
235
|
+
Panel(
|
|
236
|
+
"[bold]ChCode[/bold] — Terminal-based AI Coding Agent\n"
|
|
237
|
+
"Enter 发送 | Ctrl+Enter 换行 | /help 查看命令\n"
|
|
238
|
+
"Ctrl+C 中断生成 | Tab 切换模式 | /quit 退出",
|
|
239
|
+
border_style="cyan",
|
|
240
|
+
padding=(1, 2),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
console.print()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ─── 消息列表渲染(加载历史) ─────────────────────────────
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def render_conversation(messages: list) -> None:
|
|
250
|
+
"""渲染完整对话历史"""
|
|
251
|
+
top_flag = True
|
|
252
|
+
for i, message in enumerate(messages):
|
|
253
|
+
if message.additional_kwargs.get("hide", ""):
|
|
254
|
+
continue
|
|
255
|
+
msg_type = message.type
|
|
256
|
+
content = message.content
|
|
257
|
+
from chcode.utils import get_text_content
|
|
258
|
+
content = get_text_content(content)
|
|
259
|
+
|
|
260
|
+
if msg_type == "human":
|
|
261
|
+
if top_flag:
|
|
262
|
+
top_flag = False
|
|
263
|
+
else:
|
|
264
|
+
render_separator()
|
|
265
|
+
render_human(content or "")
|
|
266
|
+
|
|
267
|
+
elif msg_type == "ai":
|
|
268
|
+
reasoning = message.additional_kwargs.get("reasoning")
|
|
269
|
+
if reasoning:
|
|
270
|
+
render_reasoning(reasoning)
|
|
271
|
+
if content:
|
|
272
|
+
render_ai_start()
|
|
273
|
+
console.print(Markdown(content))
|
|
274
|
+
render_ai_end()
|
|
275
|
+
|
|
276
|
+
elif msg_type == "tool":
|
|
277
|
+
if content:
|
|
278
|
+
render_tool(message.name or "tool", content)
|
|
279
|
+
|
|
280
|
+
console.print()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ─── 上下文用量 ──────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _format_tokens(n: int) -> str:
|
|
287
|
+
"""格式化 token 数:123456 → 123.5K"""
|
|
288
|
+
if n >= 1000:
|
|
289
|
+
return f"{n / 1000:.1f}K"
|
|
290
|
+
return str(n)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_context_usage_text(messages: list, max_context: int) -> str:
|
|
294
|
+
"""
|
|
295
|
+
从消息列表计算上下文占用,返回带样式的文本。
|
|
296
|
+
|
|
297
|
+
取最后一次 AIMessage 的 input_tokens 作为上下文快照
|
|
298
|
+
(因为每次请求的 input_tokens 包含了完整上下文)。
|
|
299
|
+
"""
|
|
300
|
+
input_tokens = 0
|
|
301
|
+
for message in reversed(messages):
|
|
302
|
+
from langchain_core.messages import AIMessage
|
|
303
|
+
|
|
304
|
+
if isinstance(message, AIMessage):
|
|
305
|
+
usage = message.usage_metadata
|
|
306
|
+
if usage and usage.get("input_tokens"):
|
|
307
|
+
input_tokens = usage["input_tokens"]
|
|
308
|
+
break
|
|
309
|
+
|
|
310
|
+
if input_tokens == 0:
|
|
311
|
+
return ""
|
|
312
|
+
|
|
313
|
+
pct = input_tokens / max_context
|
|
314
|
+
used_str = _format_tokens(input_tokens)
|
|
315
|
+
max_str = _format_tokens(max_context)
|
|
316
|
+
pct_str = f"{pct * 100:.0f}%"
|
|
317
|
+
|
|
318
|
+
if pct < 0.7:
|
|
319
|
+
style = "yellow"
|
|
320
|
+
elif pct < 0.9:
|
|
321
|
+
style = "bold yellow"
|
|
322
|
+
else:
|
|
323
|
+
style = "bold red"
|
|
324
|
+
|
|
325
|
+
return f"[{style}]{used_str}/{max_str} {pct_str}[/{style}]"
|