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.
- emdash_cli/client.py +41 -22
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +63 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +51 -0
- emdash_cli/commands/agent/handlers/agents.py +449 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +319 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +411 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +715 -0
- emdash_cli/commands/agent/handlers/skills.py +478 -0
- emdash_cli/commands/agent/handlers/telegram.py +475 -0
- emdash_cli/commands/agent/handlers/todos.py +119 -0
- emdash_cli/commands/agent/handlers/verify.py +653 -0
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +842 -0
- emdash_cli/commands/agent/menus.py +760 -0
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/agent.py +7 -1321
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- emdash_cli/commands/server.py +99 -40
- emdash_cli/commands/skills.py +72 -6
- emdash_cli/design.py +328 -0
- emdash_cli/diff_renderer.py +438 -0
- emdash_cli/integrations/__init__.py +1 -0
- emdash_cli/integrations/telegram/__init__.py +15 -0
- emdash_cli/integrations/telegram/bot.py +402 -0
- emdash_cli/integrations/telegram/bridge.py +865 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +385 -0
- emdash_cli/main.py +52 -2
- emdash_cli/server_manager.py +70 -10
- emdash_cli/sse_renderer.py +659 -167
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
- emdash_cli-0.1.67.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.35.dist-info/RECORD +0 -30
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
- {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
|