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.
@@ -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
- console.print(f"[green]📎 Image attached[/green] [dim]({img_format})[/dim]")
535
- else:
536
- # No image, do normal paste
537
- event.current_buffer.paste_clipboard_data(event.app.clipboard.get_data())
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
- # Add image indicator if images attached
716
+ # Show attached images above prompt
554
717
  if attached_images:
555
- parts.append(("class:prompt.image", f"📎{len(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"))
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
- console.print("[yellow]Switched to plan mode[/yellow]")
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
- console.print("[green]Switched to code mode[/green]")
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 clarification with options (interactive selection)
783
- clarification = result.get("clarification")
784
- if clarification and clarification.get("options") and session_id:
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
- # Continue session with user's choice
788
- stream = client.agent_continue_stream(session_id, response)
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
- # Reset mode state to CODE
810
- from emdash_core.agent.tools.modes import ModeState, AgentMode as CoreMode
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
- stream = client.agent_continue_stream(
820
- session_id,
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]")