emdash-cli 0.1.35__py3-none-any.whl → 0.1.67__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.
Files changed (50) hide show
  1. emdash_cli/client.py +41 -22
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/__init__.py +2 -2
  4. emdash_cli/commands/agent/__init__.py +14 -0
  5. emdash_cli/commands/agent/cli.py +100 -0
  6. emdash_cli/commands/agent/constants.py +63 -0
  7. emdash_cli/commands/agent/file_utils.py +178 -0
  8. emdash_cli/commands/agent/handlers/__init__.py +51 -0
  9. emdash_cli/commands/agent/handlers/agents.py +449 -0
  10. emdash_cli/commands/agent/handlers/auth.py +69 -0
  11. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  12. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  13. emdash_cli/commands/agent/handlers/index.py +183 -0
  14. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  15. emdash_cli/commands/agent/handlers/misc.py +319 -0
  16. emdash_cli/commands/agent/handlers/registry.py +72 -0
  17. emdash_cli/commands/agent/handlers/rules.py +411 -0
  18. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  19. emdash_cli/commands/agent/handlers/setup.py +715 -0
  20. emdash_cli/commands/agent/handlers/skills.py +478 -0
  21. emdash_cli/commands/agent/handlers/telegram.py +475 -0
  22. emdash_cli/commands/agent/handlers/todos.py +119 -0
  23. emdash_cli/commands/agent/handlers/verify.py +653 -0
  24. emdash_cli/commands/agent/help.py +236 -0
  25. emdash_cli/commands/agent/interactive.py +842 -0
  26. emdash_cli/commands/agent/menus.py +760 -0
  27. emdash_cli/commands/agent/onboarding.py +619 -0
  28. emdash_cli/commands/agent/session_restore.py +210 -0
  29. emdash_cli/commands/agent.py +7 -1321
  30. emdash_cli/commands/index.py +111 -13
  31. emdash_cli/commands/registry.py +635 -0
  32. emdash_cli/commands/server.py +99 -40
  33. emdash_cli/commands/skills.py +72 -6
  34. emdash_cli/design.py +328 -0
  35. emdash_cli/diff_renderer.py +438 -0
  36. emdash_cli/integrations/__init__.py +1 -0
  37. emdash_cli/integrations/telegram/__init__.py +15 -0
  38. emdash_cli/integrations/telegram/bot.py +402 -0
  39. emdash_cli/integrations/telegram/bridge.py +865 -0
  40. emdash_cli/integrations/telegram/config.py +155 -0
  41. emdash_cli/integrations/telegram/formatter.py +385 -0
  42. emdash_cli/main.py +52 -2
  43. emdash_cli/server_manager.py +70 -10
  44. emdash_cli/sse_renderer.py +659 -167
  45. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
  46. emdash_cli-0.1.67.dist-info/RECORD +63 -0
  47. emdash_cli/commands/swarm.py +0 -86
  48. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  49. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
  50. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,760 @@
