emdash-cli 0.1.4__py3-none-any.whl → 0.1.25__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]")
@@ -459,6 +458,7 @@ def _run_interactive(
459
458
  from prompt_toolkit.history import FileHistory
460
459
  from prompt_toolkit.completion import Completer, Completion
461
460
  from prompt_toolkit.styles import Style
461
+ from prompt_toolkit.key_binding import KeyBindings
462
462
  from pathlib import Path
463
463
 
464
464
  # Current mode
@@ -469,7 +469,6 @@ def _run_interactive(
469
469
  # Style for prompt
470
470
  PROMPT_STYLE = Style.from_dict({
471
471
  "prompt.mode.plan": "#ffcc00 bold",
472
- "prompt.mode.tasks": "#cc66ff bold",
473
472
  "prompt.mode.code": "#00cc66 bold",
474
473
  "prompt.prefix": "#888888",
475
474
  "completion-menu": "bg:#1a1a2e #ffffff",
@@ -503,18 +502,35 @@ def _run_interactive(
503
502
  history_file.parent.mkdir(parents=True, exist_ok=True)
504
503
  history = FileHistory(str(history_file))
505
504
 
505
+ # Key bindings: Enter submits, Alt+Enter inserts newline
506
+ # Note: Shift+Enter is indistinguishable from Enter in most terminals
507
+ kb = KeyBindings()
508
+
509
+ @kb.add("enter")
510
+ def submit_on_enter(event):
511
+ """Submit on Enter."""
512
+ event.current_buffer.validate_and_handle()
513
+
514
+ @kb.add("escape", "enter") # Alt+Enter (Escape then Enter)
515
+ @kb.add("c-j") # Ctrl+J as alternative for newline
516
+ def insert_newline_alt(event):
517
+ """Insert a newline character with Alt+Enter or Ctrl+J."""
518
+ event.current_buffer.insert_text("\n")
519
+
506
520
  session = PromptSession(
507
521
  history=history,
508
522
  completer=SlashCommandCompleter(),
509
523
  style=PROMPT_STYLE,
510
524
  complete_while_typing=True,
525
+ multiline=True,
526
+ prompt_continuation="... ",
527
+ key_bindings=kb,
511
528
  )
512
529
 
513
530
  def get_prompt():
514
531
  """Get formatted prompt based on current mode."""
515
532
  mode_colors = {
516
533
  AgentMode.PLAN: "class:prompt.mode.plan",
517
- AgentMode.TASKS: "class:prompt.mode.tasks",
518
534
  AgentMode.CODE: "class:prompt.mode.code",
519
535
  }
520
536
  mode_name = current_mode.value
@@ -550,10 +566,6 @@ def _run_interactive(
550
566
  current_mode = AgentMode.PLAN
551
567
  console.print("[yellow]Switched to plan mode[/yellow]")
552
568
 
553
- elif command == "/tasks":
554
- current_mode = AgentMode.TASKS
555
- console.print("[magenta]Switched to tasks mode[/magenta]")
556
-
557
569
  elif command == "/code":
558
570
  current_mode = AgentMode.CODE
559
571
  console.print("[green]Switched to code mode[/green]")
@@ -730,7 +742,7 @@ def _run_interactive(
730
742
  )
731
743
 
732
744
  # Render the stream and capture any spec output
733
- result = renderer.render_stream(stream)
745
+ result = _render_with_interrupt(renderer, stream)
734
746
 
735
747
  # Check if we got a session ID back
736
748
  if result and result.get("session_id"):
@@ -747,108 +759,45 @@ def _run_interactive(
747
759
  if response:
748
760
  # Continue session with user's choice
749
761
  stream = client.agent_continue_stream(session_id, response)
750
- result = renderer.render_stream(stream)
762
+ result = _render_with_interrupt(renderer, stream)
751
763
 
752
- # Update mode if user chose tasks or code
753
- if "tasks" in response.lower():
754
- current_mode = AgentMode.TASKS
755
- elif "code" in response.lower():
764
+ # Update mode if user chose code
765
+ if "code" in response.lower():
756
766
  current_mode = AgentMode.CODE
757
767
 
758
768
  # Handle plan mode completion (show approval menu)
759
- # In plan mode, show menu after any substantial response
769
+ # Only show menu when agent explicitly submits a plan via exit_plan tool
760
770
  content = result.get("content", "")
771
+ plan_submitted = result.get("plan_submitted")
761
772
  should_show_plan_menu = (
762
773
  current_mode == AgentMode.PLAN and
763
774
  session_id and
764
- len(content) > 100 # Has substantial content
775
+ plan_submitted is not None # Agent called exit_plan tool
765
776
  )
766
777
  if should_show_plan_menu:
767
- choice, feedback = _show_spec_approval_menu()
768
-
769
- if choice == "tasks":
770
- current_mode = AgentMode.TASKS
771
- # Include the spec content explicitly in the message
772
- tasks_prompt = f"""Generate implementation tasks from this approved specification.
773
-
774
- ## Approved Specification
775
-
776
- {content}
777
-
778
- ## Your Task
779
-
780
- Use your tools to explore the codebase and create implementation tasks:
781
-
782
- 1. **Explore**: Use semantic_search and code graph tools to find:
783
- - Existing related code to modify
784
- - Patterns and conventions used in the codebase
785
- - Files that will need changes
786
-
787
- 2. **Generate Tasks**: Create detailed tasks that include:
788
- - Task ID (T1, T2, T3...)
789
- - Description of what to implement
790
- - Specific files to modify (based on your exploration)
791
- - Dependencies on other tasks
792
- - Complexity estimate (S/M/L)
793
- - Acceptance criteria
794
-
795
- 3. **Order**: Arrange tasks in implementation order, starting with foundational changes.
796
-
797
- Output a comprehensive task list that a developer can follow step-by-step."""
798
- stream = client.agent_continue_stream(
799
- session_id,
800
- tasks_prompt
801
- )
802
- result = renderer.render_stream(stream)
803
- # After generating tasks, show tasks menu
804
- content = result.get("content", "")
805
- elif choice == "code":
806
- current_mode = AgentMode.CODE
807
- stream = client.agent_continue_stream(
808
- session_id,
809
- "Start implementing the approved spec."
810
- )
811
- renderer.render_stream(stream)
812
- elif choice == "refine":
813
- stream = client.agent_continue_stream(
814
- session_id,
815
- f"Please update the spec based on this feedback: {feedback}"
816
- )
817
- renderer.render_stream(stream)
818
- elif choice == "abort":
819
- console.print("[dim]Spec discarded[/dim]")
820
- session_id = None
821
- current_spec = None
822
-
823
- # Handle tasks mode completion (show tasks approval menu)
824
- should_show_tasks_menu = (
825
- current_mode == AgentMode.TASKS and
826
- session_id and
827
- len(content) > 100 # Has substantial content
828
- )
829
- if should_show_tasks_menu:
830
- choice, feedback = _show_tasks_approval_menu()
778
+ choice, feedback = _show_plan_approval_menu()
831
779
 
832
- if choice == "code":
780
+ if choice == "approve":
833
781
  current_mode = AgentMode.CODE
782
+ # Reset mode state to CODE
783
+ from emdash_core.agent.tools.modes import ModeState, AgentMode as CoreMode
784
+ ModeState.get_instance().current_mode = CoreMode.CODE
834
785
  stream = client.agent_continue_stream(
835
786
  session_id,
836
- "Start implementing the first task from the task list."
837
- )
838
- renderer.render_stream(stream)
839
- elif choice == "refine":
840
- stream = client.agent_continue_stream(
841
- session_id,
842
- f"Please update the tasks based on this feedback: {feedback}"
787
+ "The plan has been approved. Start implementing it now."
843
788
  )
844
- renderer.render_stream(stream)
845
- elif choice == "export":
846
- console.print("[dim]Exporting tasks...[/dim]")
847
- # TODO: Implement export functionality
848
- console.print("[yellow]Export not implemented yet[/yellow]")
849
- elif choice == "abort":
850
- console.print("[dim]Tasks discarded[/dim]")
851
- session_id = None
789
+ _render_with_interrupt(renderer, stream)
790
+ elif choice == "reject":
791
+ if feedback:
792
+ stream = client.agent_continue_stream(
793
+ session_id,
794
+ f"The plan was rejected. Please revise based on this feedback: {feedback}"
795
+ )
796
+ _render_with_interrupt(renderer, stream)
797
+ else:
798
+ console.print("[dim]Plan rejected[/dim]")
799
+ session_id = None
800
+ current_spec = None
852
801
 
853
802
  console.print()
854
803