mosscode 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.
- mosscode/__init__.py +10 -0
- mosscode/__main__.py +3 -0
- mosscode/agent.py +486 -0
- mosscode/agents/__init__.py +249 -0
- mosscode/agents/builtin/explore.md +16 -0
- mosscode/agents/builtin/general.md +12 -0
- mosscode/agents/builtin/plan.md +16 -0
- mosscode/agents/runtime.py +685 -0
- mosscode/agents/tasks.py +193 -0
- mosscode/appearance.py +184 -0
- mosscode/builtin_commands.py +1363 -0
- mosscode/cli.py +547 -0
- mosscode/commands.py +254 -0
- mosscode/compaction.py +259 -0
- mosscode/config.py +337 -0
- mosscode/context.py +102 -0
- mosscode/conversation.py +1046 -0
- mosscode/coordinator.py +54 -0
- mosscode/doctor.py +303 -0
- mosscode/hooks/__init__.py +22 -0
- mosscode/hooks/actions.py +151 -0
- mosscode/hooks/conditions.py +170 -0
- mosscode/hooks/config.py +404 -0
- mosscode/hooks/engine.py +401 -0
- mosscode/hooks/events.py +48 -0
- mosscode/launcher.py +177 -0
- mosscode/mcp_client.py +460 -0
- mosscode/mcp_config.py +216 -0
- mosscode/member_ui.py +289 -0
- mosscode/memory.py +291 -0
- mosscode/memory_extract.py +245 -0
- mosscode/messages.py +216 -0
- mosscode/paths.py +31 -0
- mosscode/permissions.py +733 -0
- mosscode/prompts.py +483 -0
- mosscode/providers.py +587 -0
- mosscode/scrollback.py +106 -0
- mosscode/security.py +282 -0
- mosscode/serialization.py +121 -0
- mosscode/session.py +563 -0
- mosscode/setup.py +248 -0
- mosscode/skills/__init__.py +455 -0
- mosscode/skills/builtin/commit/SKILL.md +15 -0
- mosscode/skills/builtin/review/SKILL.md +15 -0
- mosscode/skills/builtin/test/SKILL.md +13 -0
- mosscode/skills/builtin/test/scripts/detect.py +96 -0
- mosscode/skills/builtin/test/tool.json +9 -0
- mosscode/teams/__init__.py +216 -0
- mosscode/teams/backends.py +259 -0
- mosscode/teams/board.py +230 -0
- mosscode/teams/lock.py +173 -0
- mosscode/teams/mailbox.py +172 -0
- mosscode/teams/member.py +427 -0
- mosscode/teams/merge.py +207 -0
- mosscode/teams/pane.py +20 -0
- mosscode/teams/pane_app.py +167 -0
- mosscode/teams/process.py +371 -0
- mosscode/teams/runtime.py +1355 -0
- mosscode/teams/view.py +182 -0
- mosscode/tool_budget.py +86 -0
- mosscode/tools/__init__.py +38 -0
- mosscode/tools/agent.py +127 -0
- mosscode/tools/base.py +384 -0
- mosscode/tools/diffing.py +16 -0
- mosscode/tools/execution.py +99 -0
- mosscode/tools/files.py +287 -0
- mosscode/tools/memory.py +65 -0
- mosscode/tools/plan.py +30 -0
- mosscode/tools/process.py +216 -0
- mosscode/tools/search.py +229 -0
- mosscode/tools/search_worker.py +243 -0
- mosscode/tools/shell.py +121 -0
- mosscode/tools/skill.py +118 -0
- mosscode/tools/team.py +447 -0
- mosscode/tools/windows_job.py +75 -0
- mosscode/tui.py +2325 -0
- mosscode/usage.py +68 -0
- mosscode/windows_inline.py +148 -0
- mosscode/worktree/__init__.py +29 -0
- mosscode/worktree/changes.py +59 -0
- mosscode/worktree/git.py +125 -0
- mosscode/worktree/manager.py +462 -0
- mosscode/worktree/meta.py +82 -0
- mosscode/worktree/names.py +52 -0
- mosscode/worktree/setup.py +119 -0
- mosscode-0.1.0.dist-info/METADATA +92 -0
- mosscode-0.1.0.dist-info/RECORD +91 -0
- mosscode-0.1.0.dist-info/WHEEL +5 -0
- mosscode-0.1.0.dist-info/entry_points.txt +3 -0
- mosscode-0.1.0.dist-info/licenses/LICENSE +21 -0
- mosscode-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
"""The built-in slash commands. Handlers reach the screen only through ctx.ui."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.cells import cell_len
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from .agents import SOURCE_LABELS as AGENT_SOURCES
|
|
13
|
+
from .agents.runtime import GLOBAL_BAN, WORKSPACE_TOOLS, effective_mode, stats
|
|
14
|
+
from .agents.tasks import ORIGIN_LABELS, STATUS_LABELS
|
|
15
|
+
from .appearance import KEY_HELP
|
|
16
|
+
from .commands import Command, CommandContext, CommandRegistry, Parsed, argument_problem
|
|
17
|
+
from .hooks.config import SOURCE_LABELS as HOOK_SOURCES
|
|
18
|
+
from .memory import SCOPE_LABELS
|
|
19
|
+
from .messages import ProviderError
|
|
20
|
+
from .permissions import MODE_BADGES, MODE_LABELS, MODES
|
|
21
|
+
from .skills import SOURCE_LABELS, ForkRequest
|
|
22
|
+
|
|
23
|
+
USAGE_WIDTH = 22
|
|
24
|
+
|
|
25
|
+
PLAN_ENTERED = (
|
|
26
|
+
"└ 已进入计划模式:只读调查和规划(含只读命令),模型提交计划后由你批准执行。"
|
|
27
|
+
"/do、Shift+Tab 或 /permissions 可手动退出。"
|
|
28
|
+
)
|
|
29
|
+
CLEARED = "└ 已开始新会话:屏幕、模型上下文和 Token 统计已清空,权限档位与会话授权保留。"
|
|
30
|
+
PERMISSION_HELP = (
|
|
31
|
+
"/permissions default:读取自动通过,写入和命令默认确认\n"
|
|
32
|
+
"/permissions acceptEdits:文件写入和编辑自动通过,命令默认确认\n"
|
|
33
|
+
"/permissions plan:只读调查,批准计划后执行\n"
|
|
34
|
+
"/permissions bypassPermissions:跳过未匹配规则的确认;硬限制、高风险确认和拒绝仍生效\n"
|
|
35
|
+
"Shift+Tab 依次切换 default → acceptEdits → plan"
|
|
36
|
+
)
|
|
37
|
+
MODE_CHOICES = (
|
|
38
|
+
("default", "默认:读取自动通过,写入和命令确认"),
|
|
39
|
+
("acceptEdits", "接受编辑:文件写入/编辑自动通过,命令确认"),
|
|
40
|
+
("plan", "计划:只读调查,批准计划后执行"),
|
|
41
|
+
("bypassPermissions", "跳过审批:仍遵守硬限制和拒绝规则"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def dispatch(ctx: CommandContext, parsed: Parsed) -> None:
|
|
46
|
+
"""Check the argument, then run the handler or send the expanded template."""
|
|
47
|
+
command, argument = parsed.command, parsed.argument
|
|
48
|
+
problem = argument_problem(command, argument)
|
|
49
|
+
if problem:
|
|
50
|
+
await ctx.ui.note(problem)
|
|
51
|
+
elif command.kind == "prompt":
|
|
52
|
+
await ctx.ui.submit(command.expand(argument), command=typed(command, argument))
|
|
53
|
+
elif command.kind == "skill":
|
|
54
|
+
await run_skill(ctx, command, argument)
|
|
55
|
+
else:
|
|
56
|
+
await command.handler(ctx, argument)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def typed(command: Command, argument: str) -> str:
|
|
60
|
+
"""The canonical form stored as Message.command: "/review 安全"."""
|
|
61
|
+
return f"/{command.name}" + (f" {argument}" if argument else "")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def run_skill(ctx: CommandContext, command: Command, argument: str) -> None:
|
|
65
|
+
"""Read the skill again, check its tools, then send the rendered SOP as the user's message."""
|
|
66
|
+
try:
|
|
67
|
+
skill, prompt, notices = ctx.conversation.prepare_skill(command.skill, argument)
|
|
68
|
+
except ProviderError as exc:
|
|
69
|
+
await ctx.ui.note(str(exc))
|
|
70
|
+
return
|
|
71
|
+
for notice in notices:
|
|
72
|
+
await ctx.ui.note("└ " + notice)
|
|
73
|
+
if skill.mode == "fork":
|
|
74
|
+
await ctx.ui.note(f"└ 在独立上下文中运行 Skill {skill.name}")
|
|
75
|
+
await ctx.ui.submit(prompt, command=typed(command, argument), fork=ForkRequest(skill, prompt))
|
|
76
|
+
return
|
|
77
|
+
await ctx.ui.submit(prompt, command=typed(command, argument))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Help -------------------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def help_text(registry: CommandRegistry, name: str = "", skills=None) -> str:
|
|
84
|
+
if name:
|
|
85
|
+
command = registry.get(name)
|
|
86
|
+
if command is None or command.hidden:
|
|
87
|
+
return f"未知命令 /{name.removeprefix('/')}。输入 /help 查看命令。"
|
|
88
|
+
skill = skills.get(command.skill) if skills is not None and command.kind == "skill" else None
|
|
89
|
+
if skill is not None:
|
|
90
|
+
kind = f"Skill:{skill.mode},来源{SOURCE_LABELS[skill.source]}(/skills {skill.name} 查看详情)"
|
|
91
|
+
elif command.kind == "prompt":
|
|
92
|
+
kind = "提示词命令:把预设的请求发给模型"
|
|
93
|
+
else:
|
|
94
|
+
kind = "本地命令:不经过模型"
|
|
95
|
+
lines = [
|
|
96
|
+
command.usage,
|
|
97
|
+
command.description,
|
|
98
|
+
kind,
|
|
99
|
+
"别名:" + ("、".join(f"/{alias}" for alias in command.aliases) or "无"),
|
|
100
|
+
]
|
|
101
|
+
if command.details:
|
|
102
|
+
lines.append(command.details)
|
|
103
|
+
if command.kind == "prompt":
|
|
104
|
+
lines.append("模板首行:" + command.template.strip().splitlines()[0])
|
|
105
|
+
if skill is not None:
|
|
106
|
+
lines.append("规程首行:" + _first_line(skill.body))
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
commands = registry.visible()
|
|
109
|
+
width = max([USAGE_WIDTH, *(cell_len(command.usage) + 2 for command in commands)])
|
|
110
|
+
rows = [
|
|
111
|
+
(
|
|
112
|
+
command.usage,
|
|
113
|
+
command.description
|
|
114
|
+
+ ("(别名 " + "、".join(f"/{alias}" for alias in command.aliases) + ")" if command.aliases else ""),
|
|
115
|
+
)
|
|
116
|
+
for command in commands
|
|
117
|
+
]
|
|
118
|
+
rows.append(("/help 命令", "查看该命令的用法和别名;// 开头的文字直接发给模型"))
|
|
119
|
+
lines = ["命令", *(usage + " " * (width - cell_len(usage)) + description for usage, description in rows)]
|
|
120
|
+
return "\n".join(lines) + "\n\n" + KEY_HELP
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def _help(ctx: CommandContext, argument: str) -> None:
|
|
124
|
+
await ctx.ui.note(help_text(ctx.registry, argument, ctx.conversation.skills))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Session ----------------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
TASK_OPTIONS = ("取消这些任务并继续", "返回")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def confirm_tasks(ui, conversation, proceed) -> None:
|
|
133
|
+
"""Running background tasks are cancelled only once the user says so; Esc means 返回."""
|
|
134
|
+
tasks = getattr(conversation, "tasks", None)
|
|
135
|
+
running = tasks.background() if tasks is not None else []
|
|
136
|
+
if not running:
|
|
137
|
+
await proceed()
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
async def pick(index: int) -> None:
|
|
141
|
+
if index == 0:
|
|
142
|
+
count = await conversation.cancel_tasks()
|
|
143
|
+
await ui.note(f"└ 已取消 {count} 个后台任务。")
|
|
144
|
+
await proceed()
|
|
145
|
+
|
|
146
|
+
labels = "、".join(f"{task.id}({task.label})" for task in running[:3]) + ("等" if len(running) > 3 else "")
|
|
147
|
+
ui.choose(f"有 {len(running)} 个后台任务在运行:{labels}", TASK_OPTIONS, 1, pick)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def _clear(ctx: CommandContext, argument: str) -> None:
|
|
151
|
+
await confirm_tasks(ctx.ui, ctx.conversation, lambda: _do_clear(ctx))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _do_clear(ctx: CommandContext) -> None:
|
|
155
|
+
notices = await ctx.conversation.clear()
|
|
156
|
+
ctx.ui.refresh_status()
|
|
157
|
+
await ctx.ui.clear_screen()
|
|
158
|
+
await ctx.ui.note(CLEARED)
|
|
159
|
+
for notice in notices:
|
|
160
|
+
await ctx.ui.note("└ " + notice)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def _compact(ctx: CommandContext, argument: str) -> None:
|
|
164
|
+
await ctx.ui.compact(argument)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _resume(ctx: CommandContext, argument: str) -> None:
|
|
168
|
+
if argument:
|
|
169
|
+
try:
|
|
170
|
+
ctx.conversation.resolve_session(argument)
|
|
171
|
+
except ProviderError as exc:
|
|
172
|
+
await ctx.ui.note(str(exc))
|
|
173
|
+
else:
|
|
174
|
+
await confirm_tasks(ctx.ui, ctx.conversation, lambda: _resume_now(ctx, argument))
|
|
175
|
+
return
|
|
176
|
+
await open_sessions(ctx)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def _resume_now(ctx: CommandContext, session_id: str) -> None:
|
|
180
|
+
ctx.ui.resume(session_id)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def open_sessions(ctx: CommandContext) -> None:
|
|
184
|
+
conversation = ctx.conversation
|
|
185
|
+
reason = conversation.unavailable()
|
|
186
|
+
if reason:
|
|
187
|
+
await ctx.ui.note(reason)
|
|
188
|
+
return
|
|
189
|
+
current = conversation.session_id
|
|
190
|
+
sessions = await asyncio.to_thread(conversation.sessions)
|
|
191
|
+
if not any(item.id != current for item in sessions):
|
|
192
|
+
await ctx.ui.note("本项目没有可恢复的会话。")
|
|
193
|
+
return
|
|
194
|
+
rows = []
|
|
195
|
+
for item in sessions:
|
|
196
|
+
mark = " (当前)" if item.id == current else " (使用中)" if item.locked else ""
|
|
197
|
+
rows.append(
|
|
198
|
+
Text(
|
|
199
|
+
f"{item.modified:%m-%d %H:%M} {item.title} · {item.messages} 条 · {item.id[-4:]}{mark}",
|
|
200
|
+
no_wrap=True,
|
|
201
|
+
overflow="ellipsis",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
async def pick(index: int) -> None:
|
|
206
|
+
await confirm_tasks(ctx.ui, conversation, lambda: _resume_now(ctx, sessions[index].id))
|
|
207
|
+
|
|
208
|
+
ctx.ui.choose(
|
|
209
|
+
"恢复会话 · 本项目最近的会话", rows, next((i for i, item in enumerate(sessions) if item.id != current), 0), pick
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def session_report(conversation, sessions=()) -> str:
|
|
214
|
+
session = conversation.session
|
|
215
|
+
if session is None:
|
|
216
|
+
return "本次运行没有会话。"
|
|
217
|
+
current = next((item for item in sessions if item.id == session.id), None)
|
|
218
|
+
try:
|
|
219
|
+
started = datetime.strptime(session.id[:15], "%Y%m%d-%H%M%S").strftime("%Y-%m-%d %H:%M:%S")
|
|
220
|
+
except ValueError:
|
|
221
|
+
started = "未知"
|
|
222
|
+
archive = (
|
|
223
|
+
str(session.transcript_path)
|
|
224
|
+
if conversation.persist and session.transcript_path is not None
|
|
225
|
+
else "未存档(persist_sessions: false)"
|
|
226
|
+
)
|
|
227
|
+
return "\n".join(
|
|
228
|
+
[
|
|
229
|
+
f"会话 {session.id}",
|
|
230
|
+
f" 存档 {archive}",
|
|
231
|
+
f" 标题 {current.title if current else '(尚无消息)'}",
|
|
232
|
+
f" 消息 {current.messages if current else 0} 条",
|
|
233
|
+
f" 开始 {started}",
|
|
234
|
+
"/resume 恢复其他会话。",
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def _session(ctx: CommandContext, argument: str) -> None:
|
|
240
|
+
sessions = await asyncio.to_thread(ctx.conversation.sessions)
|
|
241
|
+
await ctx.ui.note(session_report(ctx.conversation, sessions))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# Memory -----------------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def memory_report(conversation, name: str = "") -> str:
|
|
248
|
+
store = conversation.memory
|
|
249
|
+
files = conversation.instruction_files
|
|
250
|
+
instructions = [
|
|
251
|
+
"项目指令(启动、/clear 和恢复会话时加载)",
|
|
252
|
+
*(f" {item}" for item in files or ("(未找到 AGENTS.md 或 CLAUDE.md)",)),
|
|
253
|
+
]
|
|
254
|
+
if store is None:
|
|
255
|
+
return "\n".join([*instructions, "记忆已关闭(memory: false)。"])
|
|
256
|
+
notes = store.load()
|
|
257
|
+
if name:
|
|
258
|
+
matches = [note for note in notes if note.name == name]
|
|
259
|
+
if not matches:
|
|
260
|
+
return f"没有名为 {name} 的笔记。输入 /memory 查看全部笔记。"
|
|
261
|
+
parts = []
|
|
262
|
+
for note in matches:
|
|
263
|
+
try:
|
|
264
|
+
content = note.path.read_text(encoding="utf-8").strip()
|
|
265
|
+
except (OSError, UnicodeError):
|
|
266
|
+
content = "(无法读取)"
|
|
267
|
+
parts.append(f"{SCOPE_LABELS[note.scope]} · {note.path}\n\n{content}")
|
|
268
|
+
return "\n\n".join(parts)
|
|
269
|
+
lines = list(instructions)
|
|
270
|
+
for scope in ("user", "project"):
|
|
271
|
+
lines.append(f"{SCOPE_LABELS[scope]}笔记 · {store.directory(scope)}")
|
|
272
|
+
scoped = [note for note in notes if note.scope == scope]
|
|
273
|
+
for note in scoped:
|
|
274
|
+
if note.error:
|
|
275
|
+
lines.append(f" {note.path.name} 格式错误:{note.error}")
|
|
276
|
+
else:
|
|
277
|
+
lines.append(f" {note.updated} {note.type:<9} {note.name} — {note.description}")
|
|
278
|
+
if not scoped:
|
|
279
|
+
lines.append(" (暂无)")
|
|
280
|
+
lines.append(
|
|
281
|
+
f"自动笔记:{extraction_state(conversation)}。/memory 名称 查看全文;修改或删除可以直接编辑文件,"
|
|
282
|
+
"或让模型用 memory 工具完成。"
|
|
283
|
+
)
|
|
284
|
+
return "\n".join(lines)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def extraction_state(conversation) -> str:
|
|
288
|
+
extractor = conversation.extractor
|
|
289
|
+
return "关闭" if extractor is None else "已暂停" if extractor.paused else "开启"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def _memory(ctx: CommandContext, argument: str) -> None:
|
|
293
|
+
await ctx.ui.note(memory_report(ctx.conversation, argument))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _note_names(ctx: CommandContext):
|
|
297
|
+
store = ctx.conversation.memory
|
|
298
|
+
if store is None:
|
|
299
|
+
return []
|
|
300
|
+
seen, names = set(), []
|
|
301
|
+
for note in store.load():
|
|
302
|
+
if not note.error and note.name not in seen:
|
|
303
|
+
seen.add(note.name)
|
|
304
|
+
names.append((note.name, f"{SCOPE_LABELS[note.scope]} · {note.description}"))
|
|
305
|
+
return names
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# Model and modes --------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _providers(ctx: CommandContext):
|
|
312
|
+
return ctx.settings.providers if ctx.settings else ()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def _model(ctx: CommandContext, argument: str) -> None:
|
|
316
|
+
providers = _providers(ctx)
|
|
317
|
+
if argument:
|
|
318
|
+
if not any(item.name == argument for item in providers):
|
|
319
|
+
names = "、".join(item.name for item in providers) or "无"
|
|
320
|
+
await ctx.ui.note(f"未找到提供商 {argument}。可用:{names}")
|
|
321
|
+
else:
|
|
322
|
+
await ctx.ui.switch_model(argument)
|
|
323
|
+
return
|
|
324
|
+
current = ctx.ui.provider_name
|
|
325
|
+
if len(providers) < 2:
|
|
326
|
+
await ctx.ui.note(
|
|
327
|
+
f"当前模型:{current}"
|
|
328
|
+
+ (f"({ctx.ui.model})" if ctx.ui.model else "")
|
|
329
|
+
+ "。在配置文件的 providers 中添加更多提供商后可以切换。"
|
|
330
|
+
)
|
|
331
|
+
return
|
|
332
|
+
rows = [
|
|
333
|
+
Text(
|
|
334
|
+
f"{item.name}{' ✓' if item.name == current else ''} {item.protocol} / {item.model}",
|
|
335
|
+
no_wrap=True,
|
|
336
|
+
overflow="ellipsis",
|
|
337
|
+
)
|
|
338
|
+
for item in providers
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
async def pick(index: int) -> None:
|
|
342
|
+
await ctx.ui.switch_model(providers[index].name)
|
|
343
|
+
|
|
344
|
+
ctx.ui.choose(
|
|
345
|
+
"切换模型 · 对话历史保留", rows, next((i for i, item in enumerate(providers) if item.name == current), 0), pick
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _provider_names(ctx: CommandContext):
|
|
350
|
+
providers = _providers(ctx)
|
|
351
|
+
return [(item.name, "切换到该提供商") for item in providers] if len(providers) > 1 else []
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def _plan(ctx: CommandContext, argument: str) -> None:
|
|
355
|
+
try:
|
|
356
|
+
ctx.conversation.set_mode("plan")
|
|
357
|
+
except ProviderError as exc:
|
|
358
|
+
await ctx.ui.note(str(exc))
|
|
359
|
+
return
|
|
360
|
+
ctx.ui.refresh_status()
|
|
361
|
+
if argument:
|
|
362
|
+
await ctx.ui.submit(argument)
|
|
363
|
+
else:
|
|
364
|
+
await ctx.ui.note(PLAN_ENTERED)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
async def _do(ctx: CommandContext, argument: str) -> None:
|
|
368
|
+
"""Leave plan mode for the mode it was entered from; A task outside plan mode is not sent."""
|
|
369
|
+
registry = ctx.conversation.registry
|
|
370
|
+
if registry is None:
|
|
371
|
+
await ctx.ui.note("当前会话未启用工具。")
|
|
372
|
+
return
|
|
373
|
+
if registry.permissions.mode != "plan":
|
|
374
|
+
await ctx.ui.note("当前不在计划模式。" + ("任务未发送。" if argument else ""))
|
|
375
|
+
return
|
|
376
|
+
ctx.conversation.set_mode("normal")
|
|
377
|
+
mode = registry.permissions.mode
|
|
378
|
+
ctx.ui.refresh_status()
|
|
379
|
+
await ctx.ui.note(f"└ 已退出计划模式,回到{MODE_LABELS[mode]}({mode})。")
|
|
380
|
+
if argument:
|
|
381
|
+
await ctx.ui.submit(argument)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
async def _permissions(ctx: CommandContext, argument: str) -> None:
|
|
385
|
+
conversation = ctx.conversation
|
|
386
|
+
if not conversation.registry:
|
|
387
|
+
await ctx.ui.note("当前会话未启用工具。")
|
|
388
|
+
elif argument in MODES:
|
|
389
|
+
conversation.set_permission_mode(argument)
|
|
390
|
+
ctx.ui.refresh_status()
|
|
391
|
+
mode = conversation.registry.permissions.mode
|
|
392
|
+
await ctx.ui.note("└ 权限档位:" + MODE_LABELS[mode] + f"({mode});硬限制和显式拒绝仍生效。")
|
|
393
|
+
else:
|
|
394
|
+
await ctx.ui.note(PERMISSION_HELP)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# Status -----------------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _model_line(ctx: CommandContext) -> str:
|
|
401
|
+
name, model = ctx.ui.provider_name, ctx.ui.model
|
|
402
|
+
config = next((item for item in _providers(ctx) if item.name == name), None)
|
|
403
|
+
protocol = (
|
|
404
|
+
config.protocol if config else ctx.conversation.provider_info[1] if ctx.conversation.provider_info else ""
|
|
405
|
+
)
|
|
406
|
+
detail = " / ".join(part for part in (protocol, model) if part)
|
|
407
|
+
return name + (f"({detail})" if detail else "")
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _context_line(conversation) -> str:
|
|
411
|
+
usage = conversation.total_usage
|
|
412
|
+
estimate = conversation.context_estimate() if usage.requests else None
|
|
413
|
+
if estimate is None:
|
|
414
|
+
return "尚未发送请求"
|
|
415
|
+
budget = conversation.budget
|
|
416
|
+
line = (
|
|
417
|
+
f"约 {estimate:,} / {budget.effective:,} Token({round(100 * estimate / budget.effective)}%),"
|
|
418
|
+
f"自动压缩阈值 {budget.auto_threshold:,};已压缩 {conversation.compactions} 次"
|
|
419
|
+
)
|
|
420
|
+
return line + (";未配置 context_window,按 128K 估算" if not budget.configured else "")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _usage_line(conversation) -> str:
|
|
424
|
+
usage = conversation.total_usage
|
|
425
|
+
if not usage.requests:
|
|
426
|
+
return "尚未发送请求"
|
|
427
|
+
return (
|
|
428
|
+
f"输入 {usage.input_tokens:,}(缓存命中 {usage.cached_input_tokens:,})· 输出 {usage.output_tokens:,}"
|
|
429
|
+
f" · 请求 {usage.requests} 次"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _mcp_line(conversation) -> str:
|
|
434
|
+
manager = conversation.mcp_manager
|
|
435
|
+
if manager is None or not (manager.connections or manager.notices):
|
|
436
|
+
return "未配置"
|
|
437
|
+
states = [status.state for status in manager.statuses()]
|
|
438
|
+
ready = states.count("ready")
|
|
439
|
+
waiting = sum(state in {"new", "connecting"} for state in states)
|
|
440
|
+
failed = len(states) - ready - waiting
|
|
441
|
+
parts = (
|
|
442
|
+
[f"{ready} 个已连接"]
|
|
443
|
+
+ ([f"{waiting} 个连接中"] if waiting else [])
|
|
444
|
+
+ ([f"{failed} 个失败"] if failed else [])
|
|
445
|
+
+ ([f"{len(manager.notices)} 条配置问题"] if manager.notices else [])
|
|
446
|
+
)
|
|
447
|
+
return ",".join(parts) + "(/mcp 查看)"
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def status_report(ctx: CommandContext) -> str:
|
|
451
|
+
conversation = ctx.conversation
|
|
452
|
+
registry = conversation.registry
|
|
453
|
+
if registry is None:
|
|
454
|
+
mode = "未启用工具"
|
|
455
|
+
else:
|
|
456
|
+
current = registry.permissions.mode
|
|
457
|
+
mode = f"{MODE_BADGES[current]}({current})"
|
|
458
|
+
files = conversation.instruction_files
|
|
459
|
+
memory = (
|
|
460
|
+
"关闭(memory: false)" if conversation.memory is None else f"开启,自动笔记{extraction_state(conversation)}"
|
|
461
|
+
)
|
|
462
|
+
rows = [
|
|
463
|
+
("模型", _model_line(ctx)),
|
|
464
|
+
("权限档位", mode),
|
|
465
|
+
("上下文", _context_line(conversation)),
|
|
466
|
+
("本会话用量", _usage_line(conversation)),
|
|
467
|
+
("会话", conversation.session_id or "无"),
|
|
468
|
+
("工作目录", str(conversation.context_root or Path.cwd())),
|
|
469
|
+
("项目指令", f"{len(files)} 个文件(/memory 查看)" if files else "未找到"),
|
|
470
|
+
("MCP", _mcp_line(conversation)),
|
|
471
|
+
("记忆", memory),
|
|
472
|
+
]
|
|
473
|
+
if conversation.hooks is not None:
|
|
474
|
+
rows.append(("Hook", hooks_line(conversation.hooks)))
|
|
475
|
+
if conversation.active_skills:
|
|
476
|
+
rows.append(("已激活 Skill", "、".join(conversation.active_skills)))
|
|
477
|
+
if registry is not None and getattr(conversation, "agents_enabled", False):
|
|
478
|
+
rows.append(("子 Agent", agents_line(conversation)))
|
|
479
|
+
if getattr(conversation, "coordinator", False):
|
|
480
|
+
from .coordinator import STATUS
|
|
481
|
+
|
|
482
|
+
rows.append(("角色", STATUS))
|
|
483
|
+
width = max(12, *(cell_len(label) + 2 for label, _ in rows))
|
|
484
|
+
return "状态\n" + "\n".join(f" {label}{' ' * (width - cell_len(label))}{value}" for label, value in rows)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
async def _status(ctx: CommandContext, argument: str) -> None:
|
|
488
|
+
await ctx.ui.note(status_report(ctx))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# MCP --------------------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def mcp_report(conversation, name: str = "") -> str:
|
|
495
|
+
manager = conversation.mcp_manager
|
|
496
|
+
if manager is None or not (manager.connections or manager.notices):
|
|
497
|
+
return (
|
|
498
|
+
"未配置 MCP Server。在 ~/.mosscode/mcp.yaml 或项目 .mosscode/mcp.yaml 的 mcp_servers 中添加;"
|
|
499
|
+
"设置 import_mcp_json: true 可读取项目的 .mcp.json。"
|
|
500
|
+
)
|
|
501
|
+
statuses = manager.statuses()
|
|
502
|
+
if name:
|
|
503
|
+
status = next((item for item in statuses if item.name == name), None)
|
|
504
|
+
if status is None:
|
|
505
|
+
return f"未找到 MCP Server {name}。已配置:" + ("、".join(item.name for item in statuses) or "无")
|
|
506
|
+
lines = [f"MCP {status.name}({status.type})· {status.describe()}"]
|
|
507
|
+
for tool in status.tools:
|
|
508
|
+
summary = (
|
|
509
|
+
tool.description.removeprefix(f"MCP Server {tool.server_name} 的工具 {tool.remote_name}。")
|
|
510
|
+
.strip()
|
|
511
|
+
.splitlines()
|
|
512
|
+
or [""]
|
|
513
|
+
)[0]
|
|
514
|
+
lines.append(
|
|
515
|
+
f" {tool.name}" + (" [只读]" if tool.read_only else "") + (f" {summary[:80]}" if summary else "")
|
|
516
|
+
)
|
|
517
|
+
if status.state in {"ready", "closed"} and not status.tools:
|
|
518
|
+
lines.append(" (Server 没有提供工具)")
|
|
519
|
+
return "\n".join(lines)
|
|
520
|
+
marks = {"ready": "●", "connecting": "○", "new": "○"}
|
|
521
|
+
lines = [f"MCP Server({len(statuses)})"]
|
|
522
|
+
width = max((len(item.name) for item in statuses), default=0)
|
|
523
|
+
lines.extend(
|
|
524
|
+
f"{marks.get(item.state, '×')} {item.name.ljust(width)} {item.type:<5} {item.describe()}" for item in statuses
|
|
525
|
+
)
|
|
526
|
+
lines.extend("! " + notice for notice in manager.notices)
|
|
527
|
+
lines.append("/mcp 名称 查看工具;修改配置后重新启动生效,不自动重连。")
|
|
528
|
+
return "\n".join(lines)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
async def _mcp(ctx: CommandContext, argument: str) -> None:
|
|
532
|
+
await ctx.ui.note(mcp_report(ctx.conversation, argument))
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _server_names(ctx: CommandContext):
|
|
536
|
+
manager = ctx.conversation.mcp_manager
|
|
537
|
+
if manager is None:
|
|
538
|
+
return []
|
|
539
|
+
return [(connection.config.name, "查看该 Server 的工具") for connection in manager.connections]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# Skills -----------------------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _first_line(text: str) -> str:
|
|
546
|
+
return next((line.strip() for line in text.splitlines() if line.strip()), "(空)")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def skills_report(catalog, name: str = "", active=()) -> str:
|
|
550
|
+
if name:
|
|
551
|
+
skill = catalog.get(name)
|
|
552
|
+
if skill is None:
|
|
553
|
+
names = "、".join(catalog.skills) or "无"
|
|
554
|
+
return f"未找到 Skill {name}。已加载:{names}"
|
|
555
|
+
rows = [
|
|
556
|
+
("路径", str(skill.path)),
|
|
557
|
+
("来源", SOURCE_LABELS[skill.source]),
|
|
558
|
+
("覆盖", "、".join(str(path) for path in skill.overrides) or "无"),
|
|
559
|
+
("模式", skill.mode + (f"(fork_context: {skill.fork_context})" if skill.mode == "fork" else "")),
|
|
560
|
+
("模型", skill.model or "当前模型"),
|
|
561
|
+
("工具白名单", "、".join(skill.allowed_tools) or "未限制"),
|
|
562
|
+
("模型可见", "是" if skill.model_invocable else "否(disable-model-invocation)"),
|
|
563
|
+
("已激活", "是" if skill.name in active else "否"),
|
|
564
|
+
("命令", f"/{skill.name}" if skill.user_invocable else "不列出(user-invocable: false),仍可输入调用"),
|
|
565
|
+
("规程首行", _first_line(skill.body)),
|
|
566
|
+
]
|
|
567
|
+
lines = [f"Skill {skill.name}", *(f" {label}{' ' * (12 - cell_len(label))}{value}" for label, value in rows)]
|
|
568
|
+
if skill.tools:
|
|
569
|
+
lines.append(" 专属工具")
|
|
570
|
+
for tool in skill.tools:
|
|
571
|
+
lines.append(f" {tool.full_name} {' '.join(tool.command)} 超时 {tool.timeout:g} 秒")
|
|
572
|
+
return "\n".join(lines)
|
|
573
|
+
if not catalog.skills and not catalog.problems:
|
|
574
|
+
return "没有 Skill。可以在 .mosscode/skills/<名称>/SKILL.md 中添加。"
|
|
575
|
+
lines = [f"Skill({len(catalog.skills)})"]
|
|
576
|
+
width = max((cell_len(f"/{item}") for item in catalog.skills), default=0)
|
|
577
|
+
for skill in catalog.skills.values():
|
|
578
|
+
label = f"/{skill.name}"
|
|
579
|
+
lines.append(
|
|
580
|
+
f"{label}{' ' * (width - cell_len(label))} {SOURCE_LABELS[skill.source]} {skill.mode:<6} "
|
|
581
|
+
f"{skill.description}"
|
|
582
|
+
)
|
|
583
|
+
lines.extend("! " + problem for problem in catalog.problems)
|
|
584
|
+
lines.append("输入 /<名称> 调用;/skills 名称 查看详情;/skills reload 重新扫描。")
|
|
585
|
+
return "\n".join(lines)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
async def _skills(ctx: CommandContext, argument: str) -> None:
|
|
589
|
+
if argument != "reload":
|
|
590
|
+
await ctx.ui.note(skills_report(ctx.conversation.skills, argument, ctx.conversation.active_skills))
|
|
591
|
+
return
|
|
592
|
+
old = ctx.conversation.skills
|
|
593
|
+
catalog = await asyncio.to_thread(old.reload)
|
|
594
|
+
problems = ctx.registry.set_skills(catalog)
|
|
595
|
+
notices = ctx.conversation.use_skills(catalog)
|
|
596
|
+
added = [name for name in catalog.skills if name not in old.skills]
|
|
597
|
+
removed = [name for name in old.skills if name not in catalog.skills]
|
|
598
|
+
lines = [
|
|
599
|
+
f"└ 已重新扫描 Skill:共 {len(catalog.skills)} 个"
|
|
600
|
+
+ (f",新增 {'、'.join(added)}" if added else "")
|
|
601
|
+
+ (f",移除 {'、'.join(removed)}" if removed else "")
|
|
602
|
+
+ "。"
|
|
603
|
+
]
|
|
604
|
+
lines.extend(" ! " + problem for problem in (*catalog.problems, *problems, *notices))
|
|
605
|
+
await ctx.ui.note("\n".join(lines))
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _skill_names(ctx: CommandContext):
|
|
609
|
+
return [
|
|
610
|
+
("reload", "重新扫描 Skill 目录"),
|
|
611
|
+
*((skill.name, skill.description) for skill in ctx.conversation.skills.skills.values()),
|
|
612
|
+
]
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# Hooks ------------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
TRUST_OPTIONS = ("信任并启用", "本次不启用")
|
|
618
|
+
OUTCOME_LABELS = {
|
|
619
|
+
"ok": "成功",
|
|
620
|
+
"failed": "失败",
|
|
621
|
+
"rejected": "拒绝",
|
|
622
|
+
"timeout": "超时",
|
|
623
|
+
"cancelled": "已取消",
|
|
624
|
+
"skipped": "跳过",
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def hooks_line(engine) -> str:
|
|
629
|
+
if not engine.enabled:
|
|
630
|
+
return "已关闭(--no-hooks)"
|
|
631
|
+
waiting = sum(len(file.hooks) for file in engine.untrusted)
|
|
632
|
+
return f"已启用 {len(engine.hooks)} 条" + (f"(项目级未确认 {waiting} 条)" if waiting else "") + "(/hooks 查看)"
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
ACTION_LABELS = {"command": "命令", "http": "请求", "prompt": "注入", "agent": "子 Agent"}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _shown(path: Path, root: Path) -> str:
|
|
639
|
+
"""Hook files as the user knows them: relative inside the project, ~ under home."""
|
|
640
|
+
for base, prefix in ((root, ""), (Path.home(), "~")):
|
|
641
|
+
try:
|
|
642
|
+
relative = path.relative_to(base)
|
|
643
|
+
except ValueError:
|
|
644
|
+
continue
|
|
645
|
+
return str(Path(prefix, relative)) if prefix else str(relative)
|
|
646
|
+
return str(path)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _hook_lines(hook, head: str = "") -> list[str]:
|
|
650
|
+
"""One line for id and event, then the condition and the action indented below, so long ones wrap legibly."""
|
|
651
|
+
lines = [f" {hook.id} · {hook.event}{head}"]
|
|
652
|
+
if hook.condition is not None:
|
|
653
|
+
lines.append(f" 条件 {hook.condition.text}")
|
|
654
|
+
action = hook.action
|
|
655
|
+
if action is None:
|
|
656
|
+
lines.append(f" 拒绝 {hook.reject.replace(chr(10), ' ')}")
|
|
657
|
+
elif action.type in {"prompt", "agent"}:
|
|
658
|
+
text = action.message if action.type == "prompt" else action.prompt
|
|
659
|
+
lines.append(f" {ACTION_LABELS[action.type]} {text.replace(chr(10), ' ')[:60]}")
|
|
660
|
+
else:
|
|
661
|
+
lines.append(f" {ACTION_LABELS[action.type]} {action.summary()}")
|
|
662
|
+
return lines
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _hook_row(engine, hook) -> list[str]:
|
|
666
|
+
flags = "".join(f" [{flag}]" for flag in hook.flags())
|
|
667
|
+
return _hook_lines(hook, f"{flags} · {engine.status(hook)}")
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _run_row(run) -> str:
|
|
671
|
+
detail = f" {run.detail.splitlines()[0][:80]}" if run.detail.strip() else ""
|
|
672
|
+
return (
|
|
673
|
+
f" {run.time[11:19]} {run.hook} {run.event} {OUTCOME_LABELS.get(run.outcome, run.outcome)}"
|
|
674
|
+
f" {run.duration_ms} ms{detail}"
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _masked(raw: dict) -> dict:
|
|
679
|
+
"""Header values are secrets more often than not."""
|
|
680
|
+
action = raw.get("action")
|
|
681
|
+
if isinstance(action, dict) and isinstance(action.get("headers"), dict):
|
|
682
|
+
raw = {**raw, "action": {**action, "headers": {key: "***" for key in action["headers"]}}}
|
|
683
|
+
return raw
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def hooks_report(engine, name: str = "") -> str:
|
|
687
|
+
import yaml
|
|
688
|
+
|
|
689
|
+
if engine is None or not engine.enabled:
|
|
690
|
+
return "Hook 已关闭(本次运行使用了 --no-hooks)。"
|
|
691
|
+
if name:
|
|
692
|
+
hook = engine.find(name)
|
|
693
|
+
if hook is None:
|
|
694
|
+
known = "、".join(h.id for f in engine.config.files for h in f.hooks) or "无"
|
|
695
|
+
return f"未找到 Hook {name}。已加载:{known}"
|
|
696
|
+
lines = [
|
|
697
|
+
f"Hook {hook.id} · {HOOK_SOURCES[hook.source]} · {engine.status(hook)}",
|
|
698
|
+
f" 文件 {_shown(hook.path, engine.root)}",
|
|
699
|
+
" 配置",
|
|
700
|
+
]
|
|
701
|
+
dumped = yaml.safe_dump(_masked(hook.raw), allow_unicode=True, sort_keys=False).rstrip()
|
|
702
|
+
lines.extend(" " + line for line in dumped.splitlines())
|
|
703
|
+
runs = [run for run in engine.recent if run.hook == hook.id]
|
|
704
|
+
lines.append(" 本次运行的执行记录" + ("" if runs else ":无"))
|
|
705
|
+
lines.extend(" " + _run_row(run) for run in runs[-10:])
|
|
706
|
+
lines.append(f" 日志 {engine.log_path or '(本次运行没有会话目录)'}")
|
|
707
|
+
return "\n".join(lines)
|
|
708
|
+
files = [file for file in engine.config.files if file.hooks]
|
|
709
|
+
if not files and not engine.problems:
|
|
710
|
+
return (
|
|
711
|
+
"没有 Hook。可以在 ~/.mosscode/hooks.yaml 或项目 .mosscode/hooks.yaml、.mosscode/hooks.local.yaml 中添加,"
|
|
712
|
+
"格式见 docs/reference/hooks.md。"
|
|
713
|
+
)
|
|
714
|
+
lines = [f"Hook(已启用 {len(engine.hooks)} 条)"]
|
|
715
|
+
for file in files:
|
|
716
|
+
mark = "" if file.trusted else "(未确认,未启用)"
|
|
717
|
+
lines.append(f"{HOOK_SOURCES[file.source]} {_shown(file.path, engine.root)}{mark}")
|
|
718
|
+
for hook in file.hooks:
|
|
719
|
+
lines.extend(_hook_row(engine, hook))
|
|
720
|
+
lines.extend("! " + problem for problem in engine.problems)
|
|
721
|
+
for file in engine.untrusted:
|
|
722
|
+
lines.append(f"! 项目 Hook 文件 {_shown(file.path, engine.root)} 未确认;/hooks reload 后可以确认。")
|
|
723
|
+
if engine.recent:
|
|
724
|
+
lines.append("最近执行")
|
|
725
|
+
lines.extend(_run_row(run) for run in list(engine.recent)[-10:])
|
|
726
|
+
lines.append("/hooks <id> 查看详情;/hooks reload 重新加载。")
|
|
727
|
+
return "\n".join(lines)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def trust_text(file, root: Path) -> str:
|
|
731
|
+
lines = [
|
|
732
|
+
f"项目 Hook 文件 {_shown(file.path, root)} 尚未确认。",
|
|
733
|
+
"Hook 以 MossCode 的权限自动运行,不经过工具审批;请先确认内容:",
|
|
734
|
+
]
|
|
735
|
+
for hook in file.hooks:
|
|
736
|
+
lines.extend(_hook_lines(hook))
|
|
737
|
+
return "\n".join(lines)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
async def offer_trust(ui, engine, on_done=None) -> None:
|
|
741
|
+
"""One picker per untrusted project file; Esc means 本次不启用. Returns at once (callbacks chain)."""
|
|
742
|
+
pending = list(engine.pending_trust()) if engine is not None and engine.enabled else []
|
|
743
|
+
|
|
744
|
+
async def next_file() -> None:
|
|
745
|
+
if not pending:
|
|
746
|
+
if on_done is not None:
|
|
747
|
+
await on_done()
|
|
748
|
+
return
|
|
749
|
+
file = pending.pop(0)
|
|
750
|
+
shown = _shown(file.path, engine.root)
|
|
751
|
+
await ui.note(trust_text(file, engine.root))
|
|
752
|
+
|
|
753
|
+
async def pick(index: int) -> None:
|
|
754
|
+
if index == 0:
|
|
755
|
+
try:
|
|
756
|
+
engine.trust(file)
|
|
757
|
+
await ui.note(f"└ 已信任并启用 {shown}。")
|
|
758
|
+
except OSError as exc:
|
|
759
|
+
await ui.note(f"└ 无法写入信任记录({exc.strerror or exc}),未启用 {shown}。")
|
|
760
|
+
else:
|
|
761
|
+
engine.decline(file)
|
|
762
|
+
await ui.note(f"└ 本次不启用 {shown};/hooks reload 时会再次询问。")
|
|
763
|
+
ui.refresh_status()
|
|
764
|
+
await next_file()
|
|
765
|
+
|
|
766
|
+
async def cancel() -> None:
|
|
767
|
+
await pick(1)
|
|
768
|
+
|
|
769
|
+
ui.choose(f"信任项目 Hook · {file.path.name}", TRUST_OPTIONS, 0, pick, on_cancel=cancel)
|
|
770
|
+
|
|
771
|
+
await next_file()
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
async def _hooks(ctx: CommandContext, argument: str) -> None:
|
|
775
|
+
engine = ctx.conversation.hooks
|
|
776
|
+
if argument != "reload" or engine is None or not engine.enabled:
|
|
777
|
+
await ctx.ui.note(hooks_report(engine, argument))
|
|
778
|
+
return
|
|
779
|
+
added, removed = await asyncio.to_thread(engine.reload)
|
|
780
|
+
lines = [
|
|
781
|
+
f"└ 已重新加载 Hook:已启用 {len(engine.hooks)} 条"
|
|
782
|
+
+ (f",新增 {'、'.join(added)}" if added else "")
|
|
783
|
+
+ (f",移除 {'、'.join(removed)}" if removed else "")
|
|
784
|
+
+ "。"
|
|
785
|
+
]
|
|
786
|
+
lines.extend(" ! " + problem for problem in engine.problems)
|
|
787
|
+
await ctx.ui.note("\n".join(lines))
|
|
788
|
+
ctx.ui.refresh_status()
|
|
789
|
+
await offer_trust(ctx.ui, engine)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _hook_names(ctx: CommandContext):
|
|
793
|
+
engine = ctx.conversation.hooks
|
|
794
|
+
hooks = [hook for file in engine.config.files for hook in file.hooks] if engine is not None else []
|
|
795
|
+
return [("reload", "重新加载 Hook 文件"), *((hook.id, f"{hook.event} · {hook.summary()}") for hook in hooks)]
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# Sub-agents ---------------------------------------------------------------------------
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def agents_line(conversation) -> str:
|
|
802
|
+
catalog, tasks = conversation.agents, conversation.tasks
|
|
803
|
+
running = tasks.background()
|
|
804
|
+
line = f"{len(catalog.agents)} 个定义" + (f"({len(catalog.problems)} 个有问题)" if catalog.problems else "")
|
|
805
|
+
line += f",后台任务 {len(running)} 个运行中" if running else ",没有运行中的后台任务"
|
|
806
|
+
return line + "(/agents、/tasks 查看)"
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _model_missing(conversation, agent) -> bool:
|
|
810
|
+
return (
|
|
811
|
+
bool(agent.model)
|
|
812
|
+
and not conversation._is_current(agent.model)
|
|
813
|
+
and conversation._fork_provider_config(agent.model) is None
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _agent_model(conversation, agent) -> str:
|
|
818
|
+
"""A missing provider is shown here, and the call fails; it never falls back to the current model."""
|
|
819
|
+
if not agent.model:
|
|
820
|
+
return "继承当前模型"
|
|
821
|
+
return f"{agent.model}(没有该提供商,调用时报错)" if _model_missing(conversation, agent) else agent.model
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def agents_report(conversation, name: str = "") -> str:
|
|
825
|
+
if conversation.registry is None or not getattr(conversation, "agents_enabled", False):
|
|
826
|
+
return "当前会话未启用工具,子 Agent 不可用。"
|
|
827
|
+
catalog = conversation.agents
|
|
828
|
+
if name:
|
|
829
|
+
agent = catalog.get(name)
|
|
830
|
+
if agent is None:
|
|
831
|
+
return f"未找到 Agent 定义 {name}。已加载:" + ("、".join(catalog.agents) or "无")
|
|
832
|
+
names = conversation.registry.names()
|
|
833
|
+
available = [tool for tool in names if tool not in GLOBAL_BAN and agent.allows(tool)]
|
|
834
|
+
isolated = agent.isolation == "worktree"
|
|
835
|
+
lost = [tool for tool in available if tool not in WORKSPACE_TOOLS] if isolated else []
|
|
836
|
+
available = [tool for tool in available if tool not in lost]
|
|
837
|
+
rows = [
|
|
838
|
+
("路径", str(agent.path)),
|
|
839
|
+
("来源", AGENT_SOURCES[agent.source]),
|
|
840
|
+
("覆盖", "、".join(str(path) for path in agent.overrides) or "无"),
|
|
841
|
+
("描述", agent.description),
|
|
842
|
+
("模型", _agent_model(conversation, agent)),
|
|
843
|
+
(
|
|
844
|
+
"权限模式",
|
|
845
|
+
f"{agent.permission_mode or '继承会话'};本会话实际为 "
|
|
846
|
+
f"{effective_mode(agent, conversation.registry.permissions.mode)}",
|
|
847
|
+
),
|
|
848
|
+
("最大轮数", str(agent.max_turns) if agent.max_turns else "与主对话相同"),
|
|
849
|
+
("默认后台", "是" if agent.background else "否"),
|
|
850
|
+
("隔离", "worktree(在独立的 git 工作树中运行)" if isolated else "无"),
|
|
851
|
+
("工具白名单", "、".join(agent.tools) or "未限制"),
|
|
852
|
+
("禁用工具", "、".join(agent.disallowed_tools) or "无"),
|
|
853
|
+
("可用工具", "、".join(available) or "无"),
|
|
854
|
+
*([("隔离时不可用", "、".join(lost))] if lost else []),
|
|
855
|
+
("未支持字段", "、".join(agent.unsupported) or "无"),
|
|
856
|
+
("不存在的工具", "、".join(agent.unknown_tools(names)) or "无"),
|
|
857
|
+
("提示词首行", _first_line(agent.prompt)),
|
|
858
|
+
]
|
|
859
|
+
return "\n".join(
|
|
860
|
+
[f"Agent {agent.name}", *(f" {label}{' ' * (14 - cell_len(label))}{value}" for label, value in rows)]
|
|
861
|
+
)
|
|
862
|
+
lines = [f"Agent 定义({len(catalog.agents)})"]
|
|
863
|
+
width = max((cell_len(item) for item in catalog.agents), default=0)
|
|
864
|
+
for agent in catalog.agents.values():
|
|
865
|
+
marks = (f" [覆盖 {len(agent.overrides)} 个]" if agent.overrides else "") + (
|
|
866
|
+
f" [模型 {agent.model} 不存在]" if _model_missing(conversation, agent) else ""
|
|
867
|
+
)
|
|
868
|
+
lines.append(
|
|
869
|
+
f"{agent.name}{' ' * (width - cell_len(agent.name))} {AGENT_SOURCES[agent.source]} "
|
|
870
|
+
f"{agent.description}{marks}"
|
|
871
|
+
)
|
|
872
|
+
lines.extend("! " + problem for problem in catalog.problems)
|
|
873
|
+
lines.append("模型用 agent 工具调用;/agents 名称 查看详情;/agents reload 重新加载。")
|
|
874
|
+
return "\n".join(lines)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
async def _agents(ctx: CommandContext, argument: str) -> None:
|
|
878
|
+
conversation = ctx.conversation
|
|
879
|
+
if argument != "reload" or conversation.registry is None or not getattr(conversation, "agents_enabled", False):
|
|
880
|
+
await ctx.ui.note(agents_report(conversation, argument))
|
|
881
|
+
return
|
|
882
|
+
old = conversation.agents
|
|
883
|
+
catalog = await asyncio.to_thread(old.reload)
|
|
884
|
+
conversation.use_agents(catalog)
|
|
885
|
+
added = [name for name in catalog.agents if name not in old.agents]
|
|
886
|
+
removed = [name for name in old.agents if name not in catalog.agents]
|
|
887
|
+
changed = [name for name, agent in catalog.agents.items() if name in old.agents and agent != old.agents[name]]
|
|
888
|
+
lines = [
|
|
889
|
+
f"└ 已重新加载 Agent 定义:共 {len(catalog.agents)} 个"
|
|
890
|
+
+ (f",新增 {'、'.join(added)}" if added else "")
|
|
891
|
+
+ (f",移除 {'、'.join(removed)}" if removed else "")
|
|
892
|
+
+ (f",修改 {'、'.join(changed)}" if changed else "")
|
|
893
|
+
+ ";下一次请求起生效。"
|
|
894
|
+
]
|
|
895
|
+
lines.extend(" ! " + problem for problem in catalog.problems)
|
|
896
|
+
await ctx.ui.note("\n".join(lines))
|
|
897
|
+
ctx.ui.refresh_status()
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _agent_names(ctx: CommandContext):
|
|
901
|
+
if not getattr(ctx.conversation, "agents_enabled", False):
|
|
902
|
+
return []
|
|
903
|
+
return [
|
|
904
|
+
("reload", "重新加载 Agent 定义"),
|
|
905
|
+
*((agent.name, agent.description) for agent in ctx.conversation.agents.agents.values()),
|
|
906
|
+
]
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _task_usage(task) -> str:
|
|
910
|
+
usage = task.usage
|
|
911
|
+
return (
|
|
912
|
+
f"输入 {usage.input_tokens:,}(缓存命中 {usage.cached_input_tokens:,})· 输出 {usage.output_tokens:,}"
|
|
913
|
+
f" · 请求 {usage.requests} 次"
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _preview(text: str, lines: int = 12, size: int = 1500) -> list[str]:
|
|
918
|
+
shown = text[:size].splitlines()[:lines]
|
|
919
|
+
more = len(text) > size or len(text.splitlines()) > lines
|
|
920
|
+
return [*shown, *(["…(完整内容见对话记录)"] if more else [])]
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def tasks_report(conversation, task_id: str = "") -> str:
|
|
924
|
+
tasks = conversation.tasks
|
|
925
|
+
if task_id:
|
|
926
|
+
task = tasks.get(task_id)
|
|
927
|
+
if task is None:
|
|
928
|
+
return f"没有任务 {task_id}。输入 /tasks 查看本会话的任务。"
|
|
929
|
+
rows = [
|
|
930
|
+
("来源", ORIGIN_LABELS[task.origin]),
|
|
931
|
+
("类型", task.agent_type),
|
|
932
|
+
("标签", task.description),
|
|
933
|
+
("状态", STATUS_LABELS[task.status] + ("(后台)" if task.background else "(前台)")),
|
|
934
|
+
("统计", stats(task)),
|
|
935
|
+
("用量", _task_usage(task)),
|
|
936
|
+
("最近工具", task.last_tool or "无"),
|
|
937
|
+
("对话记录", str(task.transcript) if task.transcript is not None else "未保存"),
|
|
938
|
+
]
|
|
939
|
+
worktree = task.worktree
|
|
940
|
+
if worktree:
|
|
941
|
+
rows.append(("工作树", f"{worktree['relative']}(分支 {worktree['branch']})· {worktree['outcome']}"))
|
|
942
|
+
lines = [f"任务 {task.id}", *(f" {label}{' ' * (10 - cell_len(label))}{value}" for label, value in rows)]
|
|
943
|
+
lines.append(" 任务内容")
|
|
944
|
+
lines.extend(" " + line for line in _preview(task.prompt, 6, 600))
|
|
945
|
+
if task.result:
|
|
946
|
+
lines.append(" 结果")
|
|
947
|
+
lines.extend(" " + line for line in _preview(task.result))
|
|
948
|
+
if task.status == "running":
|
|
949
|
+
lines.append(f"/tasks stop {task.id} 停止。")
|
|
950
|
+
return "\n".join(lines)
|
|
951
|
+
if not tasks.tasks:
|
|
952
|
+
return "本会话还没有子 Agent 任务。"
|
|
953
|
+
lines = [f"任务({len(tasks.tasks)})"]
|
|
954
|
+
for task in tasks.tasks.values():
|
|
955
|
+
place = "后台" if task.background else "前台"
|
|
956
|
+
lines.append(
|
|
957
|
+
f" {task.id:<4} {STATUS_LABELS[task.status]} {place} {task.label} "
|
|
958
|
+
f"{task.elapsed:.0f} 秒 Token {task.tokens:,}"
|
|
959
|
+
)
|
|
960
|
+
lines.append("/tasks <id> 查看详情;/tasks stop <id> 停止。")
|
|
961
|
+
return "\n".join(lines)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
async def _tasks(ctx: CommandContext, argument: str) -> None:
|
|
965
|
+
action, _, rest = argument.partition(" ")
|
|
966
|
+
if action != "stop":
|
|
967
|
+
await ctx.ui.note(tasks_report(ctx.conversation, argument))
|
|
968
|
+
return
|
|
969
|
+
task_id = rest.strip()
|
|
970
|
+
if not task_id:
|
|
971
|
+
await ctx.ui.note("用法:/tasks stop <id>")
|
|
972
|
+
return
|
|
973
|
+
tasks = ctx.conversation.tasks
|
|
974
|
+
task = tasks.get(task_id)
|
|
975
|
+
if task is None:
|
|
976
|
+
await ctx.ui.note(f"没有任务 {task_id}。")
|
|
977
|
+
elif await tasks.stop(task_id):
|
|
978
|
+
await ctx.ui.note(f"└ 已停止任务 {task_id}({task.label});不会再收到它的通知。")
|
|
979
|
+
else:
|
|
980
|
+
await ctx.ui.note(f"任务 {task_id} 已结束({STATUS_LABELS[task.status]}),无需停止。")
|
|
981
|
+
ctx.ui.refresh_status()
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _task_names(ctx: CommandContext):
|
|
985
|
+
tasks = getattr(ctx.conversation, "tasks", None)
|
|
986
|
+
if tasks is None:
|
|
987
|
+
return []
|
|
988
|
+
return [
|
|
989
|
+
*((task.id, f"{STATUS_LABELS[task.status]} · {task.label}") for task in tasks.tasks.values()),
|
|
990
|
+
*((f"stop {task.id}", f"停止 · {task.label}") for task in tasks.running()),
|
|
991
|
+
]
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
# Worktrees ---------------------------------------------------------------------------
|
|
995
|
+
|
|
996
|
+
WORKTREE_USAGE = (
|
|
997
|
+
"用法:/worktree [list] · /worktree create <名称> · /worktree remove <名称> [--discard] · /worktree clean"
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def worktree_manager(conversation):
|
|
1002
|
+
"""(manager, problem)."""
|
|
1003
|
+
from .worktree import WorktreeError
|
|
1004
|
+
|
|
1005
|
+
getter = getattr(conversation, "worktree_manager", None)
|
|
1006
|
+
if getter is None:
|
|
1007
|
+
return None, "当前会话不支持工作树。"
|
|
1008
|
+
try:
|
|
1009
|
+
return getter(), ""
|
|
1010
|
+
except WorktreeError as exc:
|
|
1011
|
+
return None, f"工作树不可用:{exc}"
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _when(stamp: str) -> str:
|
|
1015
|
+
try:
|
|
1016
|
+
return datetime.fromisoformat(stamp).astimezone().strftime("%Y-%m-%d %H:%M")
|
|
1017
|
+
except (TypeError, ValueError):
|
|
1018
|
+
return stamp or "未知"
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
async def worktree_report(manager) -> str:
|
|
1022
|
+
entries = await manager.entries()
|
|
1023
|
+
if not entries:
|
|
1024
|
+
return "没有 MossCode 管理的工作树。/worktree create <名称> 创建一个;子 Agent 隔离运行时会自动创建。"
|
|
1025
|
+
lines = [f"工作树({len(entries)})"]
|
|
1026
|
+
for entry in entries:
|
|
1027
|
+
meta = entry.meta
|
|
1028
|
+
if meta is None:
|
|
1029
|
+
lines.append(f" {entry.name} {entry.status}")
|
|
1030
|
+
continue
|
|
1031
|
+
lines.append(f" {meta.name} {'手动' if meta.manual else '临时'} {entry.status}")
|
|
1032
|
+
lines.append(
|
|
1033
|
+
f" 分支 {meta.branch},基于 {meta.base[:7]},创建于 {_when(meta.created)};{manager.display(meta.path)}"
|
|
1034
|
+
)
|
|
1035
|
+
lines.append("/worktree remove <名称> 删除;/worktree clean 清理过期的临时工作树。")
|
|
1036
|
+
return "\n".join(lines)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def clean_text(report, *, automatic: bool = False) -> str:
|
|
1040
|
+
if automatic:
|
|
1041
|
+
return f"└ 已自动清理 {len(report.removed)} 个过期的临时工作树:{'、'.join(report.removed)}"
|
|
1042
|
+
lines = [
|
|
1043
|
+
f"└ 已删除 {len(report.removed)} 个,跳过 {len(report.skipped)} 个"
|
|
1044
|
+
+ (f":{'、'.join(report.removed)}" if report.removed else "。")
|
|
1045
|
+
]
|
|
1046
|
+
lines.extend(f" {name}:{reason}" for name, reason in report.skipped)
|
|
1047
|
+
return "\n".join(lines)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
async def _worktree(ctx: CommandContext, argument: str) -> None:
|
|
1051
|
+
from .worktree import WorktreeError
|
|
1052
|
+
|
|
1053
|
+
action, _, rest = argument.strip().partition(" ")
|
|
1054
|
+
rest = rest.strip()
|
|
1055
|
+
manager, problem = worktree_manager(ctx.conversation)
|
|
1056
|
+
if manager is None:
|
|
1057
|
+
await ctx.ui.note(problem)
|
|
1058
|
+
return
|
|
1059
|
+
try:
|
|
1060
|
+
if action in ("", "list") and not rest:
|
|
1061
|
+
await ctx.ui.note(await worktree_report(manager))
|
|
1062
|
+
elif action == "create" and rest and " " not in rest:
|
|
1063
|
+
created = await manager.create(rest)
|
|
1064
|
+
meta = created.meta
|
|
1065
|
+
root = manager.root_in(meta)
|
|
1066
|
+
head = f"工作树 {meta.name} 已存在" if created.resumed else f"已创建工作树 {meta.name}"
|
|
1067
|
+
lines = [
|
|
1068
|
+
f"└ {head}:{manager.display(meta.path)}",
|
|
1069
|
+
f" 分支 {meta.branch},基于 {meta.base[:7]}",
|
|
1070
|
+
*(f" ! {warning}" for warning in created.warnings),
|
|
1071
|
+
*([" ! 主目录有未提交的改动,它们不在这个工作树中"] if created.dirty else []),
|
|
1072
|
+
f' 在另一个终端中进入:cd "{root}"',
|
|
1073
|
+
]
|
|
1074
|
+
await ctx.ui.note("\n".join(lines))
|
|
1075
|
+
elif action == "remove" and rest:
|
|
1076
|
+
words = rest.split()
|
|
1077
|
+
discard = "--discard" in words
|
|
1078
|
+
names = [word for word in words if word != "--discard"]
|
|
1079
|
+
if len(names) != 1:
|
|
1080
|
+
await ctx.ui.note(WORKTREE_USAGE)
|
|
1081
|
+
return
|
|
1082
|
+
changes = await manager.remove(names[0], discard=discard)
|
|
1083
|
+
dropped = f"(已丢弃:{changes.describe()})" if changes.any else ""
|
|
1084
|
+
await ctx.ui.note(f"└ 已删除工作树 {names[0]} 和它的分支{dropped}。")
|
|
1085
|
+
elif action == "clean" and not rest:
|
|
1086
|
+
await ctx.ui.note(clean_text(await manager.clean()))
|
|
1087
|
+
else:
|
|
1088
|
+
await ctx.ui.note(WORKTREE_USAGE)
|
|
1089
|
+
except WorktreeError as exc:
|
|
1090
|
+
await ctx.ui.note(f"! {exc}")
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _worktree_names(ctx: CommandContext):
|
|
1094
|
+
choices = [
|
|
1095
|
+
("list", "列出工作树"),
|
|
1096
|
+
("create", "创建或恢复工作树"),
|
|
1097
|
+
("remove", "删除工作树"),
|
|
1098
|
+
("clean", "清理过期的临时工作树"),
|
|
1099
|
+
]
|
|
1100
|
+
manager, _ = worktree_manager(ctx.conversation) if getattr(ctx.conversation, "_worktrees", None) else (None, "")
|
|
1101
|
+
if manager is not None:
|
|
1102
|
+
choices.extend(
|
|
1103
|
+
(f"remove {meta.name}", "删除 · " + ("手动" if meta.manual else "临时")) for _, meta in manager.metas()
|
|
1104
|
+
)
|
|
1105
|
+
return choices
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _worktree_reads(argument: str) -> bool:
|
|
1109
|
+
"""Listing is safe during a run."""
|
|
1110
|
+
return argument.strip() in ("", "list")
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
# Teams -------------------------------------------------------------------------------
|
|
1114
|
+
|
|
1115
|
+
TEAM_USAGE = "用法:/team · /team <名称> · /team stop <成员> · /team delete <名称>"
|
|
1116
|
+
TEAM_DETAILS = (
|
|
1117
|
+
"子命令:\n"
|
|
1118
|
+
" /team 列出本项目的团队:成员数、各状态人数、当前团队和持有者\n"
|
|
1119
|
+
" /team <名称> 团队详情:成员状态、后端、工作树、最近工具调用、用量、未读消息和任务\n"
|
|
1120
|
+
" /team stop <成员> 请当前团队的成员在这一轮结束后停止,超时强制停止\n"
|
|
1121
|
+
" /team delete <名称> 确认后停止成员、删除已合并且干净的工作树和团队目录"
|
|
1122
|
+
)
|
|
1123
|
+
DELETE_OPTIONS = ("删除团队", "取消")
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
async def _team(ctx: CommandContext, argument: str) -> None:
|
|
1127
|
+
from .teams import TeamError
|
|
1128
|
+
|
|
1129
|
+
runtime = getattr(ctx.conversation, "teams", None)
|
|
1130
|
+
if runtime is None:
|
|
1131
|
+
await ctx.ui.note("当前会话不支持团队。")
|
|
1132
|
+
return
|
|
1133
|
+
words = argument.split()
|
|
1134
|
+
try:
|
|
1135
|
+
if not words:
|
|
1136
|
+
await ctx.ui.note(await asyncio.to_thread(runtime.overview))
|
|
1137
|
+
elif words[0] == "stop" and len(words) == 2:
|
|
1138
|
+
await ctx.ui.note("└ " + await runtime.stop_member(words[1]))
|
|
1139
|
+
ctx.ui.refresh_status()
|
|
1140
|
+
elif words[0] == "delete" and len(words) == 2:
|
|
1141
|
+
name = words[1]
|
|
1142
|
+
await asyncio.to_thread(runtime.detail, name) # unknown_team before asking.
|
|
1143
|
+
|
|
1144
|
+
async def picked(index: int) -> None:
|
|
1145
|
+
if index != 0:
|
|
1146
|
+
await ctx.ui.note("已取消,团队保留。")
|
|
1147
|
+
return
|
|
1148
|
+
try:
|
|
1149
|
+
text, _ = await runtime.delete(name, force=True)
|
|
1150
|
+
except TeamError as exc:
|
|
1151
|
+
await ctx.ui.note(f"! {exc.message}")
|
|
1152
|
+
return
|
|
1153
|
+
await ctx.ui.note("└ " + text)
|
|
1154
|
+
ctx.ui.refresh_status()
|
|
1155
|
+
|
|
1156
|
+
async def cancelled() -> None:
|
|
1157
|
+
await ctx.ui.note("已取消,团队保留。")
|
|
1158
|
+
|
|
1159
|
+
ctx.ui.choose(
|
|
1160
|
+
f"删除团队 {name}?会停止它的成员,删除已合并且干净的工作树和团队目录(任务、邮箱、对话记录)。",
|
|
1161
|
+
DELETE_OPTIONS,
|
|
1162
|
+
1,
|
|
1163
|
+
picked,
|
|
1164
|
+
cancelled,
|
|
1165
|
+
)
|
|
1166
|
+
elif len(words) == 1 and words[0] not in ("stop", "delete"):
|
|
1167
|
+
await ctx.ui.note(await asyncio.to_thread(runtime.detail, words[0]))
|
|
1168
|
+
else:
|
|
1169
|
+
await ctx.ui.note(TEAM_USAGE)
|
|
1170
|
+
except TeamError as exc:
|
|
1171
|
+
await ctx.ui.note(f"! {exc.message}")
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _team_names(ctx: CommandContext):
|
|
1175
|
+
runtime = getattr(ctx.conversation, "teams", None)
|
|
1176
|
+
if runtime is None:
|
|
1177
|
+
return []
|
|
1178
|
+
choices = []
|
|
1179
|
+
for store in runtime.teams():
|
|
1180
|
+
choices += [(store.name, "团队详情"), (f"delete {store.name}", "删除团队")]
|
|
1181
|
+
if runtime.current is not None:
|
|
1182
|
+
choices += [(f"stop {name}", f"停止 · {runner.status}") for name, runner in runtime.runners.items()]
|
|
1183
|
+
return choices
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _team_reads(argument: str) -> bool:
|
|
1187
|
+
"""Listing, details and stopping are safe during a run; deleting waits."""
|
|
1188
|
+
return argument.split()[:1] != ["delete"]
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
async def _exit(ctx: CommandContext, argument: str) -> None:
|
|
1192
|
+
async def leave() -> None:
|
|
1193
|
+
ctx.ui.exit()
|
|
1194
|
+
|
|
1195
|
+
await confirm_tasks(ctx.ui, ctx.conversation, leave)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
BUILTIN = CommandRegistry(
|
|
1199
|
+
(
|
|
1200
|
+
Command(
|
|
1201
|
+
"help",
|
|
1202
|
+
"查看命令和快捷键",
|
|
1203
|
+
"/help [命令]",
|
|
1204
|
+
"local",
|
|
1205
|
+
argument="optional",
|
|
1206
|
+
argument_hint="[命令]",
|
|
1207
|
+
handler=_help,
|
|
1208
|
+
),
|
|
1209
|
+
Command("clear", "开始新会话:清空模型上下文和 Token 统计", "/clear", "local", handler=_clear),
|
|
1210
|
+
Command(
|
|
1211
|
+
"compact",
|
|
1212
|
+
"压缩上下文:较早的对话换成摘要,近期消息保留",
|
|
1213
|
+
"/compact [说明]",
|
|
1214
|
+
"local",
|
|
1215
|
+
argument="optional",
|
|
1216
|
+
argument_hint="[说明]",
|
|
1217
|
+
handler=_compact,
|
|
1218
|
+
),
|
|
1219
|
+
Command(
|
|
1220
|
+
"resume",
|
|
1221
|
+
"恢复本项目之前的会话",
|
|
1222
|
+
"/resume [ID]",
|
|
1223
|
+
"local",
|
|
1224
|
+
argument="optional",
|
|
1225
|
+
argument_hint="[ID]",
|
|
1226
|
+
handler=_resume,
|
|
1227
|
+
),
|
|
1228
|
+
Command("session", "查看当前会话的 ID、存档位置和消息数", "/session", "local", handler=_session),
|
|
1229
|
+
Command(
|
|
1230
|
+
"memory",
|
|
1231
|
+
"查看已加载的项目指令和记忆笔记",
|
|
1232
|
+
"/memory [名称]",
|
|
1233
|
+
"local",
|
|
1234
|
+
argument="optional",
|
|
1235
|
+
argument_hint="[名称]",
|
|
1236
|
+
handler=_memory,
|
|
1237
|
+
complete=_note_names,
|
|
1238
|
+
),
|
|
1239
|
+
Command(
|
|
1240
|
+
"model",
|
|
1241
|
+
"切换提供商与模型,对话历史保留",
|
|
1242
|
+
"/model [名称]",
|
|
1243
|
+
"local",
|
|
1244
|
+
argument="optional",
|
|
1245
|
+
argument_hint="[名称]",
|
|
1246
|
+
handler=_model,
|
|
1247
|
+
complete=_provider_names,
|
|
1248
|
+
),
|
|
1249
|
+
Command(
|
|
1250
|
+
"plan",
|
|
1251
|
+
"只读调查和规划,提交计划经批准后执行",
|
|
1252
|
+
"/plan [任务]",
|
|
1253
|
+
"local",
|
|
1254
|
+
argument="optional",
|
|
1255
|
+
argument_hint="[任务]",
|
|
1256
|
+
handler=_plan,
|
|
1257
|
+
),
|
|
1258
|
+
Command(
|
|
1259
|
+
"do",
|
|
1260
|
+
"退出计划模式,回到进入前的权限档位",
|
|
1261
|
+
"/do [任务]",
|
|
1262
|
+
"local",
|
|
1263
|
+
argument="optional",
|
|
1264
|
+
argument_hint="[任务]",
|
|
1265
|
+
handler=_do,
|
|
1266
|
+
),
|
|
1267
|
+
Command(
|
|
1268
|
+
"permissions",
|
|
1269
|
+
"查看或切换工具权限档位",
|
|
1270
|
+
"/permissions [档位]",
|
|
1271
|
+
"local",
|
|
1272
|
+
aliases=("permission",),
|
|
1273
|
+
argument="optional",
|
|
1274
|
+
argument_hint="[档位]",
|
|
1275
|
+
handler=_permissions,
|
|
1276
|
+
complete=lambda ctx: MODE_CHOICES,
|
|
1277
|
+
),
|
|
1278
|
+
Command(
|
|
1279
|
+
"status", "查看模型、权限、上下文、用量和连接状态", "/status", "local", handler=_status, while_running=True
|
|
1280
|
+
),
|
|
1281
|
+
Command(
|
|
1282
|
+
"mcp",
|
|
1283
|
+
"查看 MCP Server 的连接状态与工具",
|
|
1284
|
+
"/mcp [名称]",
|
|
1285
|
+
"local",
|
|
1286
|
+
argument="optional",
|
|
1287
|
+
argument_hint="[名称]",
|
|
1288
|
+
handler=_mcp,
|
|
1289
|
+
complete=_server_names,
|
|
1290
|
+
),
|
|
1291
|
+
Command(
|
|
1292
|
+
"skills",
|
|
1293
|
+
"查看、重新扫描 Skill",
|
|
1294
|
+
"/skills [名称|reload]",
|
|
1295
|
+
"local",
|
|
1296
|
+
aliases=("skill",),
|
|
1297
|
+
argument="optional",
|
|
1298
|
+
argument_hint="[名称|reload]",
|
|
1299
|
+
handler=_skills,
|
|
1300
|
+
complete=_skill_names,
|
|
1301
|
+
),
|
|
1302
|
+
Command(
|
|
1303
|
+
"hooks",
|
|
1304
|
+
"查看、重新加载 Hook",
|
|
1305
|
+
"/hooks [id|reload]",
|
|
1306
|
+
"local",
|
|
1307
|
+
aliases=("hook",),
|
|
1308
|
+
argument="optional",
|
|
1309
|
+
argument_hint="[id|reload]",
|
|
1310
|
+
handler=_hooks,
|
|
1311
|
+
complete=_hook_names,
|
|
1312
|
+
),
|
|
1313
|
+
Command(
|
|
1314
|
+
"agents",
|
|
1315
|
+
"查看、重新加载子 Agent 定义",
|
|
1316
|
+
"/agents [名称|reload]",
|
|
1317
|
+
"local",
|
|
1318
|
+
aliases=("agent",),
|
|
1319
|
+
argument="optional",
|
|
1320
|
+
argument_hint="[名称|reload]",
|
|
1321
|
+
handler=_agents,
|
|
1322
|
+
complete=_agent_names,
|
|
1323
|
+
),
|
|
1324
|
+
Command(
|
|
1325
|
+
"tasks",
|
|
1326
|
+
"查看、停止本会话的子 Agent 任务",
|
|
1327
|
+
"/tasks [id|stop id]",
|
|
1328
|
+
"local",
|
|
1329
|
+
aliases=("task",),
|
|
1330
|
+
argument="optional",
|
|
1331
|
+
argument_hint="[id|stop id]",
|
|
1332
|
+
handler=_tasks,
|
|
1333
|
+
complete=_task_names,
|
|
1334
|
+
while_running=True,
|
|
1335
|
+
),
|
|
1336
|
+
Command(
|
|
1337
|
+
"worktree",
|
|
1338
|
+
"查看、创建、删除 git 工作树",
|
|
1339
|
+
"/worktree [子命令]",
|
|
1340
|
+
"local",
|
|
1341
|
+
aliases=("worktrees",),
|
|
1342
|
+
argument="optional",
|
|
1343
|
+
argument_hint="[子命令]",
|
|
1344
|
+
handler=_worktree,
|
|
1345
|
+
complete=_worktree_names,
|
|
1346
|
+
while_running=_worktree_reads,
|
|
1347
|
+
),
|
|
1348
|
+
Command(
|
|
1349
|
+
"team",
|
|
1350
|
+
"查看、停止、删除 Agent 团队",
|
|
1351
|
+
"/team [子命令]",
|
|
1352
|
+
"local",
|
|
1353
|
+
aliases=("teams",),
|
|
1354
|
+
argument="optional",
|
|
1355
|
+
argument_hint="[子命令]",
|
|
1356
|
+
handler=_team,
|
|
1357
|
+
complete=_team_names,
|
|
1358
|
+
while_running=_team_reads,
|
|
1359
|
+
details=TEAM_DETAILS,
|
|
1360
|
+
),
|
|
1361
|
+
Command("exit", "退出 MossCode", "/exit", "local", aliases=("quit",), handler=_exit),
|
|
1362
|
+
)
|
|
1363
|
+
)
|