emdash-cli 0.1.30__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 +121 -0
- emdash_cli/commands/agent.py +493 -28
- emdash_cli/session_store.py +321 -0
- emdash_cli/sse_renderer.py +224 -109
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.35.dist-info}/METADATA +2 -2
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.35.dist-info}/RECORD +9 -8
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.35.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.30.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
|
|
|
@@ -467,6 +584,8 @@ def _run_interactive(
|
|
|
467
584
|
current_spec = None
|
|
468
585
|
# Attached images for next message
|
|
469
586
|
attached_images: list[dict] = []
|
|
587
|
+
# Loaded messages from saved session (for restoration)
|
|
588
|
+
loaded_messages: list[dict] = []
|
|
470
589
|
|
|
471
590
|
# Style for prompt
|
|
472
591
|
PROMPT_STYLE = Style.from_dict({
|
|
@@ -524,17 +643,58 @@ def _run_interactive(
|
|
|
524
643
|
def paste_with_image_check(event):
|
|
525
644
|
"""Paste text or attach image from clipboard."""
|
|
526
645
|
nonlocal attached_images
|
|
527
|
-
from ..clipboard import get_clipboard_image
|
|
646
|
+
from ..clipboard import get_clipboard_image, get_image_from_path
|
|
528
647
|
|
|
529
648
|
# Try to get image from clipboard
|
|
530
649
|
image_data = get_clipboard_image()
|
|
531
650
|
if image_data:
|
|
532
651
|
base64_data, img_format = image_data
|
|
533
652
|
attached_images.append({"data": base64_data, "format": img_format})
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
|
538
698
|
|
|
539
699
|
session = PromptSession(
|
|
540
700
|
history=history,
|
|
@@ -546,13 +706,17 @@ def _run_interactive(
|
|
|
546
706
|
key_bindings=kb,
|
|
547
707
|
)
|
|
548
708
|
|
|
709
|
+
# Watch for image paths being pasted/dropped
|
|
710
|
+
session.default_buffer.on_text_changed += check_for_image_path
|
|
711
|
+
|
|
549
712
|
def get_prompt():
|
|
550
713
|
"""Get formatted prompt."""
|
|
551
714
|
nonlocal attached_images
|
|
552
715
|
parts = []
|
|
553
|
-
#
|
|
716
|
+
# Show attached images above prompt
|
|
554
717
|
if attached_images:
|
|
555
|
-
|
|
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"))
|
|
556
720
|
parts.append(("class:prompt.prefix", "> "))
|
|
557
721
|
return parts
|
|
558
722
|
|
|
@@ -583,11 +747,21 @@ def _run_interactive(
|
|
|
583
747
|
|
|
584
748
|
elif command == "/plan":
|
|
585
749
|
current_mode = AgentMode.PLAN
|
|
586
|
-
|
|
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]")
|
|
587
756
|
|
|
588
757
|
elif command == "/code":
|
|
589
758
|
current_mode = AgentMode.CODE
|
|
590
|
-
|
|
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]")
|
|
591
765
|
|
|
592
766
|
elif command == "/mode":
|
|
593
767
|
console.print(f"Current mode: [bold]{current_mode.value}[/bold]")
|
|
@@ -689,6 +863,256 @@ def _run_interactive(
|
|
|
689
863
|
|
|
690
864
|
console.print()
|
|
691
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
|
+
|
|
692
1116
|
else:
|
|
693
1117
|
console.print(f"[yellow]Unknown command: {command}[/yellow]")
|
|
694
1118
|
console.print("[dim]Type /help for available commands[/dim]")
|
|
@@ -716,7 +1140,7 @@ def _run_interactive(
|
|
|
716
1140
|
|
|
717
1141
|
# Welcome banner
|
|
718
1142
|
console.print()
|
|
719
|
-
console.print(f"[bold cyan]Emdash Code[/bold cyan] [dim]v{__version__}[/dim]")
|
|
1143
|
+
console.print(f"[bold cyan]Mendy10 Emdash Code[/bold cyan] [dim]v{__version__}[/dim]")
|
|
720
1144
|
if git_repo:
|
|
721
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'}")
|
|
722
1146
|
else:
|
|
@@ -731,6 +1155,16 @@ def _run_interactive(
|
|
|
731
1155
|
if not user_input:
|
|
732
1156
|
continue
|
|
733
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
|
+
|
|
734
1168
|
# Handle slash commands
|
|
735
1169
|
if user_input.startswith("/"):
|
|
736
1170
|
if not handle_slash_command(user_input):
|
|
@@ -757,13 +1191,17 @@ def _run_interactive(
|
|
|
757
1191
|
session_id, user_input, images=images_to_send
|
|
758
1192
|
)
|
|
759
1193
|
else:
|
|
1194
|
+
# Pass loaded_messages from saved session if available
|
|
760
1195
|
stream = client.agent_chat_stream(
|
|
761
1196
|
message=user_input,
|
|
762
1197
|
model=model,
|
|
763
1198
|
max_iterations=max_iterations,
|
|
764
1199
|
options=request_options,
|
|
765
1200
|
images=images_to_send,
|
|
1201
|
+
history=loaded_messages if loaded_messages else None,
|
|
766
1202
|
)
|
|
1203
|
+
# Clear loaded_messages after first use
|
|
1204
|
+
loaded_messages = []
|
|
767
1205
|
|
|
768
1206
|
# Clear attached images after sending
|
|
769
1207
|
attached_images = []
|
|
@@ -779,22 +1217,56 @@ def _run_interactive(
|
|
|
779
1217
|
if result and result.get("spec"):
|
|
780
1218
|
current_spec = result["spec"]
|
|
781
1219
|
|
|
782
|
-
# Handle
|
|
783
|
-
|
|
784
|
-
|
|
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
|
+
|
|
785
1226
|
response = _get_clarification_response(clarification)
|
|
786
|
-
if response:
|
|
787
|
-
|
|
788
|
-
|
|
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)
|
|
789
1238
|
result = _render_with_interrupt(renderer, stream)
|
|
790
1239
|
|
|
791
1240
|
# Update mode if user chose code
|
|
792
1241
|
if "code" in response.lower():
|
|
793
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)
|
|
794
1267
|
|
|
795
1268
|
# Handle plan mode completion (show approval menu)
|
|
796
1269
|
# Only show menu when agent explicitly submits a plan via exit_plan tool
|
|
797
|
-
content = result.get("content", "")
|
|
798
1270
|
plan_submitted = result.get("plan_submitted")
|
|
799
1271
|
should_show_plan_menu = (
|
|
800
1272
|
current_mode == AgentMode.PLAN and
|
|
@@ -806,20 +1278,13 @@ def _run_interactive(
|
|
|
806
1278
|
|
|
807
1279
|
if choice == "approve":
|
|
808
1280
|
current_mode = AgentMode.CODE
|
|
809
|
-
#
|
|
810
|
-
|
|
811
|
-
ModeState.get_instance().current_mode = CoreMode.CODE
|
|
812
|
-
stream = client.agent_continue_stream(
|
|
813
|
-
session_id,
|
|
814
|
-
"The plan has been approved. Start implementing it now."
|
|
815
|
-
)
|
|
1281
|
+
# Use the plan approve endpoint which properly resets mode on server
|
|
1282
|
+
stream = client.plan_approve_stream(session_id)
|
|
816
1283
|
_render_with_interrupt(renderer, stream)
|
|
817
1284
|
elif choice == "reject":
|
|
818
1285
|
if feedback:
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
f"The plan was rejected. Please revise based on this feedback: {feedback}"
|
|
822
|
-
)
|
|
1286
|
+
# Use the plan reject endpoint which keeps mode as PLAN on server
|
|
1287
|
+
stream = client.plan_reject_stream(session_id, feedback)
|
|
823
1288
|
_render_with_interrupt(renderer, stream)
|
|
824
1289
|
else:
|
|
825
1290
|
console.print("[dim]Plan rejected[/dim]")
|