emdash-cli 0.1.35__py3-none-any.whl → 0.1.67__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.
- emdash_cli/client.py +41 -22
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +63 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +51 -0
- emdash_cli/commands/agent/handlers/agents.py +449 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +319 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +411 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +715 -0
- emdash_cli/commands/agent/handlers/skills.py +478 -0
- emdash_cli/commands/agent/handlers/telegram.py +475 -0
- emdash_cli/commands/agent/handlers/todos.py +119 -0
- emdash_cli/commands/agent/handlers/verify.py +653 -0
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +842 -0
- emdash_cli/commands/agent/menus.py +760 -0
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/agent.py +7 -1321
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- emdash_cli/commands/server.py +99 -40
- emdash_cli/commands/skills.py +72 -6
- emdash_cli/design.py +328 -0
- emdash_cli/diff_renderer.py +438 -0
- emdash_cli/integrations/__init__.py +1 -0
- emdash_cli/integrations/telegram/__init__.py +15 -0
- emdash_cli/integrations/telegram/bot.py +402 -0
- emdash_cli/integrations/telegram/bridge.py +865 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +385 -0
- emdash_cli/main.py +52 -2
- emdash_cli/server_manager.py +70 -10
- emdash_cli/sse_renderer.py +659 -167
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
- emdash_cli-0.1.67.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.35.dist-info/RECORD +0 -30
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Handler for /rules command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
|
|
8
|
+
from ....design import (
|
|
9
|
+
Colors,
|
|
10
|
+
header,
|
|
11
|
+
footer,
|
|
12
|
+
SEPARATOR_WIDTH,
|
|
13
|
+
STATUS_ACTIVE,
|
|
14
|
+
ARROW_PROMPT,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_rules_dir() -> Path:
|
|
21
|
+
"""Get the rules directory path."""
|
|
22
|
+
return Path.cwd() / ".emdash" / "rules"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_rules() -> list[dict]:
|
|
26
|
+
"""List all rules files.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of dicts with name, file_path, and preview
|
|
30
|
+
"""
|
|
31
|
+
rules_dir = get_rules_dir()
|
|
32
|
+
rules = []
|
|
33
|
+
|
|
34
|
+
if not rules_dir.exists():
|
|
35
|
+
return rules
|
|
36
|
+
|
|
37
|
+
for md_file in sorted(rules_dir.glob("*.md")):
|
|
38
|
+
try:
|
|
39
|
+
content = md_file.read_text().strip()
|
|
40
|
+
# Get first non-empty line as preview
|
|
41
|
+
lines = [l for l in content.split("\n") if l.strip()]
|
|
42
|
+
preview = lines[0][:60] + "..." if lines and len(lines[0]) > 60 else (lines[0] if lines else "")
|
|
43
|
+
# Remove markdown heading prefix
|
|
44
|
+
if preview.startswith("#"):
|
|
45
|
+
preview = preview.lstrip("#").strip()
|
|
46
|
+
|
|
47
|
+
rules.append({
|
|
48
|
+
"name": md_file.stem,
|
|
49
|
+
"file_path": str(md_file),
|
|
50
|
+
"preview": preview,
|
|
51
|
+
})
|
|
52
|
+
except Exception:
|
|
53
|
+
rules.append({
|
|
54
|
+
"name": md_file.stem,
|
|
55
|
+
"file_path": str(md_file),
|
|
56
|
+
"preview": "(error reading file)",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return rules
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def show_rules_interactive_menu() -> tuple[str, str]:
|
|
63
|
+
"""Show interactive rules menu.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple of (action, rule_name) where action is one of:
|
|
67
|
+
- 'view': View rule details
|
|
68
|
+
- 'create': Create new rule
|
|
69
|
+
- 'delete': Delete rule
|
|
70
|
+
- 'cancel': User cancelled
|
|
71
|
+
"""
|
|
72
|
+
from prompt_toolkit import Application
|
|
73
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
74
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
75
|
+
from prompt_toolkit.styles import Style
|
|
76
|
+
|
|
77
|
+
rules = list_rules()
|
|
78
|
+
|
|
79
|
+
# Build menu items: (name, preview, is_action)
|
|
80
|
+
menu_items = []
|
|
81
|
+
|
|
82
|
+
for rule in rules:
|
|
83
|
+
menu_items.append((rule["name"], rule["preview"], False))
|
|
84
|
+
|
|
85
|
+
# Add action items at the bottom
|
|
86
|
+
menu_items.append(("+ Create New Rule", "Create a new rule with AI assistance", True))
|
|
87
|
+
|
|
88
|
+
if not menu_items:
|
|
89
|
+
menu_items.append(("+ Create New Rule", "Create a new rule with AI assistance", True))
|
|
90
|
+
|
|
91
|
+
selected_index = [0]
|
|
92
|
+
result = [("cancel", "")]
|
|
93
|
+
|
|
94
|
+
kb = KeyBindings()
|
|
95
|
+
|
|
96
|
+
@kb.add("up")
|
|
97
|
+
@kb.add("k")
|
|
98
|
+
def move_up(event):
|
|
99
|
+
selected_index[0] = (selected_index[0] - 1) % len(menu_items)
|
|
100
|
+
|
|
101
|
+
@kb.add("down")
|
|
102
|
+
@kb.add("j")
|
|
103
|
+
def move_down(event):
|
|
104
|
+
selected_index[0] = (selected_index[0] + 1) % len(menu_items)
|
|
105
|
+
|
|
106
|
+
@kb.add("enter")
|
|
107
|
+
def select(event):
|
|
108
|
+
item = menu_items[selected_index[0]]
|
|
109
|
+
name, preview, is_action = item
|
|
110
|
+
if is_action:
|
|
111
|
+
if "Create" in name:
|
|
112
|
+
result[0] = ("create", "")
|
|
113
|
+
else:
|
|
114
|
+
result[0] = ("view", name)
|
|
115
|
+
event.app.exit()
|
|
116
|
+
|
|
117
|
+
@kb.add("d")
|
|
118
|
+
def delete_rule(event):
|
|
119
|
+
item = menu_items[selected_index[0]]
|
|
120
|
+
name, preview, is_action = item
|
|
121
|
+
if not is_action:
|
|
122
|
+
result[0] = ("delete", name)
|
|
123
|
+
event.app.exit()
|
|
124
|
+
|
|
125
|
+
@kb.add("n")
|
|
126
|
+
def new_rule(event):
|
|
127
|
+
result[0] = ("create", "")
|
|
128
|
+
event.app.exit()
|
|
129
|
+
|
|
130
|
+
@kb.add("c-c")
|
|
131
|
+
@kb.add("escape")
|
|
132
|
+
@kb.add("q")
|
|
133
|
+
def cancel(event):
|
|
134
|
+
result[0] = ("cancel", "")
|
|
135
|
+
event.app.exit()
|
|
136
|
+
|
|
137
|
+
def get_formatted_menu():
|
|
138
|
+
lines = [("class:title", f"─── Rules {'─' * 35}\n\n")]
|
|
139
|
+
|
|
140
|
+
if not rules:
|
|
141
|
+
lines.append(("class:dim", " No rules defined yet.\n\n"))
|
|
142
|
+
|
|
143
|
+
for i, (name, preview, is_action) in enumerate(menu_items):
|
|
144
|
+
is_selected = i == selected_index[0]
|
|
145
|
+
prefix = "▸ " if is_selected else " "
|
|
146
|
+
|
|
147
|
+
if is_action:
|
|
148
|
+
if is_selected:
|
|
149
|
+
lines.append(("class:action-selected", f" {prefix}{name}\n"))
|
|
150
|
+
else:
|
|
151
|
+
lines.append(("class:action", f" {prefix}{name}\n"))
|
|
152
|
+
else:
|
|
153
|
+
if is_selected:
|
|
154
|
+
lines.append(("class:rule-selected", f" {prefix}{name}"))
|
|
155
|
+
lines.append(("class:preview-selected", f" {preview}\n"))
|
|
156
|
+
else:
|
|
157
|
+
lines.append(("class:rule", f" {prefix}{name}"))
|
|
158
|
+
lines.append(("class:preview", f" {preview}\n"))
|
|
159
|
+
|
|
160
|
+
lines.append(("class:hint", f"\n{'─' * 45}\n ↑↓ navigate Enter view n new d delete q quit"))
|
|
161
|
+
return lines
|
|
162
|
+
|
|
163
|
+
style = Style.from_dict({
|
|
164
|
+
"title": f"{Colors.MUTED}",
|
|
165
|
+
"dim": f"{Colors.DIM}",
|
|
166
|
+
"rule": f"{Colors.PRIMARY}",
|
|
167
|
+
"rule-selected": f"{Colors.SUCCESS} bold",
|
|
168
|
+
"action": f"{Colors.WARNING}",
|
|
169
|
+
"action-selected": f"{Colors.WARNING} bold",
|
|
170
|
+
"preview": f"{Colors.DIM}",
|
|
171
|
+
"preview-selected": f"{Colors.SUCCESS}",
|
|
172
|
+
"hint": f"{Colors.DIM}",
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
height = len(menu_items) + 5 # items + title + hint + padding
|
|
176
|
+
|
|
177
|
+
layout = Layout(
|
|
178
|
+
HSplit([
|
|
179
|
+
Window(
|
|
180
|
+
FormattedTextControl(get_formatted_menu),
|
|
181
|
+
height=height,
|
|
182
|
+
),
|
|
183
|
+
])
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
app = Application(
|
|
187
|
+
layout=layout,
|
|
188
|
+
key_bindings=kb,
|
|
189
|
+
style=style,
|
|
190
|
+
full_screen=False,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
console.print()
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
app.run()
|
|
197
|
+
except (KeyboardInterrupt, EOFError):
|
|
198
|
+
result[0] = ("cancel", "")
|
|
199
|
+
|
|
200
|
+
# Clear menu visually with separator
|
|
201
|
+
console.print()
|
|
202
|
+
|
|
203
|
+
return result[0]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def show_rule_details(name: str) -> None:
|
|
207
|
+
"""Show detailed view of a rule."""
|
|
208
|
+
rules_dir = get_rules_dir()
|
|
209
|
+
rule_file = rules_dir / f"{name}.md"
|
|
210
|
+
|
|
211
|
+
console.print()
|
|
212
|
+
console.print(f"[{Colors.MUTED}]{header(name, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
213
|
+
console.print()
|
|
214
|
+
|
|
215
|
+
if rule_file.exists():
|
|
216
|
+
try:
|
|
217
|
+
content = rule_file.read_text()
|
|
218
|
+
console.print(f" [{Colors.DIM}]file[/{Colors.DIM}] {rule_file}")
|
|
219
|
+
console.print()
|
|
220
|
+
|
|
221
|
+
# Show content with indentation
|
|
222
|
+
for line in content.split('\n'):
|
|
223
|
+
if line.startswith('#'):
|
|
224
|
+
console.print(f" [{Colors.PRIMARY}]{line}[/{Colors.PRIMARY}]")
|
|
225
|
+
else:
|
|
226
|
+
console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
console.print(f" [{Colors.ERROR}]Error reading rule: {e}[/{Colors.ERROR}]")
|
|
230
|
+
else:
|
|
231
|
+
console.print(f" [{Colors.WARNING}]Rule '{name}' not found[/{Colors.WARNING}]")
|
|
232
|
+
|
|
233
|
+
console.print()
|
|
234
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def confirm_delete(rule_name: str) -> bool:
|
|
238
|
+
"""Confirm rule deletion."""
|
|
239
|
+
from prompt_toolkit import PromptSession
|
|
240
|
+
|
|
241
|
+
console.print()
|
|
242
|
+
console.print(f"[yellow]Delete rule '{rule_name}'?[/yellow]")
|
|
243
|
+
console.print("[dim]This will remove the rule file. Type 'yes' to confirm.[/dim]")
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
session = PromptSession()
|
|
247
|
+
response = session.prompt("Confirm > ").strip().lower()
|
|
248
|
+
return response in ("yes", "y")
|
|
249
|
+
except (KeyboardInterrupt, EOFError):
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def delete_rule(name: str) -> bool:
|
|
254
|
+
"""Delete a rule file."""
|
|
255
|
+
rules_dir = get_rules_dir()
|
|
256
|
+
rule_file = rules_dir / f"{name}.md"
|
|
257
|
+
|
|
258
|
+
if not rule_file.exists():
|
|
259
|
+
console.print(f"[yellow]Rule file not found: {rule_file}[/yellow]")
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
if confirm_delete(name):
|
|
263
|
+
rule_file.unlink()
|
|
264
|
+
console.print(f"[green]Deleted rule: {name}[/green]")
|
|
265
|
+
return True
|
|
266
|
+
else:
|
|
267
|
+
console.print("[dim]Cancelled[/dim]")
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def chat_create_rule(client, renderer, model, max_iterations, render_with_interrupt) -> None:
|
|
272
|
+
"""Start a chat session to create a new rule with AI assistance."""
|
|
273
|
+
from prompt_toolkit import PromptSession
|
|
274
|
+
from prompt_toolkit.styles import Style
|
|
275
|
+
|
|
276
|
+
rules_dir = get_rules_dir()
|
|
277
|
+
|
|
278
|
+
console.print()
|
|
279
|
+
console.print(f"[{Colors.MUTED}]{header('Create Rule', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
280
|
+
console.print()
|
|
281
|
+
console.print(f" [{Colors.DIM}]Describe your rule. AI will help write it.[/{Colors.DIM}]")
|
|
282
|
+
console.print(f" [{Colors.DIM}]Type 'done' to finish.[/{Colors.DIM}]")
|
|
283
|
+
console.print()
|
|
284
|
+
|
|
285
|
+
chat_style = Style.from_dict({
|
|
286
|
+
"prompt": f"{Colors.PRIMARY} bold",
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
ps = PromptSession(style=chat_style)
|
|
290
|
+
chat_session_id = None
|
|
291
|
+
first_message = True
|
|
292
|
+
|
|
293
|
+
# Ensure rules directory exists
|
|
294
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
295
|
+
|
|
296
|
+
# Chat loop
|
|
297
|
+
while True:
|
|
298
|
+
try:
|
|
299
|
+
user_input = ps.prompt([("class:prompt", "› ")]).strip()
|
|
300
|
+
|
|
301
|
+
if not user_input:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
if user_input.lower() in ("done", "quit", "exit", "q"):
|
|
305
|
+
console.print("[dim]Finished[/dim]")
|
|
306
|
+
break
|
|
307
|
+
|
|
308
|
+
# First message includes context about rules
|
|
309
|
+
if first_message:
|
|
310
|
+
message_with_context = f"""I want to create a new rule file for my project.
|
|
311
|
+
|
|
312
|
+
**Rules directory:** `{rules_dir}`
|
|
313
|
+
|
|
314
|
+
Rules are markdown files that define guidelines for the AI agent. They are stored in `.emdash/rules/` and get injected into the agent's system prompt.
|
|
315
|
+
|
|
316
|
+
Example rule file:
|
|
317
|
+
```markdown
|
|
318
|
+
# Code Style Guidelines
|
|
319
|
+
|
|
320
|
+
- Use meaningful variable names
|
|
321
|
+
- Keep functions small and focused
|
|
322
|
+
- Add comments for complex logic
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**My request:** {user_input}
|
|
326
|
+
|
|
327
|
+
Please help me create a rule file. Ask me questions if needed to understand what rules I want, then use the Write tool to create the file at `{rules_dir}/<rule-name>.md`."""
|
|
328
|
+
stream = client.agent_chat_stream(
|
|
329
|
+
message=message_with_context,
|
|
330
|
+
model=model,
|
|
331
|
+
max_iterations=max_iterations,
|
|
332
|
+
options={"mode": "code"},
|
|
333
|
+
)
|
|
334
|
+
first_message = False
|
|
335
|
+
elif chat_session_id:
|
|
336
|
+
stream = client.agent_continue_stream(
|
|
337
|
+
chat_session_id, user_input
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
stream = client.agent_chat_stream(
|
|
341
|
+
message=user_input,
|
|
342
|
+
model=model,
|
|
343
|
+
max_iterations=max_iterations,
|
|
344
|
+
options={"mode": "code"},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
result = render_with_interrupt(renderer, stream)
|
|
348
|
+
if result and result.get("session_id"):
|
|
349
|
+
chat_session_id = result["session_id"]
|
|
350
|
+
|
|
351
|
+
except (KeyboardInterrupt, EOFError):
|
|
352
|
+
console.print()
|
|
353
|
+
console.print("[dim]Cancelled[/dim]")
|
|
354
|
+
break
|
|
355
|
+
except Exception as e:
|
|
356
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def handle_rules(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
|
|
360
|
+
"""Handle /rules command."""
|
|
361
|
+
from prompt_toolkit import PromptSession
|
|
362
|
+
|
|
363
|
+
# Handle subcommands
|
|
364
|
+
if args:
|
|
365
|
+
subparts = args.split(maxsplit=1)
|
|
366
|
+
subcommand = subparts[0].lower()
|
|
367
|
+
subargs = subparts[1] if len(subparts) > 1 else ""
|
|
368
|
+
|
|
369
|
+
if subcommand == "list":
|
|
370
|
+
rules = list_rules()
|
|
371
|
+
if rules:
|
|
372
|
+
console.print("\n[bold cyan]Rules[/bold cyan]\n")
|
|
373
|
+
for rule in rules:
|
|
374
|
+
console.print(f" [cyan]{rule['name']}[/cyan] - {rule['preview']}")
|
|
375
|
+
console.print()
|
|
376
|
+
else:
|
|
377
|
+
console.print("\n[dim]No rules defined yet.[/dim]")
|
|
378
|
+
console.print(f"[dim]Rules directory: {get_rules_dir()}[/dim]\n")
|
|
379
|
+
elif subcommand == "show" and subargs:
|
|
380
|
+
show_rule_details(subargs.strip())
|
|
381
|
+
elif subcommand == "delete" and subargs:
|
|
382
|
+
delete_rule(subargs.strip())
|
|
383
|
+
elif subcommand == "add" or subcommand == "create" or subcommand == "new":
|
|
384
|
+
chat_create_rule(client, renderer, model, max_iterations, render_with_interrupt)
|
|
385
|
+
else:
|
|
386
|
+
console.print("[yellow]Usage: /rules [list|show|add|delete] [name][/yellow]")
|
|
387
|
+
console.print("[dim]Or just /rules for interactive menu[/dim]")
|
|
388
|
+
else:
|
|
389
|
+
# Interactive menu
|
|
390
|
+
while True:
|
|
391
|
+
action, rule_name = show_rules_interactive_menu()
|
|
392
|
+
|
|
393
|
+
if action == "cancel":
|
|
394
|
+
break
|
|
395
|
+
elif action == "view":
|
|
396
|
+
show_rule_details(rule_name)
|
|
397
|
+
# After viewing, show options
|
|
398
|
+
try:
|
|
399
|
+
console.print("[red]'d'[/red] delete • [dim]Enter back[/dim]", end="")
|
|
400
|
+
ps = PromptSession()
|
|
401
|
+
resp = ps.prompt(" ").strip().lower()
|
|
402
|
+
if resp == 'd':
|
|
403
|
+
delete_rule(rule_name)
|
|
404
|
+
console.print()
|
|
405
|
+
except (KeyboardInterrupt, EOFError):
|
|
406
|
+
break
|
|
407
|
+
elif action == "create":
|
|
408
|
+
chat_create_rule(client, renderer, model, max_iterations, render_with_interrupt)
|
|
409
|
+
# Refresh menu after creating
|
|
410
|
+
elif action == "delete":
|
|
411
|
+
delete_rule(rule_name)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Handler for /session command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from ..menus import show_sessions_interactive_menu, confirm_session_delete
|
|
8
|
+
from ..constants import AgentMode
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_session(
|
|
14
|
+
args: str,
|
|
15
|
+
client,
|
|
16
|
+
model: str | None,
|
|
17
|
+
session_id_ref: list,
|
|
18
|
+
current_spec_ref: list,
|
|
19
|
+
current_mode_ref: list,
|
|
20
|
+
loaded_messages_ref: list,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Handle /session command.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
args: Command arguments
|
|
26
|
+
client: EmdashClient instance
|
|
27
|
+
model: Current model
|
|
28
|
+
session_id_ref: Reference to session_id (list wrapper for mutation)
|
|
29
|
+
current_spec_ref: Reference to current_spec (list wrapper for mutation)
|
|
30
|
+
current_mode_ref: Reference to current_mode (list wrapper for mutation)
|
|
31
|
+
loaded_messages_ref: Reference to loaded_messages (list wrapper for mutation)
|
|
32
|
+
"""
|
|
33
|
+
from ....session_store import SessionStore
|
|
34
|
+
|
|
35
|
+
store = SessionStore(Path.cwd())
|
|
36
|
+
|
|
37
|
+
# Parse subcommand
|
|
38
|
+
subparts = args.split(maxsplit=1) if args else []
|
|
39
|
+
subcommand = subparts[0].lower() if subparts else ""
|
|
40
|
+
subargs = subparts[1].strip() if len(subparts) > 1 else ""
|
|
41
|
+
|
|
42
|
+
def _load_session(name: str) -> bool:
|
|
43
|
+
"""Load a session by name. Returns True if successful."""
|
|
44
|
+
session_data = store.load_session(name)
|
|
45
|
+
if session_data:
|
|
46
|
+
session_id_ref[0] = None
|
|
47
|
+
current_spec_ref[0] = session_data.spec
|
|
48
|
+
if session_data.mode == "plan":
|
|
49
|
+
current_mode_ref[0] = AgentMode.PLAN
|
|
50
|
+
else:
|
|
51
|
+
current_mode_ref[0] = AgentMode.CODE
|
|
52
|
+
loaded_messages_ref[0] = session_data.messages
|
|
53
|
+
store.set_active_session(name)
|
|
54
|
+
console.print(f"[green]Loaded session '{name}'[/green]")
|
|
55
|
+
console.print(f"[dim]{len(session_data.messages)} messages restored, mode: {current_mode_ref[0].value}[/dim]")
|
|
56
|
+
if current_spec_ref[0]:
|
|
57
|
+
console.print("[dim]Spec restored[/dim]")
|
|
58
|
+
return True
|
|
59
|
+
else:
|
|
60
|
+
console.print(f"[yellow]Session '{name}' not found[/yellow]")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def _delete_session(name: str) -> bool:
|
|
64
|
+
"""Delete a session by name with confirmation."""
|
|
65
|
+
if confirm_session_delete(name):
|
|
66
|
+
success, msg = store.delete_session(name)
|
|
67
|
+
if success:
|
|
68
|
+
console.print(f"[green]{msg}[/green]")
|
|
69
|
+
return True
|
|
70
|
+
else:
|
|
71
|
+
console.print(f"[yellow]{msg}[/yellow]")
|
|
72
|
+
else:
|
|
73
|
+
console.print("[dim]Cancelled[/dim]")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
if subcommand == "" or subcommand == "list":
|
|
77
|
+
# Interactive menu (or list if no sessions)
|
|
78
|
+
sessions = store.list_sessions()
|
|
79
|
+
if sessions:
|
|
80
|
+
if subcommand == "list":
|
|
81
|
+
# Just list, don't show interactive menu
|
|
82
|
+
console.print("\n[bold cyan]Saved Sessions[/bold cyan]\n")
|
|
83
|
+
for s in sessions:
|
|
84
|
+
mode_color = "green" if s.mode == "code" else "yellow"
|
|
85
|
+
active_marker = " [bold green]*[/bold green]" if store.get_active_session() == s.name else ""
|
|
86
|
+
console.print(f" [cyan]{s.name}[/cyan]{active_marker} [{mode_color}]{s.mode}[/{mode_color}]")
|
|
87
|
+
console.print(f" [dim]{s.message_count} messages | {s.updated_at[:10]}[/dim]")
|
|
88
|
+
if s.summary:
|
|
89
|
+
summary = s.summary[:60] + "..." if len(s.summary) > 60 else s.summary
|
|
90
|
+
console.print(f" [dim]{summary}[/dim]")
|
|
91
|
+
console.print()
|
|
92
|
+
else:
|
|
93
|
+
# Interactive menu
|
|
94
|
+
while True:
|
|
95
|
+
action, session_name = show_sessions_interactive_menu(
|
|
96
|
+
sessions, store.get_active_session()
|
|
97
|
+
)
|
|
98
|
+
if action == "cancel":
|
|
99
|
+
break
|
|
100
|
+
elif action == "load":
|
|
101
|
+
_load_session(session_name)
|
|
102
|
+
break
|
|
103
|
+
elif action == "delete":
|
|
104
|
+
if _delete_session(session_name):
|
|
105
|
+
# Refresh sessions list
|
|
106
|
+
sessions = store.list_sessions()
|
|
107
|
+
if not sessions:
|
|
108
|
+
console.print("\n[dim]No more sessions.[/dim]\n")
|
|
109
|
+
break
|
|
110
|
+
# Continue showing menu
|
|
111
|
+
else:
|
|
112
|
+
console.print("\n[dim]No saved sessions.[/dim]")
|
|
113
|
+
console.print("[dim]Save with: /session save <name>[/dim]\n")
|
|
114
|
+
|
|
115
|
+
elif subcommand == "save":
|
|
116
|
+
if not subargs:
|
|
117
|
+
console.print("[yellow]Usage: /session save <name>[/yellow]")
|
|
118
|
+
console.print("[dim]Example: /session save auth-feature[/dim]")
|
|
119
|
+
else:
|
|
120
|
+
# Get current messages from the API session
|
|
121
|
+
if session_id_ref[0]:
|
|
122
|
+
try:
|
|
123
|
+
export_resp = client.get(f"/api/agent/chat/{session_id_ref[0]}/export")
|
|
124
|
+
if export_resp.status_code == 200:
|
|
125
|
+
data = export_resp.json()
|
|
126
|
+
messages = data.get("messages", [])
|
|
127
|
+
else:
|
|
128
|
+
messages = []
|
|
129
|
+
except Exception:
|
|
130
|
+
messages = []
|
|
131
|
+
else:
|
|
132
|
+
messages = []
|
|
133
|
+
|
|
134
|
+
success, msg = store.save_session(
|
|
135
|
+
name=subargs,
|
|
136
|
+
messages=messages,
|
|
137
|
+
mode=current_mode_ref[0].value,
|
|
138
|
+
spec=current_spec_ref[0],
|
|
139
|
+
model=model,
|
|
140
|
+
)
|
|
141
|
+
if success:
|
|
142
|
+
store.set_active_session(subargs)
|
|
143
|
+
console.print(f"[green]{msg}[/green]")
|
|
144
|
+
else:
|
|
145
|
+
console.print(f"[yellow]{msg}[/yellow]")
|
|
146
|
+
|
|
147
|
+
elif subcommand == "load":
|
|
148
|
+
if not subargs:
|
|
149
|
+
console.print("[yellow]Usage: /session load <name>[/yellow]")
|
|
150
|
+
else:
|
|
151
|
+
_load_session(subargs)
|
|
152
|
+
|
|
153
|
+
elif subcommand == "delete":
|
|
154
|
+
if not subargs:
|
|
155
|
+
console.print("[yellow]Usage: /session delete <name>[/yellow]")
|
|
156
|
+
else:
|
|
157
|
+
_delete_session(subargs)
|
|
158
|
+
|
|
159
|
+
elif subcommand == "clear":
|
|
160
|
+
session_id_ref[0] = None
|
|
161
|
+
current_spec_ref[0] = None
|
|
162
|
+
loaded_messages_ref[0] = []
|
|
163
|
+
store.set_active_session(None)
|
|
164
|
+
console.print("[green]Session cleared[/green]")
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
|
|
168
|
+
console.print("[dim]Usage: /session [list|save|load|delete|clear] [name][/dim]")
|