1
+ """Interactive menus for the agent CLI.
2
+
3
+ Contains all prompt_toolkit-based interactive menus with zen design language.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+
10
+ from ...design import (
11
+ Colors,
12
+ STATUS_ACTIVE,
13
+ STATUS_INACTIVE,
14
+ STATUS_ERROR,
15
+ DOT_BULLET,
16
+ ARROW_PROMPT,
17
+ header,
18
+ footer,
19
+ menu_hint,
20
+ )
21
+
22
+ console = Console()
23
+
24
+
25
+ def get_clarification_response(clarification: dict) -> str | None:
26
+ """Get user response for clarification with interactive selection.
27
+
28
+ Args:
29
+ clarification: Dict with question, context, and options
30
+
31
+ Returns:
32
+ User's selected option or typed response, or None if cancelled
33
+ """
34
+ from prompt_toolkit import Application, PromptSession
35
+ from prompt_toolkit.key_binding import KeyBindings
36
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
37
+ from prompt_toolkit.styles import Style
38
+
39
+ options = clarification.get("options", [])
40
+
41
+ if not options:
42
+ # No options, just get free-form input
43
+ session = PromptSession()
44
+ try:
45
+ return session.prompt("response > ").strip() or None
46
+ except (KeyboardInterrupt, EOFError):
47
+ return None
48
+
49
+ selected_index = [0]
50
+ result = [None]
51
+
52
+ # Key bindings
53
+ kb = KeyBindings()
54
+
55
+ @kb.add("up")
56
+ @kb.add("k")
57
+ def move_up(event):
58
+ selected_index[0] = (selected_index[0] - 1) % len(options)
59
+
60
+ @kb.add("down")
61
+ @kb.add("j")
62
+ def move_down(event):
63
+ selected_index[0] = (selected_index[0] + 1) % len(options)
64
+
65
+ @kb.add("enter")
66
+ def select(event):
67
+ result[0] = options[selected_index[0]]
68
+ event.app.exit()
69
+
70
+ # Number key shortcuts (1-9)
71
+ for i in range(min(9, len(options))):
72
+ @kb.add(str(i + 1))
73
+ def select_by_number(event, idx=i):
74
+ result[0] = options[idx]
75
+ event.app.exit()
76
+
77
+ @kb.add("c-c")
78
+ @kb.add("escape")
79
+ def cancel(event):
80
+ result[0] = None
81
+ event.app.exit()
82
+
83
+ @kb.add("o") # 'o' for Other - custom input
84
+ def other_input(event):
85
+ result[0] = "OTHER_INPUT"
86
+ event.app.exit()
87
+
88
+ def get_formatted_options():
89
+ lines = []
90
+ for i, opt in enumerate(options):
91
+ if i == selected_index[0]:
92
+ lines.append(("class:selected", f" {STATUS_ACTIVE} "))
93
+ lines.append(("class:selected", f"{i+1}. {opt}\n"))
94
+ else:
95
+ lines.append(("class:option", f" {STATUS_INACTIVE} "))
96
+ lines.append(("class:option", f"{i+1}. {opt}\n"))
97
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} ↑↓ move Enter select 1-9 quick o other"))
98
+ return lines
99
+
100
+ # Style (zen palette)
101
+ style = Style.from_dict({
102
+ "selected": f"{Colors.SUCCESS} bold",
103
+ "option": Colors.MUTED,
104
+ "hint": f"{Colors.DIM} italic",
105
+ })
106
+
107
+ # Calculate height based on options
108
+ height = len(options) + 2 # options + hint line + padding
109
+
110
+ # Layout
111
+ layout = Layout(
112
+ HSplit([
113
+ Window(
114
+ FormattedTextControl(get_formatted_options),
115
+ height=height,
116
+ ),
117
+ ])
118
+ )
119
+
120
+ # Application
121
+ app = Application(
122
+ layout=layout,
123
+ key_bindings=kb,
124
+ style=style,
125
+ full_screen=False,
126
+ )
127
+
128
+ console.print()
129
+
130
+ try:
131
+ app.run()
132
+ except (KeyboardInterrupt, EOFError):
133
+ return None
134
+
135
+ # Handle "other" option - get custom input
136
+ if result[0] == "OTHER_INPUT":
137
+ session = PromptSession()
138
+ console.print()
139
+ try:
140
+ return session.prompt("response > ").strip() or None
141
+ except (KeyboardInterrupt, EOFError):
142
+ return None
143
+
144
+ # Check if selected option is an "other/explain" type that needs text input
145
+ if result[0]:
146
+ lower_result = result[0].lower()
147
+ needs_input = any(phrase in lower_result for phrase in [
148
+ "something else",
149
+ "other",
150
+ "i'll explain",
151
+ "i will explain",
152
+ "let me explain",
153
+ "custom",
154
+ "none of the above",
155
+ ])
156
+ if needs_input:
157
+ session = PromptSession()
158
+ console.print()
159
+ console.print("[dim]Please explain:[/dim]")
160
+ try:
161
+ custom_input = session.prompt("response > ").strip()
162
+ if custom_input:
163
+ return custom_input
164
+ except (KeyboardInterrupt, EOFError):
165
+ return None
166
+
167
+ return result[0]
168
+
169
+
170
+ def show_plan_approval_menu() -> tuple[str, str]:
171
+ """Show plan approval menu with simple approve/feedback options.
172
+
173
+ Returns:
174
+ Tuple of (choice, user_feedback) where user_feedback is only set for 'feedback'
175
+ """
176
+ from prompt_toolkit import Application, PromptSession
177
+ from prompt_toolkit.key_binding import KeyBindings
178
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
179
+ from prompt_toolkit.styles import Style
180
+
181
+ options = [
182
+ ("approve", "Approve and start implementation"),
183
+ ("feedback", "Provide feedback on the plan"),
184
+ ]
185
+
186
+ selected_index = [0] # Use list to allow mutation in closure
187
+ result = [None]
188
+
189
+ # Key bindings
190
+ kb = KeyBindings()
191
+
192
+ @kb.add("up")
193
+ @kb.add("k")
194
+ def move_up(event):
195
+ selected_index[0] = (selected_index[0] - 1) % len(options)
196
+
197
+ @kb.add("down")
198
+ @kb.add("j")
199
+ def move_down(event):
200
+ selected_index[0] = (selected_index[0] + 1) % len(options)
201
+
202
+ @kb.add("enter")
203
+ def select(event):
204
+ result[0] = options[selected_index[0]][0]
205
+ event.app.exit()
206
+
207
+ @kb.add("1")
208
+ @kb.add("y")
209
+ def select_approve(event):
210
+ result[0] = "approve"
211
+ event.app.exit()
212
+
213
+ @kb.add("2")
214
+ @kb.add("n")
215
+ def select_feedback(event):
216
+ result[0] = "feedback"
217
+ event.app.exit()
218
+
219
+ @kb.add("c-c")
220
+ @kb.add("q")
221
+ @kb.add("escape")
222
+ def cancel(event):
223
+ result[0] = "feedback"
224
+ event.app.exit()
225
+
226
+ def get_formatted_options():
227
+ lines = [("class:title", "Approve this plan?\n\n")]
228
+ for i, (key, desc) in enumerate(options):
229
+ if i == selected_index[0]:
230
+ lines.append(("class:selected", f" {STATUS_ACTIVE} {key}\n"))
231
+ lines.append(("class:selected-desc", f" {desc}\n"))
232
+ else:
233
+ lines.append(("class:option", f" {STATUS_INACTIVE} {key}\n"))
234
+ lines.append(("class:desc", f" {desc}\n"))
235
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} y approve n feedback Esc cancel"))
236
+ return lines
237
+
238
+ # Style (zen palette)
239
+ style = Style.from_dict({
240
+ "title": f"{Colors.PRIMARY} bold",
241
+ "selected": f"{Colors.SUCCESS} bold",
242
+ "selected-desc": Colors.SUCCESS,
243
+ "option": Colors.MUTED,
244
+ "desc": Colors.DIM,
245
+ "hint": f"{Colors.DIM} italic",
246
+ })
247
+
248
+ # Layout
249
+ layout = Layout(
250
+ HSplit([
251
+ Window(
252
+ FormattedTextControl(get_formatted_options),
253
+ height=6,
254
+ ),
255
+ ])
256
+ )
257
+
258
+ # Application
259
+ app = Application(
260
+ layout=layout,
261
+ key_bindings=kb,
262
+ style=style,
263
+ full_screen=False,
264
+ )
265
+
266
+ console.print()
267
+
268
+ try:
269
+ app.run()
270
+ except (KeyboardInterrupt, EOFError):
271
+ result[0] = "feedback"
272
+
273
+ choice = result[0] or "feedback"
274
+
275
+ # Get feedback if feedback was chosen
276
+ user_feedback = ""
277
+ if choice == "feedback":
278
+ console.print()
279
+ console.print("[dim]What changes would you like?[/dim]")
280
+ try:
281
+ session = PromptSession()
282
+ user_feedback = session.prompt("feedback > ").strip()
283
+ except (KeyboardInterrupt, EOFError):
284
+ return "feedback", ""
285
+
286
+ return choice, user_feedback
287
+
288
+
289
+ def show_agents_interactive_menu() -> tuple[str, str]:
290
+ """Show interactive agents menu.
291
+
292
+ Returns:
293
+ Tuple of (action, agent_name) where action is one of:
294
+ - 'view': View agent details
295
+ - 'create': Create new agent
296
+ - 'delete': Delete agent
297
+ - 'edit': Edit agent file
298
+ - 'cancel': User cancelled
299
+ """
300
+ from prompt_toolkit import Application
301
+ from prompt_toolkit.key_binding import KeyBindings
302
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
303
+ from prompt_toolkit.styles import Style
304
+ from emdash_core.agent.toolkits import list_agent_types, get_custom_agent
305
+
306
+ # Get all agents
307
+ all_agents = list_agent_types(Path.cwd())
308
+ builtin = ["Explore", "Plan"]
309
+ custom = [a for a in all_agents if a not in builtin]
310
+
311
+ # Build menu items: each is (name, description, is_builtin, is_action)
312
+ menu_items = []
313
+
314
+ # Add built-in agents
315
+ menu_items.append(("Explore", "Fast codebase exploration (read-only)", True, False))
316
+ menu_items.append(("Plan", "Design implementation plans", True, False))
317
+
318
+ # Add custom agents
319
+ for name in custom:
320
+ agent = get_custom_agent(name, Path.cwd())
321
+ desc = agent.description if agent else "Custom agent"
322
+ menu_items.append((name, desc, False, False))
323
+
324
+ # Add action items at the bottom
325
+ menu_items.append(("+ Create New Agent", "Create a new custom agent", False, True))
326
+
327
+ selected_index = [0]
328
+ result = [("cancel", "")]
329
+
330
+ kb = KeyBindings()
331
+
332
+ @kb.add("up")
333
+ @kb.add("k")
334
+ def move_up(event):
335
+ selected_index[0] = (selected_index[0] - 1) % len(menu_items)
336
+
337
+ @kb.add("down")
338
+ @kb.add("j")
339
+ def move_down(event):
340
+ selected_index[0] = (selected_index[0] + 1) % len(menu_items)
341
+
342
+ @kb.add("enter")
343
+ def select(event):
344
+ item = menu_items[selected_index[0]]
345
+ name, desc, is_builtin, is_action = item
346
+ if is_action:
347
+ if "Create" in name:
348
+ result[0] = ("create", "")
349
+ else:
350
+ result[0] = ("view", name)
351
+ event.app.exit()
352
+
353
+ @kb.add("d")
354
+ def delete_agent(event):
355
+ item = menu_items[selected_index[0]]
356
+ name, desc, is_builtin, is_action = item
357
+ if not is_builtin and not is_action:
358
+ result[0] = ("delete", name)
359
+ event.app.exit()
360
+
361
+ @kb.add("e")
362
+ def edit_agent(event):
363
+ item = menu_items[selected_index[0]]
364
+ name, desc, is_builtin, is_action = item
365
+ if not is_builtin and not is_action:
366
+ result[0] = ("edit", name)
367
+ event.app.exit()
368
+
369
+ @kb.add("n")
370
+ def new_agent(event):
371
+ result[0] = ("create", "")
372
+ event.app.exit()
373
+
374
+ @kb.add("c-c")
375
+ @kb.add("escape")
376
+ @kb.add("q")
377
+ def cancel(event):
378
+ result[0] = ("cancel", "")
379
+ event.app.exit()
380
+
381
+ def get_formatted_menu():
382
+ lines = [("class:title", "Agents\n\n")]
383
+
384
+ for i, (name, desc, is_builtin, is_action) in enumerate(menu_items):
385
+ is_selected = i == selected_index[0]
386
+ indicator = STATUS_ACTIVE if is_selected else STATUS_INACTIVE
387
+
388
+ if is_action:
389
+ # Action item (like Create New)
390
+ if is_selected:
391
+ lines.append(("class:action-selected", f" {indicator} {name}\n"))
392
+ else:
393
+ lines.append(("class:action", f" {indicator} {name}\n"))
394
+ elif is_builtin:
395
+ # Built-in agent
396
+ if is_selected:
397
+ lines.append(("class:builtin-selected", f" {indicator} {name}\n"))
398
+ lines.append(("class:desc-selected", f" {desc}\n"))
399
+ else:
400
+ lines.append(("class:builtin", f" {indicator} {name}\n"))
401
+ lines.append(("class:desc", f" {desc}\n"))
402
+ else:
403
+ # Custom agent
404
+ if is_selected:
405
+ lines.append(("class:custom-selected", f" {indicator} {name}\n"))
406
+ lines.append(("class:desc-selected", f" {desc}\n"))
407
+ else:
408
+ lines.append(("class:custom", f" {indicator} {name}\n"))
409
+ lines.append(("class:desc", f" {desc}\n"))
410
+
411
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} ↑↓ navigate Enter view n new e edit d delete q quit"))
412
+ return lines
413
+
414
+ # Style (zen palette)
415
+ style = Style.from_dict({
416
+ "title": f"{Colors.PRIMARY} bold",
417
+ "builtin": Colors.MUTED,
418
+ "builtin-selected": f"{Colors.SUCCESS} bold",
419
+ "custom": Colors.PRIMARY,
420
+ "custom-selected": f"{Colors.SUCCESS} bold",
421
+ "action": Colors.WARNING,
422
+ "action-selected": f"{Colors.WARNING} bold",
423
+ "desc": Colors.DIM,
424
+ "desc-selected": Colors.SUCCESS,
425
+ "hint": f"{Colors.DIM} italic",
426
+ })
427
+
428
+ height = len(menu_items) + 4 # items + title + hint + padding
429
+
430
+ layout = Layout(
431
+ HSplit([
432
+ Window(
433
+ FormattedTextControl(get_formatted_menu),
434
+ height=height,
435
+ ),
436
+ ])
437
+ )
438
+
439
+ app = Application(
440
+ layout=layout,
441
+ key_bindings=kb,
442
+ style=style,
443
+ full_screen=False,
444
+ )
445
+
446
+ console.print()
447
+
448
+ try:
449
+ app.run()
450
+ except (KeyboardInterrupt, EOFError):
451
+ result[0] = ("cancel", "")
452
+
453
+ return result[0]
454
+
455
+
456
+ def prompt_agent_name() -> str:
457
+ """Prompt user for new agent name with zen styling."""
458
+ from prompt_toolkit import PromptSession
459
+
460
+ console.print()
461
+ console.print(f"[{Colors.MUTED}]{header('Create Agent', 35)}[/{Colors.MUTED}]")
462
+ console.print()
463
+ console.print(f" [{Colors.DIM}]Enter a name for your agent[/{Colors.DIM}]")
464
+ console.print(f" [{Colors.DIM}](e.g., code-reviewer, bug-finder)[/{Colors.DIM}]")
465
+ console.print()
466
+
467
+ try:
468
+ session = PromptSession()
469
+ name = session.prompt(f" {ARROW_PROMPT} ").strip()
470
+ return name.lower().replace(" ", "-") if name else ""
471
+ except (KeyboardInterrupt, EOFError):
472
+ return ""
473
+
474
+
475
+ def confirm_delete(agent_name: str) -> bool:
476
+ """Confirm agent deletion with zen styling."""
477
+ from prompt_toolkit import PromptSession
478
+
479
+ console.print()
480
+ console.print(f"[{Colors.MUTED}]{header('Delete Agent', 35)}[/{Colors.MUTED}]")
481
+ console.print()
482
+ console.print(f" [{Colors.ERROR}]{STATUS_ERROR}[/{Colors.ERROR}] This will permanently delete:")
483
+ console.print()
484
+ console.print(f" [{Colors.WARNING}]{agent_name}[/{Colors.WARNING}]")
485
+ console.print()
486
+ console.print(f" [{Colors.DIM}]Type 'delete' to confirm[/{Colors.DIM}]")
487
+ console.print()
488
+
489
+ try:
490
+ session = PromptSession()
491
+ response = session.prompt(f" {ARROW_PROMPT} ").strip().lower()
492
+ return response == "delete"
493
+ except (KeyboardInterrupt, EOFError):
494
+ return False
495
+
496
+
497
+ def show_sessions_interactive_menu(sessions: list, active_session: str | None) -> tuple[str, str]:
498
+ """Show interactive sessions menu.
499
+
500
+ Args:
501
+ sessions: List of SessionInfo objects
502
+ active_session: Name of currently active session
503
+
504
+ Returns:
505
+ Tuple of (action, session_name) where action is one of:
506
+ - 'load': Load the session
507
+ - 'delete': Delete the session
508
+ - 'cancel': User cancelled
509
+ """
510
+ from prompt_toolkit import Application
511
+ from prompt_toolkit.key_binding import KeyBindings
512
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
513
+ from prompt_toolkit.styles import Style
514
+
515
+ if not sessions:
516
+ return ("cancel", "")
517
+
518
+ # Build menu items: (name, summary, mode, message_count, updated_at, is_active)
519
+ menu_items = []
520
+ for s in sessions:
521
+ is_active = active_session == s.name
522
+ menu_items.append((s.name, s.summary or "", s.mode, s.message_count, s.updated_at, is_active))
523
+
524
+ selected_index = [0]
525
+ result = [("cancel", "")]
526
+
527
+ kb = KeyBindings()
528
+
529
+ @kb.add("up")
530
+ @kb.add("k")
531
+ def move_up(event):
532
+ selected_index[0] = (selected_index[0] - 1) % len(menu_items)
533
+
534
+ @kb.add("down")
535
+ @kb.add("j")
536
+ def move_down(event):
537
+ selected_index[0] = (selected_index[0] + 1) % len(menu_items)
538
+
539
+ @kb.add("enter")
540
+ def select(event):
541
+ item = menu_items[selected_index[0]]
542
+ result[0] = ("load", item[0])
543
+ event.app.exit()
544
+
545
+ @kb.add("d")
546
+ def delete_session(event):
547
+ item = menu_items[selected_index[0]]
548
+ result[0] = ("delete", item[0])
549
+ event.app.exit()
550
+
551
+ @kb.add("c-c")
552
+ @kb.add("escape")
553
+ @kb.add("q")
554
+ def cancel(event):
555
+ result[0] = ("cancel", "")
556
+ event.app.exit()
557
+
558
+ def get_formatted_menu():
559
+ lines = [("class:title", "Sessions\n\n")]
560
+
561
+ for i, (name, summary, mode, msg_count, updated, is_active) in enumerate(menu_items):
562
+ is_selected = i == selected_index[0]
563
+ indicator = STATUS_ACTIVE if is_selected else STATUS_INACTIVE
564
+ active_marker = f" {DOT_BULLET}" if is_active else ""
565
+
566
+ if is_selected:
567
+ lines.append(("class:name-selected", f" {indicator} {name}{active_marker}"))
568
+ lines.append(("class:mode-selected", f" [{mode}]"))
569
+ lines.append(("class:info-selected", f" {msg_count} msgs\n"))
570
+ if summary:
571
+ truncated = summary[:50] + "..." if len(summary) > 50 else summary
572
+ lines.append(("class:summary-selected", f" {truncated}\n"))
573
+ else:
574
+ lines.append(("class:name", f" {indicator} {name}{active_marker}"))
575
+ lines.append(("class:mode", f" [{mode}]"))
576
+ lines.append(("class:info", f" {msg_count} msgs\n"))
577
+ if summary:
578
+ truncated = summary[:50] + "..." if len(summary) > 50 else summary
579
+ lines.append(("class:summary", f" {truncated}\n"))
580
+
581
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} ↑↓ navigate Enter load d delete q quit"))
582
+ return lines
583
+
584
+ # Style (zen palette)
585
+ style = Style.from_dict({
586
+ "title": f"{Colors.PRIMARY} bold",
587
+ "name": Colors.PRIMARY,
588
+ "name-selected": f"{Colors.SUCCESS} bold",
589
+ "mode": Colors.MUTED,
590
+ "mode-selected": Colors.SUCCESS,
591
+ "info": Colors.DIM,
592
+ "info-selected": Colors.SUCCESS,
593
+ "summary": f"{Colors.DIM} italic",
594
+ "summary-selected": f"{Colors.SUCCESS} italic",
595
+ "hint": f"{Colors.DIM} italic",
596
+ })
597
+
598
+ height = len(menu_items) * 2 + 4 # items (with summaries) + title + hint + padding
599
+
600
+ layout = Layout(
601
+ HSplit([
602
+ Window(
603
+ FormattedTextControl(get_formatted_menu),
604
+ height=height,
605
+ ),
606
+ ])
607
+ )
608
+
609
+ app = Application(
610
+ layout=layout,
611
+ key_bindings=kb,
612
+ style=style,
613
+ full_screen=False,
614
+ )
615
+
616
+ console.print()
617
+
618
+ try:
619
+ app.run()
620
+ except (KeyboardInterrupt, EOFError):
621
+ result[0] = ("cancel", "")
622
+
623
+ return result[0]
624
+
625
+
626
+ def confirm_session_delete(session_name: str) -> bool:
627
+ """Confirm session deletion with zen styling."""
628
+ from prompt_toolkit import PromptSession
629
+
630
+ console.print()
631
+ console.print(f"[{Colors.MUTED}]{header('Delete Session', 35)}[/{Colors.MUTED}]")
632
+ console.print()
633
+ console.print(f" [{Colors.ERROR}]{STATUS_ERROR}[/{Colors.ERROR}] This will permanently delete:")
634
+ console.print()
635
+ console.print(f" [{Colors.WARNING}]{session_name}[/{Colors.WARNING}]")
636
+ console.print()
637
+ console.print(f" [{Colors.DIM}]Type 'delete' to confirm[/{Colors.DIM}]")
638
+ console.print()
639
+
640
+ try:
641
+ session = PromptSession()
642
+ response = session.prompt(f" {ARROW_PROMPT} ").strip().lower()
643
+ return response == "delete"
644
+ except (KeyboardInterrupt, EOFError):
645
+ return False
646
+
647
+
648
+ def show_plan_mode_approval_menu() -> tuple[str, str]:
649
+ """Show plan mode entry approval menu.
650
+
651
+ Returns:
652
+ Tuple of (choice, feedback) where feedback is only set for 'reject'
653
+ """
654
+ from prompt_toolkit import Application, PromptSession
655
+ from prompt_toolkit.key_binding import KeyBindings
656
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
657
+ from prompt_toolkit.styles import Style
658
+
659
+ options = [
660
+ ("approve", "Enter plan mode and explore"),
661
+ ("reject", "Skip planning, proceed directly"),
662
+ ]
663
+
664
+ selected_index = [0]
665
+ result = [None]
666
+
667
+ kb = KeyBindings()
668
+
669
+ @kb.add("up")
670
+ @kb.add("k")
671
+ def move_up(event):
672
+ selected_index[0] = (selected_index[0] - 1) % len(options)
673
+
674
+ @kb.add("down")
675
+ @kb.add("j")
676
+ def move_down(event):
677
+ selected_index[0] = (selected_index[0] + 1) % len(options)
678
+
679
+ @kb.add("enter")
680
+ def select(event):
681
+ result[0] = options[selected_index[0]][0]
682
+ event.app.exit()
683
+
684
+ @kb.add("1")
685
+ @kb.add("y")
686
+ def select_approve(event):
687
+ result[0] = "approve"
688
+ event.app.exit()
689
+
690
+ @kb.add("2")
691
+ @kb.add("n")
692
+ def select_reject(event):
693
+ result[0] = "reject"
694
+ event.app.exit()
695
+
696
+ @kb.add("c-c")
697
+ @kb.add("q")
698
+ @kb.add("escape")
699
+ def cancel(event):
700
+ result[0] = "reject"
701
+ event.app.exit()
702
+
703
+ def get_formatted_options():
704
+ lines = [("class:title", "Enter plan mode?\n\n")]
705
+ for i, (key, desc) in enumerate(options):
706
+ if i == selected_index[0]:
707
+ lines.append(("class:selected", f" {STATUS_ACTIVE} {key}\n"))
708
+ lines.append(("class:selected-desc", f" {desc}\n"))
709
+ else:
710
+ lines.append(("class:option", f" {STATUS_INACTIVE} {key}\n"))
711
+ lines.append(("class:desc", f" {desc}\n"))
712
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} y approve n skip Esc cancel"))
713
+ return lines
714
+
715
+ # Style (zen palette)
716
+ style = Style.from_dict({
717
+ "title": f"{Colors.WARNING} bold",
718
+ "selected": f"{Colors.SUCCESS} bold",
719
+ "selected-desc": Colors.SUCCESS,
720
+ "option": Colors.MUTED,
721
+ "desc": Colors.DIM,
722
+ "hint": f"{Colors.DIM} italic",
723
+ })
724
+
725
+ layout = Layout(
726
+ HSplit([
727
+ Window(
728
+ FormattedTextControl(get_formatted_options),
729
+ height=6,
730
+ ),
731
+ ])
732
+ )
733
+
734
+ app = Application(
735
+ layout=layout,
736
+ key_bindings=kb,
737
+ style=style,
738
+ full_screen=False,
739
+ )
740
+
741
+ console.print()
742
+
743
+ try:
744
+ app.run()
745
+ except (KeyboardInterrupt, EOFError):
746
+ result[0] = "reject"
747
+
748
+ choice = result[0] or "reject"
749
+
750
+ feedback = ""
751
+ if choice == "reject":
752
+ console.print()
753
+ console.print("[dim]Reason for skipping plan mode (optional):[/dim]")
754
+ try:
755
+ session = PromptSession()
756
+ feedback = session.prompt("feedback > ").strip()
757
+ except (KeyboardInterrupt, EOFError):
758
+ return "reject", ""
759
+
760
+ return choice, feedback