emdash-cli 0.1.25__py3-none-any.whl → 0.1.35__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/__init__.py +15 -0
- emdash_cli/client.py +129 -0
- emdash_cli/clipboard.py +123 -0
- emdash_cli/commands/agent.py +526 -34
- emdash_cli/session_store.py +321 -0
- emdash_cli/sse_renderer.py +224 -119
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.dist-info}/METADATA +4 -2
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.dist-info}/RECORD +10 -8
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.25.dist-info → emdash_cli-0.1.35.dist-info}/entry_points.txt +0 -0
emdash_cli/commands/agent.py
CHANGED
|
@@ -35,7 +35,9 @@ SLASH_COMMANDS = {
|
|
|
35
35
|
"/research [goal]": "Deep research on a topic",
|
|
36
36
|
# Status commands
|
|
37
37
|
"/status": "Show index and PROJECT.md status",
|
|
38
|
+
"/agents": "List, create, or show agents (e.g., /agents create my-agent)",
|
|
38
39
|
# Session management
|
|
40
|
+
"/session": "Save, load, or list sessions (e.g., /session save my-task)",
|
|
39
41
|
"/spec": "Show current specification",
|
|
40
42
|
"/reset": "Reset session state",
|
|
41
43
|
"/save": "Save current spec to disk",
|
|
@@ -373,6 +375,121 @@ def _show_plan_approval_menu() -> tuple[str, str]:
|
|
|
373
375
|
return choice, feedback
|
|
374
376
|
|
|
375
377
|
|
|
378
|
+
def _show_plan_mode_approval_menu() -> tuple[str, str]:
|
|
379
|
+
"""Show plan mode entry approval menu.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Tuple of (choice, feedback) where feedback is only set for 'reject'
|
|
383
|
+
"""
|
|
384
|
+
from prompt_toolkit import Application
|
|
385
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
386
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
387
|
+
from prompt_toolkit.styles import Style
|
|
388
|
+
|
|
389
|
+
options = [
|
|
390
|
+
("approve", "Enter plan mode and explore"),
|
|
391
|
+
("reject", "Skip planning, proceed directly"),
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
selected_index = [0]
|
|
395
|
+
result = [None]
|
|
396
|
+
|
|
397
|
+
kb = KeyBindings()
|
|
398
|
+
|
|
399
|
+
@kb.add("up")
|
|
400
|
+
@kb.add("k")
|
|
401
|
+
def move_up(event):
|
|
402
|
+
selected_index[0] = (selected_index[0] - 1) % len(options)
|
|
403
|
+
|
|
404
|
+
@kb.add("down")
|
|
405
|
+
@kb.add("j")
|
|
406
|
+
def move_down(event):
|
|
407
|
+
selected_index[0] = (selected_index[0] + 1) % len(options)
|
|
408
|
+
|
|
409
|
+
@kb.add("enter")
|
|
410
|
+
def select(event):
|
|
411
|
+
result[0] = options[selected_index[0]][0]
|
|
412
|
+
event.app.exit()
|
|
413
|
+
|
|
414
|
+
@kb.add("1")
|
|
415
|
+
@kb.add("y")
|
|
416
|
+
def select_approve(event):
|
|
417
|
+
result[0] = "approve"
|
|
418
|
+
event.app.exit()
|
|
419
|
+
|
|
420
|
+
@kb.add("2")
|
|
421
|
+
@kb.add("n")
|
|
422
|
+
def select_reject(event):
|
|
423
|
+
result[0] = "reject"
|
|
424
|
+
event.app.exit()
|
|
425
|
+
|
|
426
|
+
@kb.add("c-c")
|
|
427
|
+
@kb.add("q")
|
|
428
|
+
@kb.add("escape")
|
|
429
|
+
def cancel(event):
|
|
430
|
+
result[0] = "reject"
|
|
431
|
+
event.app.exit()
|
|
432
|
+
|
|
433
|
+
def get_formatted_options():
|
|
434
|
+
lines = [("class:title", "Enter plan mode?\n\n")]
|
|
435
|
+
for i, (key, desc) in enumerate(options):
|
|
436
|
+
if i == selected_index[0]:
|
|
437
|
+
lines.append(("class:selected", f" ❯ {key:8} "))
|
|
438
|
+
lines.append(("class:selected-desc", f"- {desc}\n"))
|
|
439
|
+
else:
|
|
440
|
+
lines.append(("class:option", f" {key:8} "))
|
|
441
|
+
lines.append(("class:desc", f"- {desc}\n"))
|
|
442
|
+
lines.append(("class:hint", "\n↑/↓ to move, Enter to select, y/n for quick select"))
|
|
443
|
+
return lines
|
|
444
|
+
|
|
445
|
+
style = Style.from_dict({
|
|
446
|
+
"title": "#ffcc00 bold",
|
|
447
|
+
"selected": "#00cc66 bold",
|
|
448
|
+
"selected-desc": "#00cc66",
|
|
449
|
+
"option": "#888888",
|
|
450
|
+
"desc": "#666666",
|
|
451
|
+
"hint": "#444444 italic",
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
layout = Layout(
|
|
455
|
+
HSplit([
|
|
456
|
+
Window(
|
|
457
|
+
FormattedTextControl(get_formatted_options),
|
|
458
|
+
height=6,
|
|
459
|
+
),
|
|
460
|
+
])
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
app = Application(
|
|
464
|
+
layout=layout,
|
|
465
|
+
key_bindings=kb,
|
|
466
|
+
style=style,
|
|
467
|
+
full_screen=False,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
console.print()
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
app.run()
|
|
474
|
+
except (KeyboardInterrupt, EOFError):
|
|
475
|
+
result[0] = "reject"
|
|
476
|
+
|
|
477
|
+
choice = result[0] or "reject"
|
|
478
|
+
|
|
479
|
+
feedback = ""
|
|
480
|
+
if choice == "reject":
|
|
481
|
+
from prompt_toolkit import PromptSession
|
|
482
|
+
console.print()
|
|
483
|
+
console.print("[dim]Reason for skipping plan mode (optional):[/dim]")
|
|
484
|
+
try:
|
|
485
|
+
session = PromptSession()
|
|
486
|
+
feedback = session.prompt("feedback > ").strip()
|
|
487
|
+
except (KeyboardInterrupt, EOFError):
|
|
488
|
+
return "reject", ""
|
|
489
|
+
|
|
490
|
+
return choice, feedback
|
|
491
|
+
|
|
492
|
+
|
|
376
493
|
def _render_with_interrupt(renderer: SSERenderer, stream) -> dict:
|
|
377
494
|
"""Render stream with ESC key interrupt support.
|
|
378
495
|
|
|
@@ -465,12 +582,17 @@ def _run_interactive(
|
|
|
465
582
|
current_mode = AgentMode(options.get("mode", "code"))
|
|
466
583
|
session_id = None
|
|
467
584
|
current_spec = None
|
|
585
|
+
# Attached images for next message
|
|
586
|
+
attached_images: list[dict] = []
|
|
587
|
+
# Loaded messages from saved session (for restoration)
|
|
588
|
+
loaded_messages: list[dict] = []
|
|
468
589
|
|
|
469
590
|
# Style for prompt
|
|
470
591
|
PROMPT_STYLE = Style.from_dict({
|
|
471
592
|
"prompt.mode.plan": "#ffcc00 bold",
|
|
472
593
|
"prompt.mode.code": "#00cc66 bold",
|
|
473
594
|
"prompt.prefix": "#888888",
|
|
595
|
+
"prompt.image": "#00ccff",
|
|
474
596
|
"completion-menu": "bg:#1a1a2e #ffffff",
|
|
475
597
|
"completion-menu.completion": "bg:#1a1a2e #ffffff",
|
|
476
598
|
"completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
|
|
@@ -517,6 +639,63 @@ def _run_interactive(
|
|
|
517
639
|
"""Insert a newline character with Alt+Enter or Ctrl+J."""
|
|
518
640
|
event.current_buffer.insert_text("\n")
|
|
519
641
|
|
|
642
|
+
@kb.add("c-v") # Ctrl+V to paste (check for images)
|
|
643
|
+
def paste_with_image_check(event):
|
|
644
|
+
"""Paste text or attach image from clipboard."""
|
|
645
|
+
nonlocal attached_images
|
|
646
|
+
from ..clipboard import get_clipboard_image, get_image_from_path
|
|
647
|
+
|
|
648
|
+
# Try to get image from clipboard
|
|
649
|
+
image_data = get_clipboard_image()
|
|
650
|
+
if image_data:
|
|
651
|
+
base64_data, img_format = image_data
|
|
652
|
+
attached_images.append({"data": base64_data, "format": img_format})
|
|
653
|
+
# Refresh prompt to show updated image list
|
|
654
|
+
event.app.invalidate()
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
# Check if clipboard contains an image file path
|
|
658
|
+
clipboard_data = event.app.clipboard.get_data()
|
|
659
|
+
if clipboard_data and clipboard_data.text:
|
|
660
|
+
text = clipboard_data.text.strip()
|
|
661
|
+
# Remove escape characters from dragged paths (e.g., "path\ with\ spaces")
|
|
662
|
+
clean_path = text.replace("\\ ", " ")
|
|
663
|
+
# Check if it looks like an image file path
|
|
664
|
+
if clean_path.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')):
|
|
665
|
+
image_data = get_image_from_path(clean_path)
|
|
666
|
+
if image_data:
|
|
667
|
+
base64_data, img_format = image_data
|
|
668
|
+
attached_images.append({"data": base64_data, "format": img_format})
|
|
669
|
+
event.app.invalidate()
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
# No image, do normal paste
|
|
673
|
+
event.current_buffer.paste_clipboard_data(clipboard_data)
|
|
674
|
+
|
|
675
|
+
def check_for_image_path(buff):
|
|
676
|
+
"""Check if buffer contains an image path and attach it."""
|
|
677
|
+
nonlocal attached_images
|
|
678
|
+
text = buff.text.strip()
|
|
679
|
+
if not text:
|
|
680
|
+
return
|
|
681
|
+
# Clean escaped spaces from dragged paths
|
|
682
|
+
clean_text = text.replace("\\ ", " ")
|
|
683
|
+
if clean_text.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')):
|
|
684
|
+
from ..clipboard import get_image_from_path
|
|
685
|
+
from prompt_toolkit.application import get_app
|
|
686
|
+
image_data = get_image_from_path(clean_text)
|
|
687
|
+
if image_data:
|
|
688
|
+
base64_data, img_format = image_data
|
|
689
|
+
attached_images.append({"data": base64_data, "format": img_format})
|
|
690
|
+
# Clear the buffer
|
|
691
|
+
buff.text = ""
|
|
692
|
+
buff.cursor_position = 0
|
|
693
|
+
# Refresh prompt to show image indicator
|
|
694
|
+
try:
|
|
695
|
+
get_app().invalidate()
|
|
696
|
+
except Exception:
|
|
697
|
+
pass
|
|
698
|
+
|
|
520
699
|
session = PromptSession(
|
|
521
700
|
history=history,
|
|
522
701
|
completer=SlashCommandCompleter(),
|
|
@@ -527,15 +706,19 @@ def _run_interactive(
|
|
|
527
706
|
key_bindings=kb,
|
|
528
707
|
)
|
|
529
708
|
|
|
709
|
+
# Watch for image paths being pasted/dropped
|
|
710
|
+
session.default_buffer.on_text_changed += check_for_image_path
|
|
711
|
+
|
|
530
712
|
def get_prompt():
|
|
531
|
-
"""Get formatted prompt
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
713
|
+
"""Get formatted prompt."""
|
|
714
|
+
nonlocal attached_images
|
|
715
|
+
parts = []
|
|
716
|
+
# Show attached images above prompt
|
|
717
|
+
if attached_images:
|
|
718
|
+
image_tags = " ".join(f"[Image #{i+1}]" for i in range(len(attached_images)))
|
|
719
|
+
parts.append(("class:prompt.image", f" {image_tags}\n"))
|
|
720
|
+
parts.append(("class:prompt.prefix", "> "))
|
|
721
|
+
return parts
|
|
539
722
|
|
|
540
723
|
def show_help():
|
|
541
724
|
"""Show available commands."""
|
|
@@ -564,11 +747,21 @@ def _run_interactive(
|
|
|
564
747
|
|
|
565
748
|
elif command == "/plan":
|
|
566
749
|
current_mode = AgentMode.PLAN
|
|
567
|
-
|
|
750
|
+
# Reset session so next chat creates a new session with plan mode
|
|
751
|
+
if session_id:
|
|
752
|
+
session_id = None
|
|
753
|
+
console.print("[bold green]✓ Plan mode activated[/bold green] [dim](session reset)[/dim]")
|
|
754
|
+
else:
|
|
755
|
+
console.print("[bold green]✓ Plan mode activated[/bold green]")
|
|
568
756
|
|
|
569
757
|
elif command == "/code":
|
|
570
758
|
current_mode = AgentMode.CODE
|
|
571
|
-
|
|
759
|
+
# Reset session so next chat creates a new session with code mode
|
|
760
|
+
if session_id:
|
|
761
|
+
session_id = None
|
|
762
|
+
console.print("[green]Switched to code mode (session reset)[/green]")
|
|
763
|
+
else:
|
|
764
|
+
console.print("[green]Switched to code mode[/green]")
|
|
572
765
|
|
|
573
766
|
elif command == "/mode":
|
|
574
767
|
console.print(f"Current mode: [bold]{current_mode.value}[/bold]")
|
|
@@ -670,6 +863,256 @@ def _run_interactive(
|
|
|
670
863
|
|
|
671
864
|
console.print()
|
|
672
865
|
|
|
866
|
+
elif command == "/agents":
|
|
867
|
+
# Agents management: list, create, show
|
|
868
|
+
from emdash_core.agent.toolkits import list_agent_types, get_custom_agent
|
|
869
|
+
|
|
870
|
+
# Parse subcommand
|
|
871
|
+
subparts = args.split(maxsplit=1) if args else []
|
|
872
|
+
subcommand = subparts[0].lower() if subparts else "list"
|
|
873
|
+
subargs = subparts[1] if len(subparts) > 1 else ""
|
|
874
|
+
|
|
875
|
+
if subcommand in ("list", ""):
|
|
876
|
+
# List all agents
|
|
877
|
+
console.print("\n[bold cyan]Available Agents[/bold cyan]\n")
|
|
878
|
+
|
|
879
|
+
all_agents = list_agent_types(Path.cwd())
|
|
880
|
+
builtin = ["Explore", "Plan"]
|
|
881
|
+
|
|
882
|
+
console.print("[bold]Built-in Agents[/bold]")
|
|
883
|
+
for agent_name in builtin:
|
|
884
|
+
if agent_name == "Explore":
|
|
885
|
+
console.print(" [green]Explore[/green] - Fast codebase exploration (read-only)")
|
|
886
|
+
elif agent_name == "Plan":
|
|
887
|
+
console.print(" [green]Plan[/green] - Design implementation plans")
|
|
888
|
+
|
|
889
|
+
# Custom agents
|
|
890
|
+
custom = [a for a in all_agents if a not in builtin]
|
|
891
|
+
if custom:
|
|
892
|
+
console.print("\n[bold]Custom Agents[/bold] [dim](.emdash/agents/)[/dim]")
|
|
893
|
+
for name in custom:
|
|
894
|
+
agent = get_custom_agent(name, Path.cwd())
|
|
895
|
+
desc = agent.description if agent else ""
|
|
896
|
+
if desc:
|
|
897
|
+
console.print(f" [cyan]{name}[/cyan] - {desc}")
|
|
898
|
+
else:
|
|
899
|
+
console.print(f" [cyan]{name}[/cyan]")
|
|
900
|
+
else:
|
|
901
|
+
console.print("\n[dim]No custom agents found.[/dim]")
|
|
902
|
+
console.print("[dim]Create with: /agents create <name>[/dim]")
|
|
903
|
+
|
|
904
|
+
console.print()
|
|
905
|
+
|
|
906
|
+
elif subcommand == "create":
|
|
907
|
+
# Create a new custom agent
|
|
908
|
+
if not subargs:
|
|
909
|
+
console.print("[yellow]Usage: /agents create <name>[/yellow]")
|
|
910
|
+
console.print("[dim]Example: /agents create code-reviewer[/dim]")
|
|
911
|
+
else:
|
|
912
|
+
agent_name = subargs.strip().lower().replace(" ", "-")
|
|
913
|
+
agents_dir = Path.cwd() / ".emdash" / "agents"
|
|
914
|
+
agent_file = agents_dir / f"{agent_name}.md"
|
|
915
|
+
|
|
916
|
+
if agent_file.exists():
|
|
917
|
+
console.print(f"[yellow]Agent '{agent_name}' already exists[/yellow]")
|
|
918
|
+
console.print(f"[dim]Edit: {agent_file}[/dim]")
|
|
919
|
+
else:
|
|
920
|
+
# Create directory if needed
|
|
921
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
922
|
+
|
|
923
|
+
# Create template
|
|
924
|
+
template = f'''---
|
|
925
|
+
description: Custom agent for specific tasks
|
|
926
|
+
tools: [grep, glob, read_file, semantic_search]
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
# System Prompt
|
|
930
|
+
|
|
931
|
+
You are a specialized assistant for {agent_name.replace("-", " ")} tasks.
|
|
932
|
+
|
|
933
|
+
## Your Mission
|
|
934
|
+
|
|
935
|
+
Describe what this agent should accomplish:
|
|
936
|
+
- Task 1
|
|
937
|
+
- Task 2
|
|
938
|
+
- Task 3
|
|
939
|
+
|
|
940
|
+
## Approach
|
|
941
|
+
|
|
942
|
+
1. **Step One**
|
|
943
|
+
- Details about the first step
|
|
944
|
+
|
|
945
|
+
2. **Step Two**
|
|
946
|
+
- Details about the second step
|
|
947
|
+
|
|
948
|
+
## Output Format
|
|
949
|
+
|
|
950
|
+
Describe how the agent should format its responses.
|
|
951
|
+
|
|
952
|
+
# Examples
|
|
953
|
+
|
|
954
|
+
## Example 1
|
|
955
|
+
User: Example user request
|
|
956
|
+
Agent: Example agent response describing what it would do
|
|
957
|
+
'''
|
|
958
|
+
agent_file.write_text(template)
|
|
959
|
+
console.print(f"[green]Created agent: {agent_name}[/green]")
|
|
960
|
+
console.print(f"[dim]Edit to customize: {agent_file}[/dim]")
|
|
961
|
+
console.print(f"\n[dim]Spawn with Task tool: subagent_type='{agent_name}'[/dim]")
|
|
962
|
+
|
|
963
|
+
elif subcommand == "show":
|
|
964
|
+
# Show details of an agent
|
|
965
|
+
if not subargs:
|
|
966
|
+
console.print("[yellow]Usage: /agents show <name>[/yellow]")
|
|
967
|
+
else:
|
|
968
|
+
agent_name = subargs.strip()
|
|
969
|
+
builtin = ["Explore", "Plan"]
|
|
970
|
+
|
|
971
|
+
if agent_name in builtin:
|
|
972
|
+
console.print(f"\n[bold cyan]{agent_name}[/bold cyan] [dim](built-in)[/dim]\n")
|
|
973
|
+
if agent_name == "Explore":
|
|
974
|
+
console.print("Fast codebase exploration agent (read-only)")
|
|
975
|
+
console.print("\n[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
|
|
976
|
+
elif agent_name == "Plan":
|
|
977
|
+
console.print("Implementation planning agent")
|
|
978
|
+
console.print("\n[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
|
|
979
|
+
console.print()
|
|
980
|
+
else:
|
|
981
|
+
agent = get_custom_agent(agent_name, Path.cwd())
|
|
982
|
+
if agent:
|
|
983
|
+
console.print(f"\n[bold cyan]{agent.name}[/bold cyan] [dim](custom)[/dim]\n")
|
|
984
|
+
if agent.description:
|
|
985
|
+
console.print(f"[bold]Description:[/bold] {agent.description}")
|
|
986
|
+
if agent.tools:
|
|
987
|
+
console.print(f"[bold]Tools:[/bold] {', '.join(agent.tools)}")
|
|
988
|
+
if agent.file_path:
|
|
989
|
+
console.print(f"[bold]File:[/bold] {agent.file_path}")
|
|
990
|
+
if agent.system_prompt:
|
|
991
|
+
console.print(f"\n[bold]System Prompt:[/bold]")
|
|
992
|
+
# Show first 500 chars of system prompt
|
|
993
|
+
preview = agent.system_prompt[:500]
|
|
994
|
+
if len(agent.system_prompt) > 500:
|
|
995
|
+
preview += "..."
|
|
996
|
+
console.print(Panel(preview, border_style="dim"))
|
|
997
|
+
console.print()
|
|
998
|
+
else:
|
|
999
|
+
console.print(f"[yellow]Agent '{agent_name}' not found[/yellow]")
|
|
1000
|
+
console.print("[dim]Use /agents to list available agents[/dim]")
|
|
1001
|
+
|
|
1002
|
+
else:
|
|
1003
|
+
console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
|
|
1004
|
+
console.print("[dim]Usage: /agents [list|create|show] [name][/dim]")
|
|
1005
|
+
|
|
1006
|
+
elif command == "/session":
|
|
1007
|
+
# Session management: list, save, load, delete, clear
|
|
1008
|
+
from ..session_store import SessionStore
|
|
1009
|
+
|
|
1010
|
+
store = SessionStore(Path.cwd())
|
|
1011
|
+
|
|
1012
|
+
# Parse subcommand
|
|
1013
|
+
subparts = args.split(maxsplit=1) if args else []
|
|
1014
|
+
subcommand = subparts[0].lower() if subparts else "list"
|
|
1015
|
+
subargs = subparts[1].strip() if len(subparts) > 1 else ""
|
|
1016
|
+
|
|
1017
|
+
if subcommand == "list" or subcommand == "":
|
|
1018
|
+
# List all sessions
|
|
1019
|
+
sessions = store.list_sessions()
|
|
1020
|
+
if sessions:
|
|
1021
|
+
console.print("\n[bold cyan]Saved Sessions[/bold cyan]\n")
|
|
1022
|
+
for s in sessions:
|
|
1023
|
+
mode_color = "green" if s.mode == "code" else "yellow"
|
|
1024
|
+
active_marker = " [bold green]*[/bold green]" if store.get_active_session() == s.name else ""
|
|
1025
|
+
console.print(f" [cyan]{s.name}[/cyan]{active_marker} [{mode_color}]{s.mode}[/{mode_color}]")
|
|
1026
|
+
console.print(f" [dim]{s.message_count} messages | {s.updated_at[:10]}[/dim]")
|
|
1027
|
+
if s.summary:
|
|
1028
|
+
summary = s.summary[:60] + "..." if len(s.summary) > 60 else s.summary
|
|
1029
|
+
console.print(f" [dim]{summary}[/dim]")
|
|
1030
|
+
console.print()
|
|
1031
|
+
else:
|
|
1032
|
+
console.print("\n[dim]No saved sessions.[/dim]")
|
|
1033
|
+
console.print("[dim]Save with: /session save <name>[/dim]\n")
|
|
1034
|
+
|
|
1035
|
+
elif subcommand == "save":
|
|
1036
|
+
if not subargs:
|
|
1037
|
+
console.print("[yellow]Usage: /session save <name>[/yellow]")
|
|
1038
|
+
console.print("[dim]Example: /session save auth-feature[/dim]")
|
|
1039
|
+
else:
|
|
1040
|
+
# Get current messages from the API session
|
|
1041
|
+
if session_id:
|
|
1042
|
+
try:
|
|
1043
|
+
# Export messages from server
|
|
1044
|
+
export_resp = client.get(f"/api/agent/chat/{session_id}/export")
|
|
1045
|
+
if export_resp.status_code == 200:
|
|
1046
|
+
data = export_resp.json()
|
|
1047
|
+
messages = data.get("messages", [])
|
|
1048
|
+
else:
|
|
1049
|
+
messages = []
|
|
1050
|
+
except Exception:
|
|
1051
|
+
messages = []
|
|
1052
|
+
else:
|
|
1053
|
+
messages = []
|
|
1054
|
+
|
|
1055
|
+
success, msg = store.save_session(
|
|
1056
|
+
name=subargs,
|
|
1057
|
+
messages=messages,
|
|
1058
|
+
mode=current_mode.value,
|
|
1059
|
+
spec=current_spec,
|
|
1060
|
+
model=model,
|
|
1061
|
+
)
|
|
1062
|
+
if success:
|
|
1063
|
+
store.set_active_session(subargs)
|
|
1064
|
+
console.print(f"[green]{msg}[/green]")
|
|
1065
|
+
else:
|
|
1066
|
+
console.print(f"[yellow]{msg}[/yellow]")
|
|
1067
|
+
|
|
1068
|
+
elif subcommand == "load":
|
|
1069
|
+
if not subargs:
|
|
1070
|
+
console.print("[yellow]Usage: /session load <name>[/yellow]")
|
|
1071
|
+
else:
|
|
1072
|
+
session_data = store.load_session(subargs)
|
|
1073
|
+
if session_data:
|
|
1074
|
+
# Reset current session
|
|
1075
|
+
session_id = None
|
|
1076
|
+
current_spec = session_data.spec
|
|
1077
|
+
if session_data.mode == "plan":
|
|
1078
|
+
current_mode = AgentMode.PLAN
|
|
1079
|
+
else:
|
|
1080
|
+
current_mode = AgentMode.CODE
|
|
1081
|
+
|
|
1082
|
+
# Store loaded messages for replay
|
|
1083
|
+
nonlocal loaded_messages
|
|
1084
|
+
loaded_messages = session_data.messages
|
|
1085
|
+
|
|
1086
|
+
store.set_active_session(subargs)
|
|
1087
|
+
console.print(f"[green]Loaded session '{subargs}'[/green]")
|
|
1088
|
+
console.print(f"[dim]{len(session_data.messages)} messages restored, mode: {current_mode.value}[/dim]")
|
|
1089
|
+
if current_spec:
|
|
1090
|
+
console.print("[dim]Spec restored[/dim]")
|
|
1091
|
+
else:
|
|
1092
|
+
console.print(f"[yellow]Session '{subargs}' not found[/yellow]")
|
|
1093
|
+
|
|
1094
|
+
elif subcommand == "delete":
|
|
1095
|
+
if not subargs:
|
|
1096
|
+
console.print("[yellow]Usage: /session delete <name>[/yellow]")
|
|
1097
|
+
else:
|
|
1098
|
+
success, msg = store.delete_session(subargs)
|
|
1099
|
+
if success:
|
|
1100
|
+
console.print(f"[green]{msg}[/green]")
|
|
1101
|
+
else:
|
|
1102
|
+
console.print(f"[yellow]{msg}[/yellow]")
|
|
1103
|
+
|
|
1104
|
+
elif subcommand == "clear":
|
|
1105
|
+
# Clear current session state
|
|
1106
|
+
session_id = None
|
|
1107
|
+
current_spec = None
|
|
1108
|
+
loaded_messages = []
|
|
1109
|
+
store.set_active_session(None)
|
|
1110
|
+
console.print("[green]Session cleared[/green]")
|
|
1111
|
+
|
|
1112
|
+
else:
|
|
1113
|
+
console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
|
|
1114
|
+
console.print("[dim]Usage: /session [list|save|load|delete|clear] [name][/dim]")
|
|
1115
|
+
|
|
673
1116
|
else:
|
|
674
1117
|
console.print(f"[yellow]Unknown command: {command}[/yellow]")
|
|
675
1118
|
console.print("[dim]Type /help for available commands[/dim]")
|
|
@@ -695,14 +1138,13 @@ def _run_interactive(
|
|
|
695
1138
|
except Exception:
|
|
696
1139
|
pass
|
|
697
1140
|
|
|
1141
|
+
# Welcome banner
|
|
698
1142
|
console.print()
|
|
699
|
-
console.print(f"[bold cyan]
|
|
700
|
-
console.print(f"Mode: [bold]{current_mode.value}[/bold] | Model: [dim]{model or 'default'}[/dim]")
|
|
1143
|
+
console.print(f"[bold cyan]Mendy10 Emdash Code[/bold cyan] [dim]v{__version__}[/dim]")
|
|
701
1144
|
if git_repo:
|
|
702
|
-
console.print(f"Repo: [bold green]{git_repo}[/bold green] |
|
|
1145
|
+
console.print(f"[dim]Repo:[/dim] [bold green]{git_repo}[/bold green] [dim]| Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {model or 'default'}")
|
|
703
1146
|
else:
|
|
704
|
-
console.print(f"
|
|
705
|
-
console.print("Type your task or [cyan]/help[/cyan] for commands. Use Ctrl+C to exit.")
|
|
1147
|
+
console.print(f"[dim]Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {model or 'default'}")
|
|
706
1148
|
console.print()
|
|
707
1149
|
|
|
708
1150
|
while True:
|
|
@@ -713,6 +1155,16 @@ def _run_interactive(
|
|
|
713
1155
|
if not user_input:
|
|
714
1156
|
continue
|
|
715
1157
|
|
|
1158
|
+
# Check if input is an image file path (dragged file)
|
|
1159
|
+
clean_input = user_input.replace("\\ ", " ")
|
|
1160
|
+
if clean_input.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')):
|
|
1161
|
+
from ..clipboard import get_image_from_path
|
|
1162
|
+
image_data = get_image_from_path(clean_input)
|
|
1163
|
+
if image_data:
|
|
1164
|
+
base64_data, img_format = image_data
|
|
1165
|
+
attached_images.append({"data": base64_data, "format": img_format})
|
|
1166
|
+
continue # Prompt again for actual message
|
|
1167
|
+
|
|
716
1168
|
# Handle slash commands
|
|
717
1169
|
if user_input.startswith("/"):
|
|
718
1170
|
if not handle_slash_command(user_input):
|
|
@@ -731,15 +1183,28 @@ def _run_interactive(
|
|
|
731
1183
|
|
|
732
1184
|
# Run agent with current mode
|
|
733
1185
|
try:
|
|
1186
|
+
# Prepare images for API call
|
|
1187
|
+
images_to_send = attached_images if attached_images else None
|
|
1188
|
+
|
|
734
1189
|
if session_id:
|
|
735
|
-
stream = client.agent_continue_stream(
|
|
1190
|
+
stream = client.agent_continue_stream(
|
|
1191
|
+
session_id, user_input, images=images_to_send
|
|
1192
|
+
)
|
|
736
1193
|
else:
|
|
1194
|
+
# Pass loaded_messages from saved session if available
|
|
737
1195
|
stream = client.agent_chat_stream(
|
|
738
1196
|
message=user_input,
|
|
739
1197
|
model=model,
|
|
740
1198
|
max_iterations=max_iterations,
|
|
741
1199
|
options=request_options,
|
|
1200
|
+
images=images_to_send,
|
|
1201
|
+
history=loaded_messages if loaded_messages else None,
|
|
742
1202
|
)
|
|
1203
|
+
# Clear loaded_messages after first use
|
|
1204
|
+
loaded_messages = []
|
|
1205
|
+
|
|
1206
|
+
# Clear attached images after sending
|
|
1207
|
+
attached_images = []
|
|
743
1208
|
|
|
744
1209
|
# Render the stream and capture any spec output
|
|
745
1210
|
result = _render_with_interrupt(renderer, stream)
|
|
@@ -752,22 +1217,56 @@ def _run_interactive(
|
|
|
752
1217
|
if result and result.get("spec"):
|
|
753
1218
|
current_spec = result["spec"]
|
|
754
1219
|
|
|
755
|
-
# Handle
|
|
756
|
-
|
|
757
|
-
|
|
1220
|
+
# Handle clarifications (may be chained - loop until no more)
|
|
1221
|
+
while True:
|
|
1222
|
+
clarification = result.get("clarification")
|
|
1223
|
+
if not (clarification and session_id):
|
|
1224
|
+
break
|
|
1225
|
+
|
|
758
1226
|
response = _get_clarification_response(clarification)
|
|
759
|
-
if response:
|
|
760
|
-
|
|
761
|
-
|
|
1227
|
+
if not response:
|
|
1228
|
+
break
|
|
1229
|
+
|
|
1230
|
+
# Show the user's selection in the chat
|
|
1231
|
+
console.print()
|
|
1232
|
+
console.print(f"[dim]Selected:[/dim] [bold]{response}[/bold]")
|
|
1233
|
+
console.print()
|
|
1234
|
+
|
|
1235
|
+
# Use dedicated clarification answer endpoint
|
|
1236
|
+
try:
|
|
1237
|
+
stream = client.clarification_answer_stream(session_id, response)
|
|
762
1238
|
result = _render_with_interrupt(renderer, stream)
|
|
763
1239
|
|
|
764
1240
|
# Update mode if user chose code
|
|
765
1241
|
if "code" in response.lower():
|
|
766
1242
|
current_mode = AgentMode.CODE
|
|
1243
|
+
except Exception as e:
|
|
1244
|
+
console.print(f"[red]Error continuing session: {e}[/red]")
|
|
1245
|
+
break
|
|
1246
|
+
|
|
1247
|
+
# Handle plan mode entry request (show approval menu)
|
|
1248
|
+
plan_mode_requested = result.get("plan_mode_requested")
|
|
1249
|
+
if plan_mode_requested is not None and session_id:
|
|
1250
|
+
choice, feedback = _show_plan_mode_approval_menu()
|
|
1251
|
+
|
|
1252
|
+
if choice == "approve":
|
|
1253
|
+
current_mode = AgentMode.PLAN
|
|
1254
|
+
console.print()
|
|
1255
|
+
console.print("[bold green]✓ Plan mode activated[/bold green]")
|
|
1256
|
+
console.print()
|
|
1257
|
+
# Use the planmode approve endpoint
|
|
1258
|
+
stream = client.planmode_approve_stream(session_id)
|
|
1259
|
+
result = _render_with_interrupt(renderer, stream)
|
|
1260
|
+
# After approval, check if there's now a plan submitted
|
|
1261
|
+
if result.get("plan_submitted"):
|
|
1262
|
+
plan_submitted = result.get("plan_submitted")
|
|
1263
|
+
elif choice == "reject":
|
|
1264
|
+
# Use the planmode reject endpoint - stay in code mode
|
|
1265
|
+
stream = client.planmode_reject_stream(session_id, feedback)
|
|
1266
|
+
_render_with_interrupt(renderer, stream)
|
|
767
1267
|
|
|
768
1268
|
# Handle plan mode completion (show approval menu)
|
|
769
1269
|
# Only show menu when agent explicitly submits a plan via exit_plan tool
|
|
770
|
-
content = result.get("content", "")
|
|
771
1270
|
plan_submitted = result.get("plan_submitted")
|
|
772
1271
|
should_show_plan_menu = (
|
|
773
1272
|
current_mode == AgentMode.PLAN and
|
|
@@ -779,20 +1278,13 @@ def _run_interactive(
|
|
|
779
1278
|
|
|
780
1279
|
if choice == "approve":
|
|
781
1280
|
current_mode = AgentMode.CODE
|
|
782
|
-
#
|
|
783
|
-
|
|
784
|
-
ModeState.get_instance().current_mode = CoreMode.CODE
|
|
785
|
-
stream = client.agent_continue_stream(
|
|
786
|
-
session_id,
|
|
787
|
-
"The plan has been approved. Start implementing it now."
|
|
788
|
-
)
|
|
1281
|
+
# Use the plan approve endpoint which properly resets mode on server
|
|
1282
|
+
stream = client.plan_approve_stream(session_id)
|
|
789
1283
|
_render_with_interrupt(renderer, stream)
|
|
790
1284
|
elif choice == "reject":
|
|
791
1285
|
if feedback:
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
f"The plan was rejected. Please revise based on this feedback: {feedback}"
|
|
795
|
-
)
|
|
1286
|
+
# Use the plan reject endpoint which keeps mode as PLAN on server
|
|
1287
|
+
stream = client.plan_reject_stream(session_id, feedback)
|
|
796
1288
|
_render_with_interrupt(renderer, stream)
|
|
797
1289
|
else:
|
|
798
1290
|
console.print("[dim]Plan rejected[/dim]")
|