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