msapling-cli 0.1.2__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.
- msapling_cli/__init__.py +2 -0
- msapling_cli/agent.py +671 -0
- msapling_cli/api.py +394 -0
- msapling_cli/completer.py +415 -0
- msapling_cli/config.py +56 -0
- msapling_cli/local.py +133 -0
- msapling_cli/main.py +1038 -0
- msapling_cli/mcp/__init__.py +1 -0
- msapling_cli/mcp/server.py +411 -0
- msapling_cli/memory.py +97 -0
- msapling_cli/session.py +102 -0
- msapling_cli/shell.py +1583 -0
- msapling_cli/storage.py +265 -0
- msapling_cli/tier.py +78 -0
- msapling_cli/tui.py +475 -0
- msapling_cli/worker_pool.py +233 -0
- msapling_cli-0.1.2.dist-info/METADATA +132 -0
- msapling_cli-0.1.2.dist-info/RECORD +22 -0
- msapling_cli-0.1.2.dist-info/WHEEL +5 -0
- msapling_cli-0.1.2.dist-info/entry_points.txt +3 -0
- msapling_cli-0.1.2.dist-info/licenses/LICENSE +21 -0
- msapling_cli-0.1.2.dist-info/top_level.txt +1 -0
msapling_cli/tui.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""MSapling TUI — Terminal UI layer matching Claude Code / Codex / Gemini CLI polish.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Status bar at bottom (model, cost, tokens, mode, project)
|
|
5
|
+
- Animated working indicator with shimmer effect
|
|
6
|
+
- Inline colored diffs for file edits
|
|
7
|
+
- Styled tool execution blocks with spinners
|
|
8
|
+
- Themed prompt and response rendering
|
|
9
|
+
- Width-responsive layout
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from rich.console import Console, Group
|
|
19
|
+
from rich.columns import Columns
|
|
20
|
+
from rich.live import Live
|
|
21
|
+
from rich.markdown import Markdown
|
|
22
|
+
from rich.markup import escape
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.rule import Rule
|
|
25
|
+
from rich.style import Style
|
|
26
|
+
from rich.syntax import Syntax
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
from rich.theme import Theme
|
|
30
|
+
|
|
31
|
+
# ─── MSapling Color Theme ─────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
MSAPLING_THEME = Theme({
|
|
34
|
+
"ms.accent": "bold #3b82f6", # Blue accent
|
|
35
|
+
"ms.accent2": "#22c55e", # Green
|
|
36
|
+
"ms.warn": "#f59e0b", # Amber
|
|
37
|
+
"ms.error": "bold #ef4444", # Red
|
|
38
|
+
"ms.dim": "dim #64748b", # Slate gray
|
|
39
|
+
"ms.muted": "#94a3b8", # Light slate
|
|
40
|
+
"ms.tool": "bold #a78bfa", # Purple for tool names
|
|
41
|
+
"ms.file": "bold #38bdf8", # Sky blue for file paths
|
|
42
|
+
"ms.cost": "#facc15", # Yellow for costs
|
|
43
|
+
"ms.prompt": "bold #22c55e", # Green prompt
|
|
44
|
+
"ms.user": "bold #e2e8f0", # Bright user text
|
|
45
|
+
"ms.assistant": "#e2e8f0", # Assistant text
|
|
46
|
+
"ms.diff.add": "#22c55e", # Diff added
|
|
47
|
+
"ms.diff.del": "#ef4444", # Diff removed
|
|
48
|
+
"ms.diff.hunk": "bold #3b82f6", # Diff hunk header
|
|
49
|
+
"ms.status": "#94a3b8 on #1e293b", # Status bar
|
|
50
|
+
"ms.status.key": "bold #3b82f6 on #1e293b",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
console = Console(theme=MSAPLING_THEME)
|
|
54
|
+
|
|
55
|
+
# ─── Working Indicator ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
58
|
+
_SHIMMER_CHARS = "MSapling"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def shimmer_text(label: str = "Working", elapsed: float = 0.0) -> Text:
|
|
62
|
+
"""Create a shimmer animation effect on text (like Claude Code's 'Working' indicator)."""
|
|
63
|
+
text = Text()
|
|
64
|
+
frame_idx = int(elapsed * 8) % len(_SPINNER_FRAMES)
|
|
65
|
+
text.append(f" {_SPINNER_FRAMES[frame_idx]} ", style="ms.accent")
|
|
66
|
+
|
|
67
|
+
# Shimmer: sweep a bright highlight across the label
|
|
68
|
+
cycle = 2.0 # seconds per sweep
|
|
69
|
+
phase = (elapsed % cycle) / cycle
|
|
70
|
+
for i, ch in enumerate(label):
|
|
71
|
+
char_pos = i / max(len(label) - 1, 1)
|
|
72
|
+
distance = abs(char_pos - phase)
|
|
73
|
+
brightness = max(0, 1.0 - distance * 3)
|
|
74
|
+
if brightness > 0.5:
|
|
75
|
+
text.append(ch, style="bold #ffffff")
|
|
76
|
+
elif brightness > 0.2:
|
|
77
|
+
text.append(ch, style="#e2e8f0")
|
|
78
|
+
else:
|
|
79
|
+
text.append(ch, style="ms.dim")
|
|
80
|
+
|
|
81
|
+
# Elapsed time
|
|
82
|
+
if elapsed >= 60:
|
|
83
|
+
mins = int(elapsed // 60)
|
|
84
|
+
secs = int(elapsed % 60)
|
|
85
|
+
time_str = f" {mins}m {secs}s"
|
|
86
|
+
else:
|
|
87
|
+
time_str = f" {elapsed:.0f}s"
|
|
88
|
+
text.append(time_str, style="ms.dim")
|
|
89
|
+
text.append(" press Esc to interrupt", style="ms.dim")
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ─── Status Bar ────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def status_bar(
|
|
96
|
+
model: str = "",
|
|
97
|
+
cost: float = 0.0,
|
|
98
|
+
tokens: int = 0,
|
|
99
|
+
mode: str = "agent",
|
|
100
|
+
project: str = "",
|
|
101
|
+
tier: str = "free",
|
|
102
|
+
credits: float = 0.0,
|
|
103
|
+
) -> Text:
|
|
104
|
+
"""Render a bottom status bar like Claude Code / Gemini CLI."""
|
|
105
|
+
width = console.width or 80
|
|
106
|
+
bar = Text()
|
|
107
|
+
|
|
108
|
+
# Left side: model + mode
|
|
109
|
+
model_short = model.split("/")[-1] if "/" in model else model
|
|
110
|
+
bar.append(f" {model_short} ", style="ms.status.key")
|
|
111
|
+
bar.append(" ", style="ms.status")
|
|
112
|
+
|
|
113
|
+
mode_style = "bold #22c55e on #1e293b" if mode == "agent" else "bold #f59e0b on #1e293b"
|
|
114
|
+
bar.append(f" {mode.upper()} ", style=mode_style)
|
|
115
|
+
bar.append(" ", style="ms.status")
|
|
116
|
+
|
|
117
|
+
# Center: project
|
|
118
|
+
proj_short = project[:20] if project else "~"
|
|
119
|
+
bar.append(f" {proj_short} ", style="ms.status")
|
|
120
|
+
|
|
121
|
+
# Right side: cost + tokens + credits
|
|
122
|
+
right_parts = []
|
|
123
|
+
if tokens > 0:
|
|
124
|
+
right_parts.append(f"{tokens:,} tok")
|
|
125
|
+
if cost > 0:
|
|
126
|
+
right_parts.append(f"${cost:.4f}")
|
|
127
|
+
right_parts.append(f"${credits:.2f} fuel")
|
|
128
|
+
right_parts.append(tier)
|
|
129
|
+
right_text = " ".join(right_parts)
|
|
130
|
+
|
|
131
|
+
# Pad to fill width
|
|
132
|
+
used = bar.cell_len + len(right_text) + 2
|
|
133
|
+
padding = max(0, width - used)
|
|
134
|
+
bar.append(" " * padding, style="ms.status")
|
|
135
|
+
bar.append(f" {right_text} ", style="ms.status")
|
|
136
|
+
|
|
137
|
+
return bar
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ─── Tool Execution Display ───────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def tool_header(tool_name: str, args: Dict[str, Any]) -> Panel:
|
|
143
|
+
"""Render a tool execution header with icon and args summary."""
|
|
144
|
+
icons = {
|
|
145
|
+
"read_file": "📖",
|
|
146
|
+
"write_file": "📝",
|
|
147
|
+
"edit_file": "✏️",
|
|
148
|
+
"run_command": "⚡",
|
|
149
|
+
"glob_files": "🔍",
|
|
150
|
+
"grep_search": "🔎",
|
|
151
|
+
"web_search": "🌐",
|
|
152
|
+
}
|
|
153
|
+
icon = icons.get(tool_name, "🔧")
|
|
154
|
+
|
|
155
|
+
# Format args concisely
|
|
156
|
+
if tool_name in ("read_file", "write_file", "edit_file"):
|
|
157
|
+
summary = args.get("path", "")
|
|
158
|
+
elif tool_name == "run_command":
|
|
159
|
+
summary = args.get("command", "")[:60]
|
|
160
|
+
elif tool_name == "glob_files":
|
|
161
|
+
summary = args.get("pattern", "")
|
|
162
|
+
elif tool_name == "grep_search":
|
|
163
|
+
summary = f"{args.get('pattern', '')} in {args.get('path', '.')}"
|
|
164
|
+
elif tool_name == "web_search":
|
|
165
|
+
summary = args.get("query", "")[:60]
|
|
166
|
+
else:
|
|
167
|
+
summary = str(args)[:60]
|
|
168
|
+
|
|
169
|
+
title = Text()
|
|
170
|
+
title.append(f" {icon} ", style="ms.tool")
|
|
171
|
+
title.append(tool_name, style="ms.tool")
|
|
172
|
+
title.append(f" {summary}", style="ms.file")
|
|
173
|
+
|
|
174
|
+
return Panel(title, border_style="ms.dim", padding=(0, 1), expand=True)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def tool_result_display(tool_name: str, result: str, args: Dict[str, Any] = None) -> Panel:
|
|
178
|
+
"""Render tool result with appropriate formatting."""
|
|
179
|
+
args = args or {}
|
|
180
|
+
|
|
181
|
+
# For edit_file, show a mini diff
|
|
182
|
+
if tool_name == "edit_file" and "Edited" in result:
|
|
183
|
+
old_s = args.get("old_string", "")
|
|
184
|
+
new_s = args.get("new_string", "")
|
|
185
|
+
diff_text = _make_inline_diff(old_s, new_s, args.get("path", ""))
|
|
186
|
+
content = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
|
|
187
|
+
return Panel(content, border_style="ms.accent2", title="[ms.accent2]edit applied[/ms.accent2]", padding=(0, 1))
|
|
188
|
+
|
|
189
|
+
# For run_command, show as terminal output
|
|
190
|
+
if tool_name == "run_command":
|
|
191
|
+
lines = result.strip().split("\n")
|
|
192
|
+
display = "\n".join(lines[:20])
|
|
193
|
+
if len(lines) > 20:
|
|
194
|
+
display += f"\n... ({len(lines) - 20} more lines)"
|
|
195
|
+
return Panel(
|
|
196
|
+
Text(display, style="ms.muted"),
|
|
197
|
+
border_style="ms.dim",
|
|
198
|
+
title="[ms.dim]output[/ms.dim]",
|
|
199
|
+
padding=(0, 1),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# For read_file, show truncated with syntax highlighting
|
|
203
|
+
if tool_name == "read_file" and len(result) > 200:
|
|
204
|
+
path = args.get("path", "")
|
|
205
|
+
ext = path.rsplit(".", 1)[-1] if "." in path else ""
|
|
206
|
+
lang = {"py": "python", "ts": "typescript", "js": "javascript", "tsx": "tsx", "rs": "rust", "go": "go"}.get(ext, ext)
|
|
207
|
+
lines = result.split("\n")
|
|
208
|
+
preview = "\n".join(lines[:15])
|
|
209
|
+
if len(lines) > 15:
|
|
210
|
+
preview += f"\n... ({len(lines) - 15} more lines)"
|
|
211
|
+
return Panel(
|
|
212
|
+
Syntax(preview, lang or "text", theme="monokai", line_numbers=True),
|
|
213
|
+
border_style="ms.dim",
|
|
214
|
+
title=f"[ms.file]{path}[/ms.file]",
|
|
215
|
+
padding=(0, 1),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Default: plain text
|
|
219
|
+
display = result[:500]
|
|
220
|
+
if len(result) > 500:
|
|
221
|
+
display += "..."
|
|
222
|
+
return Panel(Text(display, style="ms.muted"), border_style="ms.dim", padding=(0, 1))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _make_inline_diff(old_string: str, new_string: str, path: str = "") -> str:
|
|
226
|
+
"""Create a minimal inline diff for display."""
|
|
227
|
+
lines = []
|
|
228
|
+
lines.append(f"--- a/{path}")
|
|
229
|
+
lines.append(f"+++ b/{path}")
|
|
230
|
+
lines.append("@@ @@")
|
|
231
|
+
for line in old_string.splitlines():
|
|
232
|
+
lines.append(f"- {line}")
|
|
233
|
+
for line in new_string.splitlines():
|
|
234
|
+
lines.append(f"+ {line}")
|
|
235
|
+
return "\n".join(lines)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ─── Prompt Rendering ──────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def render_prompt() -> str:
|
|
241
|
+
"""Return the styled prompt string for input."""
|
|
242
|
+
return "[ms.prompt]❯[/ms.prompt] "
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def render_user_message(text: str):
|
|
246
|
+
"""Display a user message in the conversation."""
|
|
247
|
+
console.print()
|
|
248
|
+
msg = Text()
|
|
249
|
+
msg.append("❯ ", style="ms.prompt")
|
|
250
|
+
msg.append(text, style="ms.user")
|
|
251
|
+
console.print(msg)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def render_assistant_response(text: str):
|
|
255
|
+
"""Display the assistant's final response."""
|
|
256
|
+
console.print()
|
|
257
|
+
console.print(Markdown(text))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def render_cost_footer(tokens: int, cost: float):
|
|
261
|
+
"""Show a compact cost footer after a response."""
|
|
262
|
+
footer = Text()
|
|
263
|
+
footer.append(f" {tokens:,} tokens", style="ms.dim")
|
|
264
|
+
footer.append(f" ${cost:.4f}", style="ms.cost")
|
|
265
|
+
console.print(footer)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ─── Session Header ───────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
def render_session_header(
|
|
271
|
+
model: str,
|
|
272
|
+
tier: str,
|
|
273
|
+
credits: float,
|
|
274
|
+
project: str,
|
|
275
|
+
agent_mode: bool,
|
|
276
|
+
instructions_loaded: bool = False,
|
|
277
|
+
memories_loaded: bool = False,
|
|
278
|
+
):
|
|
279
|
+
"""Render the session startup header."""
|
|
280
|
+
console.print()
|
|
281
|
+
|
|
282
|
+
# Title
|
|
283
|
+
title = Text()
|
|
284
|
+
title.append(" ◆ ", style="ms.accent")
|
|
285
|
+
title.append("MSapling", style="bold #ffffff")
|
|
286
|
+
title.append(" v0.1.0", style="ms.dim")
|
|
287
|
+
|
|
288
|
+
# Info lines
|
|
289
|
+
info = Text()
|
|
290
|
+
info.append(f"\n Model ", style="ms.dim")
|
|
291
|
+
info.append(model, style="ms.accent")
|
|
292
|
+
info.append(f"\n Project ", style="ms.dim")
|
|
293
|
+
info.append(project, style="ms.file")
|
|
294
|
+
info.append(f"\n Tier ", style="ms.dim")
|
|
295
|
+
info.append(tier, style="ms.accent2" if tier != "free" else "ms.warn")
|
|
296
|
+
info.append(f" • ${credits:.4f} fuel", style="ms.cost")
|
|
297
|
+
|
|
298
|
+
mode_text = "Agent mode" if agent_mode else "Simple mode"
|
|
299
|
+
mode_style = "ms.accent2" if agent_mode else "ms.warn"
|
|
300
|
+
info.append(f"\n Mode ", style="ms.dim")
|
|
301
|
+
info.append(mode_text, style=mode_style)
|
|
302
|
+
if agent_mode:
|
|
303
|
+
info.append(" (AI reads files, writes code, runs commands)", style="ms.dim")
|
|
304
|
+
|
|
305
|
+
# Context loaded
|
|
306
|
+
if instructions_loaded or memories_loaded:
|
|
307
|
+
info.append(f"\n Context ", style="ms.dim")
|
|
308
|
+
parts = []
|
|
309
|
+
if instructions_loaded:
|
|
310
|
+
parts.append("project instructions")
|
|
311
|
+
if memories_loaded:
|
|
312
|
+
parts.append("memories")
|
|
313
|
+
info.append(", ".join(parts), style="ms.dim")
|
|
314
|
+
|
|
315
|
+
console.print(Panel(
|
|
316
|
+
Group(title, info),
|
|
317
|
+
border_style="ms.accent",
|
|
318
|
+
padding=(0, 1),
|
|
319
|
+
))
|
|
320
|
+
|
|
321
|
+
# Hint line
|
|
322
|
+
hint = Text()
|
|
323
|
+
hint.append(" /help", style="ms.accent")
|
|
324
|
+
hint.append(" commands ", style="ms.dim")
|
|
325
|
+
hint.append("/simple", style="ms.accent")
|
|
326
|
+
hint.append(" chat-only ", style="ms.dim")
|
|
327
|
+
hint.append("Ctrl+C", style="ms.accent")
|
|
328
|
+
hint.append(" exit", style="ms.dim")
|
|
329
|
+
console.print(hint)
|
|
330
|
+
console.print()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ─── Approval Prompt ──────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
def render_approval_prompt(tool_name: str, args: Dict[str, Any]) -> str:
|
|
336
|
+
"""Show a styled approval prompt. Returns 'y', 'n', or 'always'."""
|
|
337
|
+
icons = {
|
|
338
|
+
"write_file": "📝",
|
|
339
|
+
"edit_file": "✏️",
|
|
340
|
+
"run_command": "⚡",
|
|
341
|
+
}
|
|
342
|
+
icon = icons.get(tool_name, "🔧")
|
|
343
|
+
|
|
344
|
+
console.print()
|
|
345
|
+
if tool_name == "write_file":
|
|
346
|
+
path = args.get("path", "?")
|
|
347
|
+
content = args.get("content", "")
|
|
348
|
+
lines = content.split("\n")
|
|
349
|
+
preview = "\n".join(lines[:10])
|
|
350
|
+
if len(lines) > 10:
|
|
351
|
+
preview += f"\n... ({len(lines) - 10} more lines)"
|
|
352
|
+
console.print(Panel(
|
|
353
|
+
Group(
|
|
354
|
+
Text(f"Path: {path}", style="ms.file"),
|
|
355
|
+
Text(),
|
|
356
|
+
Syntax(preview, _lang_from_path(path), theme="monokai"),
|
|
357
|
+
),
|
|
358
|
+
title=f"[ms.warn]{icon} write_file[/ms.warn]",
|
|
359
|
+
border_style="ms.warn",
|
|
360
|
+
padding=(0, 1),
|
|
361
|
+
))
|
|
362
|
+
elif tool_name == "edit_file":
|
|
363
|
+
path = args.get("path", "?")
|
|
364
|
+
old_s = args.get("old_string", "")
|
|
365
|
+
new_s = args.get("new_string", "")
|
|
366
|
+
diff = _make_inline_diff(old_s, new_s, path)
|
|
367
|
+
console.print(Panel(
|
|
368
|
+
Syntax(diff, "diff", theme="monokai"),
|
|
369
|
+
title=f"[ms.warn]{icon} edit_file → {path}[/ms.warn]",
|
|
370
|
+
border_style="ms.warn",
|
|
371
|
+
padding=(0, 1),
|
|
372
|
+
))
|
|
373
|
+
elif tool_name == "run_command":
|
|
374
|
+
cmd = args.get("command", "")
|
|
375
|
+
console.print(Panel(
|
|
376
|
+
Text(f"$ {cmd}", style="bold"),
|
|
377
|
+
title=f"[ms.warn]{icon} run_command[/ms.warn]",
|
|
378
|
+
border_style="ms.warn",
|
|
379
|
+
padding=(0, 1),
|
|
380
|
+
))
|
|
381
|
+
else:
|
|
382
|
+
console.print(Panel(
|
|
383
|
+
Text(f"{tool_name}: {args}", style="ms.muted"),
|
|
384
|
+
title=f"[ms.warn]{icon} {tool_name}[/ms.warn]",
|
|
385
|
+
border_style="ms.warn",
|
|
386
|
+
))
|
|
387
|
+
|
|
388
|
+
while True:
|
|
389
|
+
answer = console.input("[ms.warn] Allow? [/ms.warn][ms.dim](y/n/always)[/ms.dim] ").strip().lower()
|
|
390
|
+
if answer in ("y", "yes"):
|
|
391
|
+
return "y"
|
|
392
|
+
if answer in ("n", "no"):
|
|
393
|
+
return "n"
|
|
394
|
+
if answer in ("always", "a"):
|
|
395
|
+
return "always"
|
|
396
|
+
console.print(" [ms.dim]Enter y, n, or always[/ms.dim]")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _lang_from_path(path: str) -> str:
|
|
400
|
+
ext = path.rsplit(".", 1)[-1] if "." in path else ""
|
|
401
|
+
return {"py": "python", "ts": "typescript", "js": "javascript", "tsx": "tsx", "rs": "rust", "go": "go", "rb": "ruby", "java": "java", "md": "markdown"}.get(ext, "text")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ─── Slash Command Help ───────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
def render_help():
|
|
407
|
+
"""Render styled help panel."""
|
|
408
|
+
sections = [
|
|
409
|
+
("Session", [
|
|
410
|
+
("/help", "Show this help"),
|
|
411
|
+
("/clear", "Clear conversation"),
|
|
412
|
+
("/compact", "Compress context (LLM-summarized)"),
|
|
413
|
+
("/context", "Show context usage"),
|
|
414
|
+
("/save", "Save session"),
|
|
415
|
+
("/resume", "Resume previous session"),
|
|
416
|
+
("/fork", "Fork conversation"),
|
|
417
|
+
("/rewind [n]", "Undo last n messages"),
|
|
418
|
+
("/quit", "Exit"),
|
|
419
|
+
]),
|
|
420
|
+
("Model & Mode", [
|
|
421
|
+
("/model <id>", "Switch model"),
|
|
422
|
+
("/models", "List available models"),
|
|
423
|
+
("/agent", "Agent mode (AI uses tools)"),
|
|
424
|
+
("/simple", "Simple chat (no tools)"),
|
|
425
|
+
("/plan", "Plan mode (suggest only)"),
|
|
426
|
+
("/vim", "Multiline input"),
|
|
427
|
+
("/effort <lvl>", "low / medium / high"),
|
|
428
|
+
]),
|
|
429
|
+
("Files", [
|
|
430
|
+
("/files [glob]", "List files"),
|
|
431
|
+
("/read <path>", "Read file into context"),
|
|
432
|
+
("/write <path>", "Write file"),
|
|
433
|
+
("/edit <path>", "AI-edit file"),
|
|
434
|
+
("/grep <pattern>", "Search file contents"),
|
|
435
|
+
("/mention <path>", "Attach to next message"),
|
|
436
|
+
("/image <path>", "Attach image"),
|
|
437
|
+
]),
|
|
438
|
+
("Terminal & Git", [
|
|
439
|
+
("/run <cmd>", "Execute command"),
|
|
440
|
+
("/git <args>", "Git command"),
|
|
441
|
+
("/diff", "Git diff"),
|
|
442
|
+
("/status", "Git status"),
|
|
443
|
+
("/worktree", "Git worktree management"),
|
|
444
|
+
]),
|
|
445
|
+
("Multi-Model (Pro)", [
|
|
446
|
+
("/multi <prompt>", "Parallel comparison"),
|
|
447
|
+
("/swarm <prompt>", "Parallel + synthesis"),
|
|
448
|
+
("/agent spawn", "Background subagent"),
|
|
449
|
+
]),
|
|
450
|
+
("Workers (Multi-Chat)", [
|
|
451
|
+
("/workers", "List parallel workers"),
|
|
452
|
+
("/join <name|#>", "Switch to worker"),
|
|
453
|
+
("/broadcast <msg>", "Message all workers"),
|
|
454
|
+
("/collect", "Show worker outputs"),
|
|
455
|
+
("/kill <name|#>", "Remove a worker"),
|
|
456
|
+
("/budget [amt]", "Set cost ceiling"),
|
|
457
|
+
]),
|
|
458
|
+
("System", [
|
|
459
|
+
("/project", "Project info"),
|
|
460
|
+
("/init", "Generate .msapling.md"),
|
|
461
|
+
("/cost", "Session cost"),
|
|
462
|
+
("/memory", "Persistent memory"),
|
|
463
|
+
("/hooks", "Lifecycle hooks"),
|
|
464
|
+
("/mdrive", "Cloud file sync"),
|
|
465
|
+
("/whoami", "Account info"),
|
|
466
|
+
]),
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
for section_name, commands in sections:
|
|
470
|
+
table = Table(show_header=False, border_style="ms.dim", padding=(0, 1), expand=True)
|
|
471
|
+
table.add_column("Command", style="ms.accent", min_width=20)
|
|
472
|
+
table.add_column("Description", style="ms.muted")
|
|
473
|
+
for cmd, desc in commands:
|
|
474
|
+
table.add_row(cmd, desc)
|
|
475
|
+
console.print(Panel(table, title=f"[ms.accent]{section_name}[/ms.accent]", border_style="ms.dim", padding=(0, 0)))
|