emdash-cli 0.1.17__py3-none-any.whl → 0.1.30__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.
@@ -1,5 +1,8 @@
1
1
  """Agent CLI commands."""
2
2
 
3
+ import os
4
+ import threading
5
+
3
6
  import click
4
7
  from enum import Enum
5
8
  from rich.console import Console
@@ -7,6 +10,7 @@ from rich.panel import Panel
7
10
  from rich.markdown import Markdown
8
11
 
9
12
  from ..client import EmdashClient
13
+ from ..keyboard import KeyListener
10
14
  from ..server_manager import get_server_manager
11
15
  from ..sse_renderer import SSERenderer
12
16
 
@@ -16,15 +20,13 @@ console = Console()
16
20
  class AgentMode(Enum):
17
21
  """Agent operation modes."""
18
22
  PLAN = "plan"
19
- TASKS = "tasks"
20
23
  CODE = "code"
21
24
 
22
25
 
23
26
  # Slash commands available in interactive mode
24
27
  SLASH_COMMANDS = {
25
28
  # Mode switching
26
- "/plan": "Switch to plan mode (explore codebase, create specs)",
27
- "/tasks": "Switch to tasks mode (generate task lists)",
29
+ "/plan": "Switch to plan mode (explore codebase, create plans)",
28
30
  "/code": "Switch to code mode (execute file changes)",
29
31
  "/mode": "Show current mode",
30
32
  # Generation commands
@@ -51,10 +53,10 @@ def agent():
51
53
  @agent.command("code")
52
54
  @click.argument("task", required=False)
53
55
  @click.option("--model", "-m", default=None, help="Model to use")
54
- @click.option("--mode", type=click.Choice(["plan", "tasks", "code"]), default="code",
56
+ @click.option("--mode", type=click.Choice(["plan", "code"]), default="code",
55
57
  help="Starting mode")
56
58
  @click.option("--quiet", "-q", is_flag=True, help="Less verbose output")
57
- @click.option("--max-iterations", default=20, help="Max agent iterations")
59
+ @click.option("--max-iterations", default=int(os.getenv("EMDASH_MAX_ITERATIONS", "100")), help="Max agent iterations")
58
60
  @click.option("--no-graph-tools", is_flag=True, help="Skip graph exploration tools")
59
61
  @click.option("--save", is_flag=True, help="Save specs to specs/<feature>/")
60
62
  def agent_code(
@@ -72,13 +74,11 @@ def agent_code(
72
74
  Without TASK: Start interactive REPL mode
73
75
 
74
76
  MODES:
75
- plan - Explore codebase and create specifications
76
- tasks - Generate implementation task lists
77
+ plan - Explore codebase and create plans (read-only)
77
78
  code - Execute code changes (default)
78
79
 
79
80
  SLASH COMMANDS (in interactive mode):
80
81
  /plan - Switch to plan mode
81
- /tasks - Switch to tasks mode
82
82
  /code - Switch to code mode
83
83
  /help - Show available commands
84
84
  /reset - Reset session
@@ -111,7 +111,7 @@ def agent_code(
111
111
 
112
112
 
113
113
  def _get_clarification_response(clarification: dict) -> str | None:
114
- """Get user response for clarification with options.
114
+ """Get user response for clarification with interactive selection.
115
115
 
116
116
  Args:
117
117
  clarification: Dict with question, context, and options
@@ -119,49 +119,22 @@ def _get_clarification_response(clarification: dict) -> str | None:
119
119
  Returns:
120
120
  User's selected option or typed response, or None if cancelled
121
121
  """
122
- from prompt_toolkit import PromptSession
123
-
124
- options = clarification.get("options", [])
125
-
126
- session = PromptSession()
127
- console.print("[dim]Enter number or type response:[/dim]")
128
-
129
- try:
130
- response = session.prompt("choice > ").strip()
131
-
132
- if not response:
133
- return None
134
-
135
- # Map number to option
136
- if response.isdigit():
137
- idx = int(response) - 1
138
- if 0 <= idx < len(options):
139
- return options[idx]
140
-
141
- return response
142
- except (KeyboardInterrupt, EOFError):
143
- return None
144
-
145
-
146
- def _show_spec_approval_menu() -> tuple[str, str]:
147
- """Show spec approval menu with arrow-key selection.
148
-
149
- Returns:
150
- Tuple of (choice, feedback) where feedback is only set for 'refine'
151
- """
152
- from prompt_toolkit import Application
122
+ from prompt_toolkit import Application, PromptSession
153
123
  from prompt_toolkit.key_binding import KeyBindings
154
124
  from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
155
125
  from prompt_toolkit.styles import Style
156
126
 
157
- options = [
158
- ("tasks", "Generate implementation tasks"),
159
- ("code", "Start coding directly"),
160
- ("refine", "Provide feedback to improve"),
161
- ("abort", "Cancel and discard"),
162
- ]
127
+ options = clarification.get("options", [])
163
128
 
164
- selected_index = [0] # Use list to allow mutation in closure
129
+ if not options:
130
+ # No options, just get free-form input
131
+ session = PromptSession()
132
+ try:
133
+ return session.prompt("response > ").strip() or None
134
+ except (KeyboardInterrupt, EOFError):
135
+ return None
136
+
137
+ selected_index = [0]
165
138
  result = [None]
166
139
 
167
140
  # Key bindings
@@ -179,64 +152,53 @@ def _show_spec_approval_menu() -> tuple[str, str]:
179
152
 
180
153
  @kb.add("enter")
181
154
  def select(event):
182
- result[0] = options[selected_index[0]][0]
155
+ result[0] = options[selected_index[0]]
183
156
  event.app.exit()
184
157
 
185
- @kb.add("1")
186
- def select_1(event):
187
- result[0] = "tasks"
188
- event.app.exit()
189
-
190
- @kb.add("2")
191
- def select_2(event):
192
- result[0] = "code"
193
- event.app.exit()
194
-
195
- @kb.add("3")
196
- def select_3(event):
197
- result[0] = "refine"
198
- event.app.exit()
199
-
200
- @kb.add("4")
201
- def select_4(event):
202
- result[0] = "abort"
203
- event.app.exit()
158
+ # Number key shortcuts (1-9)
159
+ for i in range(min(9, len(options))):
160
+ @kb.add(str(i + 1))
161
+ def select_by_number(event, idx=i):
162
+ result[0] = options[idx]
163
+ event.app.exit()
204
164
 
205
165
  @kb.add("c-c")
206
- @kb.add("q")
207
166
  @kb.add("escape")
208
167
  def cancel(event):
209
- result[0] = "abort"
168
+ result[0] = None
169
+ event.app.exit()
170
+
171
+ @kb.add("o") # 'o' for Other - custom input
172
+ def other_input(event):
173
+ result[0] = "OTHER_INPUT"
210
174
  event.app.exit()
211
175
 
212
176
  def get_formatted_options():
213
- lines = [("class:title", "What would you like to do with this spec?\n\n")]
214
- for i, (key, desc) in enumerate(options):
177
+ lines = []
178
+ for i, opt in enumerate(options):
215
179
  if i == selected_index[0]:
216
- lines.append(("class:selected", f" ❯ {key:8} "))
217
- lines.append(("class:selected-desc", f"- {desc}\n"))
180
+ lines.append(("class:selected", f" ❯ [{i+1}] {opt}\n"))
218
181
  else:
219
- lines.append(("class:option", f" {key:8} "))
220
- lines.append(("class:desc", f"- {desc}\n"))
221
- lines.append(("class:hint", "\n↑/↓ to move, Enter to select, q to cancel"))
182
+ lines.append(("class:option", f" [{i+1}] {opt}\n"))
183
+ lines.append(("class:hint", "\n↑/↓ to move, Enter to select, 1-9 for quick select, o for other"))
222
184
  return lines
223
185
 
224
186
  # Style
225
187
  style = Style.from_dict({
226
- "title": "#00ccff bold",
227
188
  "selected": "#00cc66 bold",
228
- "selected-desc": "#00cc66",
229
189
  "option": "#888888",
230
- "desc": "#666666",
231
190
  "hint": "#444444 italic",
232
191
  })
233
192
 
193
+ # Calculate height based on options
194
+ height = len(options) + 2 # options + hint line + padding
195
+
234
196
  # Layout
235
197
  layout = Layout(
236
198
  HSplit([
237
199
  Window(
238
200
  FormattedTextControl(get_formatted_options),
239
- height=8,
201
+ height=height,
240
202
  ),
241
203
  ])
242
204
  )
@@ -254,30 +216,48 @@ def _show_spec_approval_menu() -> tuple[str, str]:
254
216
  try:
255
217
  app.run()
256
218
  except (KeyboardInterrupt, EOFError):
257
- result[0] = "abort"
258
-
259
- choice = result[0] or "abort"
219
+ return None
260
220
 
261
- # Get feedback if refine was chosen
262
- feedback = ""
263
- if choice == "refine":
264
- from prompt_toolkit import PromptSession
221
+ # Handle "other" option - get custom input
222
+ if result[0] == "OTHER_INPUT":
223
+ session = PromptSession()
265
224
  console.print()
266
- console.print("[dim]What changes would you like?[/dim]")
267
225
  try:
268
- session = PromptSession()
269
- feedback = session.prompt("feedback > ").strip()
226
+ return session.prompt("response > ").strip() or None
270
227
  except (KeyboardInterrupt, EOFError):
271
- return "abort", ""
228
+ return None
272
229
 
273
- return choice, feedback
230
+ # Check if selected option is an "other/explain" type that needs text input
231
+ if result[0]:
232
+ lower_result = result[0].lower()
233
+ needs_input = any(phrase in lower_result for phrase in [
234
+ "something else",
235
+ "other",
236
+ "i'll explain",
237
+ "i will explain",
238
+ "let me explain",
239
+ "custom",
240
+ "none of the above",
241
+ ])
242
+ if needs_input:
243
+ session = PromptSession()
244
+ console.print()
245
+ console.print("[dim]Please explain:[/dim]")
246
+ try:
247
+ custom_input = session.prompt("response > ").strip()
248
+ if custom_input:
249
+ return custom_input
250
+ except (KeyboardInterrupt, EOFError):
251
+ return None
252
+
253
+ return result[0]
274
254
 
275
255
 
276
- def _show_tasks_approval_menu() -> tuple[str, str]:
277
- """Show tasks approval menu with arrow-key selection.
256
+ def _show_plan_approval_menu() -> tuple[str, str]:
257
+ """Show plan approval menu with simple approve/reject options.
278
258
 
279
259
  Returns:
280
- Tuple of (choice, feedback) where feedback is only set for 'refine'
260
+ Tuple of (choice, feedback) where feedback is only set for 'reject'
281
261
  """
282
262
  from prompt_toolkit import Application
283
263
  from prompt_toolkit.key_binding import KeyBindings
@@ -285,10 +265,8 @@ def _show_tasks_approval_menu() -> tuple[str, str]:
285
265
  from prompt_toolkit.styles import Style
286
266
 
287
267
  options = [
288
- ("code", "Start implementing these tasks"),
289
- ("refine", "Refine tasks with more details"),
290
- ("export", "Export tasks to file"),
291
- ("abort", "Cancel and discard"),
268
+ ("approve", "Approve and start implementation"),
269
+ ("reject", "Reject and provide feedback"),
292
270
  ]
293
271
 
294
272
  selected_index = [0] # Use list to allow mutation in closure
@@ -313,34 +291,26 @@ def _show_tasks_approval_menu() -> tuple[str, str]:
313
291
  event.app.exit()
314
292
 
315
293
  @kb.add("1")
316
- def select_1(event):
317
- result[0] = "code"
294
+ @kb.add("y")
295
+ def select_approve(event):
296
+ result[0] = "approve"
318
297
  event.app.exit()
319
298
 
320
299
  @kb.add("2")
321
- def select_2(event):
322
- result[0] = "refine"
323
- event.app.exit()
324
-
325
- @kb.add("3")
326
- def select_3(event):
327
- result[0] = "export"
328
- event.app.exit()
329
-
330
- @kb.add("4")
331
- def select_4(event):
332
- result[0] = "abort"
300
+ @kb.add("n")
301
+ def select_reject(event):
302
+ result[0] = "reject"
333
303
  event.app.exit()
334
304
 
335
305
  @kb.add("c-c")
336
306
  @kb.add("q")
337
307
  @kb.add("escape")
338
308
  def cancel(event):
339
- result[0] = "abort"
309
+ result[0] = "reject"
340
310
  event.app.exit()
341
311
 
342
312
  def get_formatted_options():
343
- lines = [("class:title", "What would you like to do with these tasks?\n\n")]
313
+ lines = [("class:title", "Approve this plan?\n\n")]
344
314
  for i, (key, desc) in enumerate(options):
345
315
  if i == selected_index[0]:
346
316
  lines.append(("class:selected", f" ❯ {key:8} "))
@@ -348,14 +318,14 @@ def _show_tasks_approval_menu() -> tuple[str, str]:
348
318
  else:
349
319
  lines.append(("class:option", f" {key:8} "))
350
320
  lines.append(("class:desc", f"- {desc}\n"))
351
- lines.append(("class:hint", "\n↑/↓ to move, Enter to select, q to cancel"))
321
+ lines.append(("class:hint", "\n↑/↓ to move, Enter to select, y/n for quick select"))
352
322
  return lines
353
323
 
354
324
  # Style
355
325
  style = Style.from_dict({
356
- "title": "#cc66ff bold", # Purple for tasks
357
- "selected": "#cc66ff bold",
358
- "selected-desc": "#cc66ff",
326
+ "title": "#00ccff bold",
327
+ "selected": "#00cc66 bold",
328
+ "selected-desc": "#00cc66",
359
329
  "option": "#888888",
360
330
  "desc": "#666666",
361
331
  "hint": "#444444 italic",
@@ -366,7 +336,7 @@ def _show_tasks_approval_menu() -> tuple[str, str]:
366
336
  HSplit([
367
337
  Window(
368
338
  FormattedTextControl(get_formatted_options),
369
- height=8,
339
+ height=6,
370
340
  ),
371
341
  ])
372
342
  )
@@ -384,25 +354,50 @@ def _show_tasks_approval_menu() -> tuple[str, str]:
384
354
  try:
385
355
  app.run()
386
356
  except (KeyboardInterrupt, EOFError):
387
- result[0] = "abort"
357
+ result[0] = "reject"
388
358
 
389
- choice = result[0] or "abort"
359
+ choice = result[0] or "reject"
390
360
 
391
- # Get feedback if refine was chosen
361
+ # Get feedback if reject was chosen
392
362
  feedback = ""
393
- if choice == "refine":
363
+ if choice == "reject":
394
364
  from prompt_toolkit import PromptSession
395
365
  console.print()
396
- console.print("[dim]What changes would you like to the tasks?[/dim]")
366
+ console.print("[dim]What changes would you like?[/dim]")
397
367
  try:
398
368
  session = PromptSession()
399
369
  feedback = session.prompt("feedback > ").strip()
400
370
  except (KeyboardInterrupt, EOFError):
401
- return "abort", ""
371
+ return "reject", ""
402
372
 
403
373
  return choice, feedback
404
374
 
405
375
 
376
+ def _render_with_interrupt(renderer: SSERenderer, stream) -> dict:
377
+ """Render stream with ESC key interrupt support.
378
+
379
+ Args:
380
+ renderer: SSE renderer instance
381
+ stream: SSE stream iterator
382
+
383
+ Returns:
384
+ Result dict from renderer, with 'interrupted' flag
385
+ """
386
+ interrupt_event = threading.Event()
387
+
388
+ def on_escape():
389
+ interrupt_event.set()
390
+
391
+ listener = KeyListener(on_escape)
392
+
393
+ try:
394
+ listener.start()
395
+ result = renderer.render_stream(stream, interrupt_event=interrupt_event)
396
+ return result
397
+ finally:
398
+ listener.stop()
399
+
400
+
406
401
  def _run_single_task(
407
402
  client: EmdashClient,
408
403
  renderer: SSERenderer,
@@ -419,7 +414,9 @@ def _run_single_task(
419
414
  max_iterations=max_iterations,
420
415
  options=options,
421
416
  )
422
- renderer.render_stream(stream)
417
+ result = _render_with_interrupt(renderer, stream)
418
+ if result.get("interrupted"):
419
+ console.print("[dim]Task interrupted. You can continue or start a new task.[/dim]")
423
420
  except Exception as e:
424
421
  console.print(f"[red]Error: {e}[/red]")
425
422
  raise click.Abort()
@@ -441,7 +438,9 @@ def _run_slash_command_task(
441
438
  max_iterations=max_iterations,
442
439
  options=options,
443
440
  )
444
- renderer.render_stream(stream)
441
+ result = _render_with_interrupt(renderer, stream)
442
+ if result.get("interrupted"):
443
+ console.print("[dim]Task interrupted.[/dim]")
445
444
  console.print()
446
445
  except Exception as e:
447
446
  console.print(f"[red]Error: {e}[/red]")
@@ -466,13 +465,15 @@ def _run_interactive(
466
465
  current_mode = AgentMode(options.get("mode", "code"))
467
466
  session_id = None
468
467
  current_spec = None
468
+ # Attached images for next message
469
+ attached_images: list[dict] = []
469
470
 
470
471
  # Style for prompt
471
472
  PROMPT_STYLE = Style.from_dict({
472
473
  "prompt.mode.plan": "#ffcc00 bold",
473
- "prompt.mode.tasks": "#cc66ff bold",
474
474
  "prompt.mode.code": "#00cc66 bold",
475
475
  "prompt.prefix": "#888888",
476
+ "prompt.image": "#00ccff",
476
477
  "completion-menu": "bg:#1a1a2e #ffffff",
477
478
  "completion-menu.completion": "bg:#1a1a2e #ffffff",
478
479
  "completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
@@ -519,6 +520,22 @@ def _run_interactive(
519
520
  """Insert a newline character with Alt+Enter or Ctrl+J."""
520
521
  event.current_buffer.insert_text("\n")
521
522
 
523
+ @kb.add("c-v") # Ctrl+V to paste (check for images)
524
+ def paste_with_image_check(event):
525
+ """Paste text or attach image from clipboard."""
526
+ nonlocal attached_images
527
+ from ..clipboard import get_clipboard_image
528
+
529
+ # Try to get image from clipboard
530
+ image_data = get_clipboard_image()
531
+ if image_data:
532
+ base64_data, img_format = image_data
533
+ 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())
538
+
522
539
  session = PromptSession(
523
540
  history=history,
524
541
  completer=SlashCommandCompleter(),
@@ -530,15 +547,14 @@ def _run_interactive(
530
547
  )
531
548
 
532
549
  def get_prompt():
533
- """Get formatted prompt based on current mode."""
534
- mode_colors = {
535
- AgentMode.PLAN: "class:prompt.mode.plan",
536
- AgentMode.TASKS: "class:prompt.mode.tasks",
537
- AgentMode.CODE: "class:prompt.mode.code",
538
- }
539
- mode_name = current_mode.value
540
- color_class = mode_colors.get(current_mode, "class:prompt.mode.code")
541
- return [(color_class, f"[{mode_name}]"), ("", " "), ("class:prompt.prefix", "> ")]
550
+ """Get formatted prompt."""
551
+ nonlocal attached_images
552
+ parts = []
553
+ # Add image indicator if images attached
554
+ if attached_images:
555
+ parts.append(("class:prompt.image", f"📎{len(attached_images)} "))
556
+ parts.append(("class:prompt.prefix", "> "))
557
+ return parts
542
558
 
543
559
  def show_help():
544
560
  """Show available commands."""
@@ -569,10 +585,6 @@ def _run_interactive(
569
585
  current_mode = AgentMode.PLAN
570
586
  console.print("[yellow]Switched to plan mode[/yellow]")
571
587
 
572
- elif command == "/tasks":
573
- current_mode = AgentMode.TASKS
574
- console.print("[magenta]Switched to tasks mode[/magenta]")
575
-
576
588
  elif command == "/code":
577
589
  current_mode = AgentMode.CODE
578
590
  console.print("[green]Switched to code mode[/green]")
@@ -702,14 +714,13 @@ def _run_interactive(
702
714
  except Exception:
703
715
  pass
704
716
 
717
+ # Welcome banner
705
718
  console.print()
706
- console.print(f"[bold cyan]EmDash Agent[/bold cyan] [dim]v{__version__}[/dim]")
707
- console.print(f"Mode: [bold]{current_mode.value}[/bold] | Model: [dim]{model or 'default'}[/dim]")
719
+ console.print(f"[bold cyan]Emdash Code[/bold cyan] [dim]v{__version__}[/dim]")
708
720
  if git_repo:
709
- console.print(f"Repo: [bold green]{git_repo}[/bold green] | Path: [dim]{cwd}[/dim]")
721
+ 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'}")
710
722
  else:
711
- console.print(f"Path: [dim]{cwd}[/dim]")
712
- console.print("Type your task or [cyan]/help[/cyan] for commands. Use Ctrl+C to exit.")
723
+ console.print(f"[dim]Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {model or 'default'}")
713
724
  console.print()
714
725
 
715
726
  while True:
@@ -738,18 +749,27 @@ def _run_interactive(
738
749
 
739
750
  # Run agent with current mode
740
751
  try:
752
+ # Prepare images for API call
753
+ images_to_send = attached_images if attached_images else None
754
+
741
755
  if session_id:
742
- stream = client.agent_continue_stream(session_id, user_input)
756
+ stream = client.agent_continue_stream(
757
+ session_id, user_input, images=images_to_send
758
+ )
743
759
  else:
744
760
  stream = client.agent_chat_stream(
745
761
  message=user_input,
746
762
  model=model,
747
763
  max_iterations=max_iterations,
748
764
  options=request_options,
765
+ images=images_to_send,
749
766
  )
750
767
 
768
+ # Clear attached images after sending
769
+ attached_images = []
770
+
751
771
  # Render the stream and capture any spec output
752
- result = renderer.render_stream(stream)
772
+ result = _render_with_interrupt(renderer, stream)
753
773
 
754
774
  # Check if we got a session ID back
755
775
  if result and result.get("session_id"):
@@ -766,108 +786,45 @@ def _run_interactive(
766
786
  if response:
767
787
  # Continue session with user's choice
768
788
  stream = client.agent_continue_stream(session_id, response)
769
- result = renderer.render_stream(stream)
789
+ result = _render_with_interrupt(renderer, stream)
770
790
 
771
- # Update mode if user chose tasks or code
772
- if "tasks" in response.lower():
773
- current_mode = AgentMode.TASKS
774
- elif "code" in response.lower():
791
+ # Update mode if user chose code
792
+ if "code" in response.lower():
775
793
  current_mode = AgentMode.CODE
776
794
 
777
795
  # Handle plan mode completion (show approval menu)
778
- # In plan mode, show menu after any substantial response
796
+ # Only show menu when agent explicitly submits a plan via exit_plan tool
779
797
  content = result.get("content", "")
798
+ plan_submitted = result.get("plan_submitted")
780
799
  should_show_plan_menu = (
781
800
  current_mode == AgentMode.PLAN and
782
801
  session_id and
783
- len(content) > 100 # Has substantial content
802
+ plan_submitted is not None # Agent called exit_plan tool
784
803
  )
785
804
  if should_show_plan_menu:
786
- choice, feedback = _show_spec_approval_menu()
787
-
788
- if choice == "tasks":
789
- current_mode = AgentMode.TASKS
790
- # Include the spec content explicitly in the message
791
- tasks_prompt = f"""Generate implementation tasks from this approved specification.
792
-
793
- ## Approved Specification
794
-
795
- {content}
796
-
797
- ## Your Task
798
-
799
- Use your tools to explore the codebase and create implementation tasks:
800
-
801
- 1. **Explore**: Use semantic_search and code graph tools to find:
802
- - Existing related code to modify
803
- - Patterns and conventions used in the codebase
804
- - Files that will need changes
805
-
806
- 2. **Generate Tasks**: Create detailed tasks that include:
807
- - Task ID (T1, T2, T3...)
808
- - Description of what to implement
809
- - Specific files to modify (based on your exploration)
810
- - Dependencies on other tasks
811
- - Complexity estimate (S/M/L)
812
- - Acceptance criteria
813
-
814
- 3. **Order**: Arrange tasks in implementation order, starting with foundational changes.
815
-
816
- Output a comprehensive task list that a developer can follow step-by-step."""
817
- stream = client.agent_continue_stream(
818
- session_id,
819
- tasks_prompt
820
- )
821
- result = renderer.render_stream(stream)
822
- # After generating tasks, show tasks menu
823
- content = result.get("content", "")
824
- elif choice == "code":
825
- current_mode = AgentMode.CODE
826
- stream = client.agent_continue_stream(
827
- session_id,
828
- "Start implementing the approved spec."
829
- )
830
- renderer.render_stream(stream)
831
- elif choice == "refine":
832
- stream = client.agent_continue_stream(
833
- session_id,
834
- f"Please update the spec based on this feedback: {feedback}"
835
- )
836
- renderer.render_stream(stream)
837
- elif choice == "abort":
838
- console.print("[dim]Spec discarded[/dim]")
839
- session_id = None
840
- current_spec = None
841
-
842
- # Handle tasks mode completion (show tasks approval menu)
843
- should_show_tasks_menu = (
844
- current_mode == AgentMode.TASKS and
845
- session_id and
846
- len(content) > 100 # Has substantial content
847
- )
848
- if should_show_tasks_menu:
849
- choice, feedback = _show_tasks_approval_menu()
805
+ choice, feedback = _show_plan_approval_menu()
850
806
 
851
- if choice == "code":
807
+ if choice == "approve":
852
808
  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
853
812
  stream = client.agent_continue_stream(
854
813
  session_id,
855
- "Start implementing the first task from the task list."
856
- )
857
- renderer.render_stream(stream)
858
- elif choice == "refine":
859
- stream = client.agent_continue_stream(
860
- session_id,
861
- f"Please update the tasks based on this feedback: {feedback}"
814
+ "The plan has been approved. Start implementing it now."
862
815
  )
863
- renderer.render_stream(stream)
864
- elif choice == "export":
865
- console.print("[dim]Exporting tasks...[/dim]")
866
- # TODO: Implement export functionality
867
- console.print("[yellow]Export not implemented yet[/yellow]")
868
- elif choice == "abort":
869
- console.print("[dim]Tasks discarded[/dim]")
870
- session_id = None
816
+ _render_with_interrupt(renderer, stream)
817
+ elif choice == "reject":
818
+ 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
+ )
823
+ _render_with_interrupt(renderer, stream)
824
+ else:
825
+ console.print("[dim]Plan rejected[/dim]")
826
+ session_id = None
827
+ current_spec = None
871
828
 
872
829
  console.print()
873
830