super-code-assistant 3.3.6__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.
- commands/__init__.py +859 -0
- core/__init__.py +0 -0
- core/config.py +263 -0
- core/config_template.json +7 -0
- core/context.py +271 -0
- core/engine.py +635 -0
- core/file_state.py +279 -0
- core/llm.py +309 -0
- core/model_capabilities.py +45 -0
- core/permissions.py +204 -0
- core/sandbox/__init__.py +15 -0
- core/sandbox/blacklist.py +176 -0
- core/sandbox/config.py +38 -0
- core/sandbox/network.py +136 -0
- core/sandbox/path_protection.py +126 -0
- core/session.py +295 -0
- core/tool.py +45 -0
- features/__init__.py +0 -0
- features/compact.py +945 -0
- features/coordinator.py +105 -0
- features/cost_tracker.py +184 -0
- features/extract_memories.py +326 -0
- features/find_relevant_memories.py +376 -0
- features/git_ai.py +256 -0
- features/memory.py +531 -0
- features/memory_age.py +66 -0
- features/memory_scan.py +153 -0
- features/memory_types.py +34 -0
- features/plan.py +327 -0
- features/skills.py +300 -0
- features/worker_manager.py +232 -0
- mcp/__init__.py +0 -0
- mcp/client.py +112 -0
- mcp/loader.py +80 -0
- mcp/tool_proxy.py +59 -0
- super_code_assistant-3.3.6.dist-info/METADATA +45 -0
- super_code_assistant-3.3.6.dist-info/RECORD +61 -0
- super_code_assistant-3.3.6.dist-info/WHEEL +5 -0
- super_code_assistant-3.3.6.dist-info/entry_points.txt +2 -0
- super_code_assistant-3.3.6.dist-info/top_level.txt +7 -0
- tools/__init__.py +21 -0
- tools/agent.py +132 -0
- tools/ask_user.py +111 -0
- tools/bash.py +77 -0
- tools/file_edit.py +269 -0
- tools/file_read.py +206 -0
- tools/file_write.py +78 -0
- tools/glob_tool.py +81 -0
- tools/grep_tool.py +134 -0
- tools/plan_tools.py +75 -0
- tools/skill.py +108 -0
- tools/tool.py +44 -0
- tools/web_fetch.py +129 -0
- tools/web_search.py +220 -0
- tui/__init__.py +0 -0
- tui/app.py +726 -0
- tui/clipboard_image.py +42 -0
- tui/keylistener.py +140 -0
- tui/prompt.py +752 -0
- tui/query.py +200 -0
- tui/rendering.py +135 -0
tui/query.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Run a single query turn with TUI feedback (spinner, markdown streaming)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.markup import escape as _escape
|
|
8
|
+
|
|
9
|
+
from core.engine import AbortedError, Engine, _REJECT_MESSAGE, _SIBLING_REJECT_MESSAGE
|
|
10
|
+
from tui.keylistener import EscListener
|
|
11
|
+
from tui.rendering import (StreamingMarkdown, SpinnerManager, tool_preview, collapsed_tool_summary,
|
|
12
|
+
SPINNER_THINKING, SPINNER_COMPACT, SPINNER_PREPARING, SPINNER_WORKING)
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_query(engine: Engine, user_input: str, print_mode: bool,
|
|
18
|
+
permissions=None, quiet: bool = False) -> None:
|
|
19
|
+
|
|
20
|
+
listener = EscListener(on_cancel=engine.abort) # 创建一个 Esc 键监听器,当用户按下 Esc 键时自动调用 engine.abort() 方法来中断引擎正在执行的任务
|
|
21
|
+
spinner = SpinnerManager(console)
|
|
22
|
+
md_stream = StreamingMarkdown(console)
|
|
23
|
+
first_text = True # 标记是否是第一次接收到文本内容,以便在首次收到文本时停止加载动画并切换到流式渲染模式
|
|
24
|
+
streaming = False # 追踪当前是否处于流式文本输出状态,配合 Esc 键监听实现中途取消功能。
|
|
25
|
+
# 键(key)是 tool_use_id(API 分配的唯一 id),值(value)是 (工具名, 用于终端显示的带箭头格式化字符串)。
|
|
26
|
+
# 使用 id 而非 "tool_name(preview)" 字符串作 key,避免同名工具(如两个 Grep 用相同 pattern)
|
|
27
|
+
# 因 key 碰撞导致第二条记录覆盖第一条,进而使 pending_tools 永远无法清空,spinner 卡死。
|
|
28
|
+
pending_tools: dict[str, tuple[str, str]] = {}
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
with listener:
|
|
32
|
+
if not quiet:
|
|
33
|
+
spinner.start("Thinking…", SPINNER_THINKING)
|
|
34
|
+
|
|
35
|
+
for event in engine.submit(user_input):
|
|
36
|
+
if not quiet and streaming and listener.pressed:
|
|
37
|
+
md_stream.flush()
|
|
38
|
+
spinner.stop()
|
|
39
|
+
engine.cancel_turn()
|
|
40
|
+
console.print("\n[dim yellow]⏹ Turn cancelled (Esc)[/dim yellow]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if event[0] == "thinking":
|
|
44
|
+
if not quiet and first_text:
|
|
45
|
+
spinner.start("Thinking…", SPINNER_THINKING)
|
|
46
|
+
|
|
47
|
+
elif event[0] == "compact":
|
|
48
|
+
# 轮内紧急压缩:在工具链中途触发,显示专用提示
|
|
49
|
+
if not quiet:
|
|
50
|
+
md_stream.flush()
|
|
51
|
+
spinner.start("Compacting context…", SPINNER_COMPACT)
|
|
52
|
+
|
|
53
|
+
elif event[0] == "stale_reclaim":
|
|
54
|
+
# 过期 Read 结果回收:发送前替换掉已被后续编辑的旧版本内容
|
|
55
|
+
if not quiet:
|
|
56
|
+
md_stream.flush()
|
|
57
|
+
stats = event[1]
|
|
58
|
+
n = stats.get("reclaimed", 0)
|
|
59
|
+
chars = stats.get("chars_removed", 0)
|
|
60
|
+
console.print(
|
|
61
|
+
f"[dim]Reclaimed {n} stale read result(s) "
|
|
62
|
+
f"(−{chars:,} chars)[/dim]"
|
|
63
|
+
)
|
|
64
|
+
spinner.start("Thinking…", SPINNER_THINKING)
|
|
65
|
+
|
|
66
|
+
elif event[0] == "notification":
|
|
67
|
+
# worker 完成通知:engine 在 mid-turn 注入了通知到对话中
|
|
68
|
+
if not quiet:
|
|
69
|
+
md_stream.flush()
|
|
70
|
+
count = event[1].count("<task-notification>")
|
|
71
|
+
console.print(
|
|
72
|
+
f"[dim]Worker completed ({count}).[/dim]" if count > 1
|
|
73
|
+
else "[dim]Worker completed.[/dim]"
|
|
74
|
+
)
|
|
75
|
+
spinner.start("Thinking…", SPINNER_THINKING)
|
|
76
|
+
|
|
77
|
+
elif event[0] == "text":
|
|
78
|
+
if quiet:
|
|
79
|
+
continue
|
|
80
|
+
if first_text:
|
|
81
|
+
spinner.stop()
|
|
82
|
+
streaming = True
|
|
83
|
+
first_text = False
|
|
84
|
+
if print_mode:
|
|
85
|
+
print(event[1], end="", flush=True)
|
|
86
|
+
else:
|
|
87
|
+
md_stream.feed(event[1])
|
|
88
|
+
|
|
89
|
+
elif event[0] == "waiting":
|
|
90
|
+
if not quiet:
|
|
91
|
+
md_stream.flush()
|
|
92
|
+
streaming = False
|
|
93
|
+
if not quiet:
|
|
94
|
+
spinner.start("Preparing tool call…", SPINNER_PREPARING)
|
|
95
|
+
|
|
96
|
+
elif event[0] == "tool_call":
|
|
97
|
+
if not quiet:
|
|
98
|
+
spinner.stop()
|
|
99
|
+
streaming = False
|
|
100
|
+
# event 格式: ("tool_call", tool_name, tool_input, activity, tool_use_id)
|
|
101
|
+
# 用 tool_use_id 作 key,保证同名工具(如两个 Grep)各自独立追踪,不会 key 碰撞
|
|
102
|
+
_, tool_name, tool_input, activity, tool_id = event
|
|
103
|
+
preview = tool_preview(tool_name, tool_input)
|
|
104
|
+
line = f"↳ {tool_name}({preview})"
|
|
105
|
+
pending_tools[tool_id] = (tool_name, line)
|
|
106
|
+
|
|
107
|
+
elif event[0] == "tool_executing":
|
|
108
|
+
if not quiet:
|
|
109
|
+
# event 格式: ("tool_executing", tool_name, tool_input, activity, tool_use_id)
|
|
110
|
+
_, tool_name, tool_input, activity, tool_id = event
|
|
111
|
+
# 交互式工具(AskUserQuestion)执行期间需要独占 terminal 等用户输入,
|
|
112
|
+
# Rich Live spinner 的后台重绘会与 input() 的行编辑抢光标控制,导致
|
|
113
|
+
# 用户按键被吞、卡住直到按 Enter 才返回。这里显式不启 spinner,让
|
|
114
|
+
# 工具自己拥有 terminal;工具返回后 tool_result 路径正常 stop()。
|
|
115
|
+
if tool_name == "AskUserQuestion":
|
|
116
|
+
spinner.stop()
|
|
117
|
+
else:
|
|
118
|
+
n = len(pending_tools)
|
|
119
|
+
if n > 1:
|
|
120
|
+
names = [tn for tn, _ in pending_tools.values()]
|
|
121
|
+
spinner.start(collapsed_tool_summary(names), SPINNER_WORKING)
|
|
122
|
+
else:
|
|
123
|
+
_, line = pending_tools.get(tool_id, ("", f"↳ {tool_name}"))
|
|
124
|
+
activity_text = activity or f"Running {tool_name}…"
|
|
125
|
+
spinner.start(f"{line} … {activity_text}", SPINNER_WORKING)
|
|
126
|
+
|
|
127
|
+
elif event[0] == "tool_result":
|
|
128
|
+
if not quiet:
|
|
129
|
+
spinner.stop()
|
|
130
|
+
# event 格式: ("tool_result", tool_name, tool_input, result, tool_use_id)
|
|
131
|
+
# 用 tool_use_id pop,精确匹配对应的 tool_call,不受工具名或参数重复影响
|
|
132
|
+
_, tool_name, tool_input, result, tool_id = event
|
|
133
|
+
tname, line = pending_tools.pop(tool_id, (tool_name, f"↳ {tool_name}"))
|
|
134
|
+
if result.is_error:
|
|
135
|
+
console.print(f" [dim]{_escape(line)}[/dim] [red]✗[/red]", highlight=False)
|
|
136
|
+
# REJECT_MESSAGE / SIBLING_REJECT_MESSAGE 是给 LLM 看的硬
|
|
137
|
+
# 指令体("STOP what you are doing..."),不是用户视角的错误
|
|
138
|
+
# 信息——用户主动拒的工具再把这串硬指令红字读一遍很多余。
|
|
139
|
+
# 真实工具失败(Bash 报错、Read 文件不存在等)仍然正常显示。
|
|
140
|
+
if result.content not in (_REJECT_MESSAGE, _SIBLING_REJECT_MESSAGE):
|
|
141
|
+
console.print(f" [red]{_escape(result.content[:200])}[/red]")
|
|
142
|
+
else:
|
|
143
|
+
console.print(f" [dim]{_escape(line)}[/dim] [green]✓[/green]", highlight=False)
|
|
144
|
+
if pending_tools:
|
|
145
|
+
names = [tn for tn, _ in pending_tools.values()]
|
|
146
|
+
spinner.start(collapsed_tool_summary(names), SPINNER_WORKING)
|
|
147
|
+
else:
|
|
148
|
+
streaming = False
|
|
149
|
+
spinner.start("Thinking…", SPINNER_THINKING)
|
|
150
|
+
first_text = True
|
|
151
|
+
|
|
152
|
+
elif event[0] == "error":
|
|
153
|
+
if not quiet:
|
|
154
|
+
md_stream.flush()
|
|
155
|
+
spinner.stop()
|
|
156
|
+
console.print(f"\n[bold red]{_escape(event[1])}[/bold red]")
|
|
157
|
+
|
|
158
|
+
elif event[0] == "turn_aborted_by_deny":
|
|
159
|
+
# engine 检测到本轮发生过 deny → 硬结束 turn,给一行收尾提示,
|
|
160
|
+
# 避免用户看到一堆 ✗ 后突然回输入框疑惑。
|
|
161
|
+
if not quiet:
|
|
162
|
+
md_stream.flush()
|
|
163
|
+
spinner.stop()
|
|
164
|
+
console.print("[dim yellow]⏹ Stopped after tool use rejected. Type your next message.[/dim yellow]")
|
|
165
|
+
|
|
166
|
+
md_stream.flush()
|
|
167
|
+
spinner.stop()
|
|
168
|
+
except (AbortedError, KeyboardInterrupt):
|
|
169
|
+
md_stream.flush()
|
|
170
|
+
spinner.stop()
|
|
171
|
+
if not isinstance(sys.exc_info()[1], AbortedError):
|
|
172
|
+
engine.cancel_turn()
|
|
173
|
+
if not quiet:
|
|
174
|
+
console.print("\n[dim yellow]⏹ Turn cancelled[/dim yellow]")
|
|
175
|
+
return
|
|
176
|
+
finally:
|
|
177
|
+
md_stream.flush()
|
|
178
|
+
spinner.stop()
|
|
179
|
+
|
|
180
|
+
if not print_mode:
|
|
181
|
+
console.print()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
tui/rendering.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Streaming markdown renderer and spinner manager for the TUI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
from rich.markdown import Markdown as RichMarkdown
|
|
9
|
+
from rich.spinner import Spinner
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
_BLOCK_BOUNDARY_RE = re.compile(r"\n(?=\n|\#{1,6} |```|---|\* |- |\d+\. )")
|
|
13
|
+
|
|
14
|
+
# 各 UI 场景专属 spinner 风格(Rich 内置)与配色,让不同状态一眼可辨、告别全场景单一灰白 dots。
|
|
15
|
+
# 元组格式: (动画风格名, Rich 颜色名),start() 里解包。
|
|
16
|
+
SPINNER_THINKING = ("dots8", "cyan") # 思考中:点阵旋转(青,冷静)
|
|
17
|
+
SPINNER_COMPACT = ("arc", "yellow") # 上下文压缩:圆弧转动(黄,整理收拢)
|
|
18
|
+
SPINNER_PREPARING = ("line", "bright_blue") # 准备工具调用:短线脉冲(蓝,待命)
|
|
19
|
+
SPINNER_WORKING = ("bouncingBar", "green") # 工具执行中:弹跳条(绿,干活推进)
|
|
20
|
+
SPINNER_MEMORY = ("star", "magenta") # 记忆检索:星光闪烁(品红,联想检索)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StreamingMarkdown:
|
|
24
|
+
def __init__(self, console: Console):
|
|
25
|
+
self._console = console
|
|
26
|
+
self._buf = ""
|
|
27
|
+
self._stable_len = 0
|
|
28
|
+
self._live: Live | None = None
|
|
29
|
+
|
|
30
|
+
def feed(self, chunk: str) -> None:
|
|
31
|
+
self._buf += chunk
|
|
32
|
+
self._render()
|
|
33
|
+
|
|
34
|
+
def _render(self) -> None: # 智能区分并分别渲染稳定文本和不完整文本:将已完成的 Markdown 块直接打印到终端,而将正在接收中的不完整部分用 Live 组件动态刷新以实现打字机效果。
|
|
35
|
+
text = self._buf
|
|
36
|
+
boundary = self._stable_len
|
|
37
|
+
for m in _BLOCK_BOUNDARY_RE.finditer(text, self._stable_len):
|
|
38
|
+
boundary = m.start()
|
|
39
|
+
if boundary > self._stable_len:
|
|
40
|
+
if self._live is not None:
|
|
41
|
+
self._live.stop()
|
|
42
|
+
self._live = None
|
|
43
|
+
stable_text = text[self._stable_len:boundary]
|
|
44
|
+
self._console.print(RichMarkdown(stable_text), end="")
|
|
45
|
+
self._stable_len = boundary
|
|
46
|
+
unstable = text[self._stable_len:]
|
|
47
|
+
if unstable:
|
|
48
|
+
if self._live is None:
|
|
49
|
+
self._live = Live(RichMarkdown(unstable), console=self._console,
|
|
50
|
+
refresh_per_second=8, transient=True)
|
|
51
|
+
self._live.start()
|
|
52
|
+
else:
|
|
53
|
+
self._live.update(RichMarkdown(unstable))
|
|
54
|
+
|
|
55
|
+
def flush(self) -> None: # 强制完成所有剩余文本的渲染并清空缓冲区,确保流式传输结束时没有内容遗漏。
|
|
56
|
+
if self._live is not None:
|
|
57
|
+
self._live.stop()
|
|
58
|
+
self._live = None
|
|
59
|
+
remaining = self._buf[self._stable_len:]
|
|
60
|
+
if remaining:
|
|
61
|
+
self._console.print(RichMarkdown(remaining), end="")
|
|
62
|
+
self._buf = ""
|
|
63
|
+
self._stable_len = 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SpinnerManager:
|
|
67
|
+
def __init__(self, console: Console):
|
|
68
|
+
self._console = console
|
|
69
|
+
self._live: Live | None = None
|
|
70
|
+
self._spinner: Spinner | None = None
|
|
71
|
+
self._spinner_name: str | tuple | None = None # 当前 (风格名, 颜色) 配置(Rich Spinner 不保存 name,需自己记录)
|
|
72
|
+
|
|
73
|
+
def start(self, text: str = "Thinking…", spinner: str | tuple[str, str] = "dots"):
|
|
74
|
+
# 启动带提示文本的加载动画,每秒刷新12次且结束后自动消失。
|
|
75
|
+
# spinner 参数:Rich 动画风格名,或 (风格名, 颜色) 元组(见上方 SPINNER_* 常量);
|
|
76
|
+
# 只传风格名时默认青色。帧与文字同色,去掉 dim 避免灰白。
|
|
77
|
+
name, color = spinner if isinstance(spinner, tuple) else (spinner, "cyan")
|
|
78
|
+
# 幂等:Live 已在运行时只原地换文本,不销毁重建。思考模型按 token 高频
|
|
79
|
+
# 产生 thinking 事件,若每次都 stop+重建(清屏+重启刷新线程),spinner 会
|
|
80
|
+
# 忽隐忽现、出现"没反应"的空窗。复用同一个 Spinner 实例避免动画相位重置。
|
|
81
|
+
# 但请求的风格/颜色与当前不同(场景切换)时必须重建,否则换不了动画。
|
|
82
|
+
if self._live is not None and self._spinner is not None:
|
|
83
|
+
if self._spinner_name != spinner:
|
|
84
|
+
self._live.stop()
|
|
85
|
+
self._live = None
|
|
86
|
+
self._spinner = None
|
|
87
|
+
else:
|
|
88
|
+
self._spinner.text = Text(text, style=color)
|
|
89
|
+
self._live.update(self._spinner)
|
|
90
|
+
return
|
|
91
|
+
self._spinner = Spinner(name, text=Text(text, style=color), style=color)
|
|
92
|
+
self._spinner_name = spinner
|
|
93
|
+
self._live = Live(
|
|
94
|
+
self._spinner,
|
|
95
|
+
console=self._console, refresh_per_second=12, transient=True,
|
|
96
|
+
)
|
|
97
|
+
self._live.start()
|
|
98
|
+
|
|
99
|
+
def stop(self): # 停止并清除当前正在显示的加载动画。
|
|
100
|
+
if self._live is not None:
|
|
101
|
+
self._live.stop()
|
|
102
|
+
self._live = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def tool_preview(tool_name: str, tool_input: dict) -> str: # 根据工具类型生成简洁的工具调用预览文本,用于在界面上显示即将执行的操作摘要。
|
|
106
|
+
if tool_name == "Bash":
|
|
107
|
+
cmd = tool_input.get("command", "")
|
|
108
|
+
return cmd[:80] + ("…" if len(cmd) > 80 else "")
|
|
109
|
+
if tool_name in ("Read", "Edit", "Write"):
|
|
110
|
+
fp = tool_input.get("file_path", "")
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
return Path(fp).name if fp else ""
|
|
113
|
+
if tool_name in ("Glob", "Grep"):
|
|
114
|
+
pat = tool_input.get("pattern", "")
|
|
115
|
+
p = tool_input.get("path", "")
|
|
116
|
+
return f"{pat} in {p}" if p else pat
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def collapsed_tool_summary(tool_names: list[str]) -> str: # 是将多个工具调用按类型统计并生成简洁的汇总描述文本,用" · "连接显示。
|
|
121
|
+
from collections import Counter
|
|
122
|
+
counts = Counter(tool_names)
|
|
123
|
+
_LABELS = {
|
|
124
|
+
"Read": ("Reading {n} files", "Reading file"),
|
|
125
|
+
"Glob": ("Searching {n} patterns", "Searching"),
|
|
126
|
+
"Grep": ("Searching {n} patterns", "Searching"),
|
|
127
|
+
"Bash": ("Running {n} commands", "Running command"),
|
|
128
|
+
"Edit": ("Editing {n} files", "Editing file"),
|
|
129
|
+
"Write": ("Writing {n} files", "Writing file"),
|
|
130
|
+
}
|
|
131
|
+
parts = []
|
|
132
|
+
for name, n in counts.items():
|
|
133
|
+
plural, singular = _LABELS.get(name, (f"{name} ×{{n}}", name))
|
|
134
|
+
parts.append(plural.format(n=n) if n > 1 else singular)
|
|
135
|
+
return " · ".join(parts) + "…"
|