emdash-cli 0.1.46__py3-none-any.whl → 0.1.70__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 +12 -28
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/constants.py +78 -0
- emdash_cli/commands/agent/handlers/__init__.py +10 -0
- emdash_cli/commands/agent/handlers/agents.py +67 -39
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +119 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +48 -31
- emdash_cli/commands/agent/handlers/sessions.py +1 -1
- emdash_cli/commands/agent/handlers/setup.py +187 -54
- emdash_cli/commands/agent/handlers/skills.py +42 -4
- emdash_cli/commands/agent/handlers/telegram.py +523 -0
- emdash_cli/commands/agent/handlers/todos.py +55 -34
- emdash_cli/commands/agent/handlers/verify.py +10 -5
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +278 -47
- emdash_cli/commands/agent/menus.py +116 -84
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- 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 +980 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +392 -0
- emdash_cli/main.py +52 -2
- emdash_cli/sse_renderer.py +632 -171
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/METADATA +2 -2
- emdash_cli-0.1.70.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.46.dist-info/RECORD +0 -49
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/entry_points.txt +0 -0
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"""Handlers for miscellaneous slash commands."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
8
|
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from emdash_cli.design import Colors, EM_DASH
|
|
14
|
+
from emdash_cli.diff_renderer import render_diff
|
|
7
15
|
|
|
8
16
|
console = Console()
|
|
9
17
|
|
|
@@ -197,4 +205,115 @@ def handle_context(renderer) -> None:
|
|
|
197
205
|
console.print(f"\n[dim]No reranked items: {debug_info}[/dim]")
|
|
198
206
|
else:
|
|
199
207
|
console.print(f"\n[dim]No reranked items yet. Items appear after exploration (file reads, searches).[/dim]")
|
|
208
|
+
|
|
209
|
+
# Show full context frame as JSON
|
|
210
|
+
console.print(f"\n[bold]Full Context Frame:[/bold]")
|
|
211
|
+
context_json = json.dumps(context_data, indent=2, default=str)
|
|
212
|
+
syntax = Syntax(context_json, "json", theme="monokai", line_numbers=False)
|
|
213
|
+
console.print(syntax)
|
|
200
214
|
console.print()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def handle_compact(client, session_id: str | None) -> None:
|
|
218
|
+
"""Handle /compact command.
|
|
219
|
+
|
|
220
|
+
Manually triggers message history compaction using LLM summarization.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
client: EmdashClient instance
|
|
224
|
+
session_id: Current session ID (if any)
|
|
225
|
+
"""
|
|
226
|
+
if not session_id:
|
|
227
|
+
console.print("\n[yellow]No active session. Start a conversation first.[/yellow]\n")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
console.print("\n[bold cyan]Compacting message history...[/bold cyan]\n")
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
response = client.post(f"/api/agent/chat/{session_id}/compact")
|
|
234
|
+
|
|
235
|
+
if response.status_code == 404:
|
|
236
|
+
console.print("[yellow]Session not found.[/yellow]\n")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
if response.status_code != 200:
|
|
240
|
+
console.print(f"[red]Error: {response.text}[/red]\n")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
data = response.json()
|
|
244
|
+
|
|
245
|
+
if not data.get("compacted"):
|
|
246
|
+
reason = data.get("reason", "Unknown reason")
|
|
247
|
+
console.print(f"[yellow]Could not compact: {reason}[/yellow]\n")
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
# Show stats
|
|
251
|
+
original_msgs = data.get("original_message_count", 0)
|
|
252
|
+
new_msgs = data.get("new_message_count", 0)
|
|
253
|
+
original_tokens = data.get("original_tokens", 0)
|
|
254
|
+
new_tokens = data.get("new_tokens", 0)
|
|
255
|
+
reduction = data.get("reduction_percent", 0)
|
|
256
|
+
|
|
257
|
+
console.print("[green]✓ Compaction complete![/green]\n")
|
|
258
|
+
console.print(f"[bold]Messages:[/bold] {original_msgs} → {new_msgs}")
|
|
259
|
+
console.print(f"[bold]Tokens:[/bold] {original_tokens:,} → {new_tokens:,} ([green]-{reduction}%[/green])")
|
|
260
|
+
|
|
261
|
+
# Show the summary
|
|
262
|
+
summary = data.get("summary")
|
|
263
|
+
if summary:
|
|
264
|
+
console.print(f"\n[bold]Summary:[/bold]")
|
|
265
|
+
console.print(f"[dim]{'─' * 60}[/dim]")
|
|
266
|
+
console.print(summary)
|
|
267
|
+
console.print(f"[dim]{'─' * 60}[/dim]")
|
|
268
|
+
|
|
269
|
+
console.print()
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
console.print(f"[red]Error during compaction: {e}[/red]\n")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def handle_diff(args: str = "") -> None:
|
|
276
|
+
"""Handle /diff command - show uncommitted changes in GitHub-style diff view.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
args: Optional file path to show diff for specific file
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
# Build git diff command
|
|
283
|
+
cmd = ["git", "diff", "--no-color"]
|
|
284
|
+
if args:
|
|
285
|
+
cmd.append(args)
|
|
286
|
+
|
|
287
|
+
# Also include staged changes
|
|
288
|
+
result_unstaged = subprocess.run(
|
|
289
|
+
cmd, capture_output=True, text=True, cwd=Path.cwd()
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
cmd_staged = ["git", "diff", "--staged", "--no-color"]
|
|
293
|
+
if args:
|
|
294
|
+
cmd_staged.append(args)
|
|
295
|
+
|
|
296
|
+
result_staged = subprocess.run(
|
|
297
|
+
cmd_staged, capture_output=True, text=True, cwd=Path.cwd()
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Combine diffs
|
|
301
|
+
diff_output = ""
|
|
302
|
+
if result_staged.stdout:
|
|
303
|
+
diff_output += result_staged.stdout
|
|
304
|
+
if result_unstaged.stdout:
|
|
305
|
+
if diff_output:
|
|
306
|
+
diff_output += "\n"
|
|
307
|
+
diff_output += result_unstaged.stdout
|
|
308
|
+
|
|
309
|
+
if not diff_output:
|
|
310
|
+
console.print(f"\n[{Colors.MUTED}]No uncommitted changes.[/{Colors.MUTED}]\n")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
# Render diff with line numbers and syntax highlighting
|
|
314
|
+
render_diff(diff_output, console)
|
|
315
|
+
|
|
316
|
+
except FileNotFoundError:
|
|
317
|
+
console.print(f"\n[{Colors.ERROR}]Git not found. Make sure git is installed.[/{Colors.ERROR}]\n")
|
|
318
|
+
except Exception as e:
|
|
319
|
+
console.print(f"\n[{Colors.ERROR}]Error running git diff: {e}[/{Colors.ERROR}]\n")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Handler for /registry command."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
console = Console()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_registry(args: str) -> None:
|
|
9
|
+
"""Handle /registry command.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
args: Command arguments (list, show, install, search)
|
|
13
|
+
"""
|
|
14
|
+
from emdash_cli.commands.registry import (
|
|
15
|
+
_show_registry_wizard,
|
|
16
|
+
_fetch_registry,
|
|
17
|
+
registry_list,
|
|
18
|
+
registry_show,
|
|
19
|
+
registry_install,
|
|
20
|
+
registry_search,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Parse subcommand
|
|
24
|
+
subparts = args.split(maxsplit=1) if args else []
|
|
25
|
+
subcommand = subparts[0].lower() if subparts else ""
|
|
26
|
+
subargs = subparts[1] if len(subparts) > 1 else ""
|
|
27
|
+
|
|
28
|
+
if subcommand == "" or subcommand == "wizard":
|
|
29
|
+
# Show interactive wizard (default)
|
|
30
|
+
_show_registry_wizard()
|
|
31
|
+
|
|
32
|
+
elif subcommand == "list":
|
|
33
|
+
# List components
|
|
34
|
+
component_type = subargs if subargs else None
|
|
35
|
+
if component_type and component_type not in ["skills", "rules", "agents", "verifiers"]:
|
|
36
|
+
console.print(f"[yellow]Unknown type: {component_type}[/yellow]")
|
|
37
|
+
console.print("[dim]Types: skills, rules, agents, verifiers[/dim]")
|
|
38
|
+
return
|
|
39
|
+
# Invoke click command
|
|
40
|
+
registry_list.callback(component_type)
|
|
41
|
+
|
|
42
|
+
elif subcommand == "show":
|
|
43
|
+
if not subargs:
|
|
44
|
+
console.print("[yellow]Usage: /registry show type:name[/yellow]")
|
|
45
|
+
console.print("[dim]Example: /registry show skill:frontend-design[/dim]")
|
|
46
|
+
return
|
|
47
|
+
registry_show.callback(subargs)
|
|
48
|
+
|
|
49
|
+
elif subcommand == "install":
|
|
50
|
+
if not subargs:
|
|
51
|
+
console.print("[yellow]Usage: /registry install type:name [type:name ...][/yellow]")
|
|
52
|
+
console.print("[dim]Example: /registry install skill:frontend-design rule:typescript[/dim]")
|
|
53
|
+
return
|
|
54
|
+
component_ids = tuple(subargs.split())
|
|
55
|
+
registry_install.callback(component_ids)
|
|
56
|
+
|
|
57
|
+
elif subcommand == "search":
|
|
58
|
+
if not subargs:
|
|
59
|
+
console.print("[yellow]Usage: /registry search query[/yellow]")
|
|
60
|
+
console.print("[dim]Example: /registry search frontend[/dim]")
|
|
61
|
+
return
|
|
62
|
+
# Simple search without tag filtering from slash command
|
|
63
|
+
registry_search.callback(subargs, ())
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
|
|
67
|
+
console.print("[dim]Usage: /registry [list|show|install|search][/dim]")
|
|
68
|
+
console.print("[dim] /registry - Interactive wizard[/dim]")
|
|
69
|
+
console.print("[dim] /registry list - List all components[/dim]")
|
|
70
|
+
console.print("[dim] /registry show x:y - Show component details[/dim]")
|
|
71
|
+
console.print("[dim] /registry install x:y - Install components[/dim]")
|
|
72
|
+
console.print("[dim] /registry search q - Search registry[/dim]")
|
|
@@ -5,6 +5,15 @@ from pathlib import Path
|
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
from rich.panel import Panel
|
|
7
7
|
|
|
8
|
+
from ....design import (
|
|
9
|
+
Colors,
|
|
10
|
+
header,
|
|
11
|
+
footer,
|
|
12
|
+
SEPARATOR_WIDTH,
|
|
13
|
+
STATUS_ACTIVE,
|
|
14
|
+
ARROW_PROMPT,
|
|
15
|
+
)
|
|
16
|
+
|
|
8
17
|
console = Console()
|
|
9
18
|
|
|
10
19
|
|
|
@@ -126,41 +135,41 @@ def show_rules_interactive_menu() -> tuple[str, str]:
|
|
|
126
135
|
event.app.exit()
|
|
127
136
|
|
|
128
137
|
def get_formatted_menu():
|
|
129
|
-
lines = [("class:title", "Rules\n\n")]
|
|
138
|
+
lines = [("class:title", f"─── Rules {'─' * 35}\n\n")]
|
|
130
139
|
|
|
131
140
|
if not rules:
|
|
132
|
-
lines.append(("class:dim", "No rules defined yet.\n\n"))
|
|
141
|
+
lines.append(("class:dim", " No rules defined yet.\n\n"))
|
|
133
142
|
|
|
134
143
|
for i, (name, preview, is_action) in enumerate(menu_items):
|
|
135
144
|
is_selected = i == selected_index[0]
|
|
136
|
-
prefix = "
|
|
145
|
+
prefix = "▸ " if is_selected else " "
|
|
137
146
|
|
|
138
147
|
if is_action:
|
|
139
148
|
if is_selected:
|
|
140
|
-
lines.append(("class:action-selected", f"{prefix}{name}\n"))
|
|
149
|
+
lines.append(("class:action-selected", f" {prefix}{name}\n"))
|
|
141
150
|
else:
|
|
142
|
-
lines.append(("class:action", f"{prefix}{name}\n"))
|
|
151
|
+
lines.append(("class:action", f" {prefix}{name}\n"))
|
|
143
152
|
else:
|
|
144
153
|
if is_selected:
|
|
145
|
-
lines.append(("class:rule-selected", f"{prefix}{name}"))
|
|
146
|
-
lines.append(("class:preview-selected", f"
|
|
154
|
+
lines.append(("class:rule-selected", f" {prefix}{name}"))
|
|
155
|
+
lines.append(("class:preview-selected", f" {preview}\n"))
|
|
147
156
|
else:
|
|
148
|
-
lines.append(("class:rule", f"{prefix}{name}"))
|
|
149
|
-
lines.append(("class:preview", f"
|
|
157
|
+
lines.append(("class:rule", f" {prefix}{name}"))
|
|
158
|
+
lines.append(("class:preview", f" {preview}\n"))
|
|
150
159
|
|
|
151
|
-
lines.append(("class:hint", "\n
|
|
160
|
+
lines.append(("class:hint", f"\n{'─' * 45}\n ↑↓ navigate Enter view n new d delete q quit"))
|
|
152
161
|
return lines
|
|
153
162
|
|
|
154
163
|
style = Style.from_dict({
|
|
155
|
-
"title": "
|
|
156
|
-
"dim": "
|
|
157
|
-
"rule": "
|
|
158
|
-
"rule-selected": "
|
|
159
|
-
"action": "
|
|
160
|
-
"action-selected": "
|
|
161
|
-
"preview": "
|
|
162
|
-
"preview-selected": "
|
|
163
|
-
"hint": "
|
|
164
|
+
"title": f"{Colors.MUTED}",
|
|
165
|
+
"dim": f"{Colors.DIM}",
|
|
166
|
+
"rule": f"{Colors.PRIMARY}",
|
|
167
|
+
"rule-selected": f"{Colors.SUCCESS} bold",
|
|
168
|
+
"action": f"{Colors.WARNING}",
|
|
169
|
+
"action-selected": f"{Colors.WARNING} bold",
|
|
170
|
+
"preview": f"{Colors.DIM}",
|
|
171
|
+
"preview-selected": f"{Colors.SUCCESS}",
|
|
172
|
+
"hint": f"{Colors.DIM}",
|
|
164
173
|
})
|
|
165
174
|
|
|
166
175
|
height = len(menu_items) + 5 # items + title + hint + padding
|
|
@@ -200,22 +209,29 @@ def show_rule_details(name: str) -> None:
|
|
|
200
209
|
rule_file = rules_dir / f"{name}.md"
|
|
201
210
|
|
|
202
211
|
console.print()
|
|
203
|
-
console.print("[
|
|
212
|
+
console.print(f"[{Colors.MUTED}]{header(name, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
204
213
|
console.print()
|
|
205
214
|
|
|
206
215
|
if rule_file.exists():
|
|
207
216
|
try:
|
|
208
217
|
content = rule_file.read_text()
|
|
209
|
-
console.print(f"[
|
|
210
|
-
console.print(
|
|
211
|
-
|
|
218
|
+
console.print(f" [{Colors.DIM}]file[/{Colors.DIM}] {rule_file}")
|
|
219
|
+
console.print()
|
|
220
|
+
|
|
221
|
+
# Show content with indentation
|
|
222
|
+
for line in content.split('\n'):
|
|
223
|
+
if line.startswith('#'):
|
|
224
|
+
console.print(f" [{Colors.PRIMARY}]{line}[/{Colors.PRIMARY}]")
|
|
225
|
+
else:
|
|
226
|
+
console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
|
|
227
|
+
|
|
212
228
|
except Exception as e:
|
|
213
|
-
console.print(f"[
|
|
229
|
+
console.print(f" [{Colors.ERROR}]Error reading rule: {e}[/{Colors.ERROR}]")
|
|
214
230
|
else:
|
|
215
|
-
console.print(f"[
|
|
231
|
+
console.print(f" [{Colors.WARNING}]Rule '{name}' not found[/{Colors.WARNING}]")
|
|
216
232
|
|
|
217
233
|
console.print()
|
|
218
|
-
console.print("[
|
|
234
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
219
235
|
|
|
220
236
|
|
|
221
237
|
def confirm_delete(rule_name: str) -> bool:
|
|
@@ -260,13 +276,14 @@ def chat_create_rule(client, renderer, model, max_iterations, render_with_interr
|
|
|
260
276
|
rules_dir = get_rules_dir()
|
|
261
277
|
|
|
262
278
|
console.print()
|
|
263
|
-
console.print("[
|
|
264
|
-
console.print(
|
|
265
|
-
console.print("[
|
|
279
|
+
console.print(f"[{Colors.MUTED}]{header('Create Rule', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
280
|
+
console.print()
|
|
281
|
+
console.print(f" [{Colors.DIM}]Describe your rule. AI will help write it.[/{Colors.DIM}]")
|
|
282
|
+
console.print(f" [{Colors.DIM}]Type 'done' to finish.[/{Colors.DIM}]")
|
|
266
283
|
console.print()
|
|
267
284
|
|
|
268
285
|
chat_style = Style.from_dict({
|
|
269
|
-
"prompt": "
|
|
286
|
+
"prompt": f"{Colors.PRIMARY} bold",
|
|
270
287
|
})
|
|
271
288
|
|
|
272
289
|
ps = PromptSession(style=chat_style)
|
|
@@ -379,7 +396,7 @@ def handle_rules(args: str, client, renderer, model, max_iterations, render_with
|
|
|
379
396
|
show_rule_details(rule_name)
|
|
380
397
|
# After viewing, show options
|
|
381
398
|
try:
|
|
382
|
-
console.print("[
|
|
399
|
+
console.print("[red]'d'[/red] delete • [dim]Enter back[/dim]", end="")
|
|
383
400
|
ps = PromptSession()
|
|
384
401
|
resp = ps.prompt(" ").strip().lower()
|
|
385
402
|
if resp == 'd':
|
|
@@ -30,7 +30,7 @@ def handle_session(
|
|
|
30
30
|
current_mode_ref: Reference to current_mode (list wrapper for mutation)
|
|
31
31
|
loaded_messages_ref: Reference to loaded_messages (list wrapper for mutation)
|
|
32
32
|
"""
|
|
33
|
-
from
|
|
33
|
+
from ....session_store import SessionStore
|
|
34
34
|
|
|
35
35
|
store = SessionStore(Path.cwd())
|
|
36
36
|
|
|
@@ -100,81 +100,214 @@ When this skill is invoked, you should:
|
|
|
100
100
|
|
|
101
101
|
def show_setup_menu() -> SetupMode | None:
|
|
102
102
|
"""Show the main setup menu and return selected mode."""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
border_style="cyan",
|
|
108
|
-
))
|
|
109
|
-
console.print()
|
|
103
|
+
from prompt_toolkit import Application
|
|
104
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
105
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
106
|
+
from prompt_toolkit.styles import Style
|
|
110
107
|
|
|
111
108
|
options = [
|
|
112
109
|
(SetupMode.RULES, "Rules", "Define coding standards and guidelines for the agent"),
|
|
113
110
|
(SetupMode.AGENTS, "Agents", "Create custom agents with specialized prompts"),
|
|
114
111
|
(SetupMode.SKILLS, "Skills", "Add reusable skills invokable via slash commands"),
|
|
115
112
|
(SetupMode.VERIFIERS, "Verifiers", "Set up verification checks for your work"),
|
|
113
|
+
(None, "Quit", "Exit setup wizard"),
|
|
116
114
|
]
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
selected_index = [0]
|
|
117
|
+
result = [None]
|
|
118
|
+
|
|
119
|
+
kb = KeyBindings()
|
|
120
|
+
|
|
121
|
+
@kb.add("up")
|
|
122
|
+
@kb.add("k")
|
|
123
|
+
def move_up(event):
|
|
124
|
+
selected_index[0] = (selected_index[0] - 1) % len(options)
|
|
125
|
+
|
|
126
|
+
@kb.add("down")
|
|
127
|
+
@kb.add("j")
|
|
128
|
+
def move_down(event):
|
|
129
|
+
selected_index[0] = (selected_index[0] + 1) % len(options)
|
|
130
|
+
|
|
131
|
+
@kb.add("enter")
|
|
132
|
+
def select(event):
|
|
133
|
+
result[0] = options[selected_index[0]][0]
|
|
134
|
+
event.app.exit()
|
|
135
|
+
|
|
136
|
+
@kb.add("c-c")
|
|
137
|
+
@kb.add("escape")
|
|
138
|
+
@kb.add("q")
|
|
139
|
+
def cancel(event):
|
|
140
|
+
result[0] = None
|
|
141
|
+
event.app.exit()
|
|
142
|
+
|
|
143
|
+
def get_formatted_menu():
|
|
144
|
+
lines = [
|
|
145
|
+
("class:title", "Emdash Setup Wizard\n"),
|
|
146
|
+
("class:subtitle", "Configure your project's rules, agents, skills, and verifiers.\n\n"),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for i, (mode, name, desc) in enumerate(options):
|
|
150
|
+
is_selected = i == selected_index[0]
|
|
151
|
+
prefix = "❯ " if is_selected else " "
|
|
152
|
+
|
|
153
|
+
if mode is None: # Quit option
|
|
154
|
+
if is_selected:
|
|
155
|
+
lines.append(("class:quit-selected", f"{prefix}{name}\n"))
|
|
156
|
+
else:
|
|
157
|
+
lines.append(("class:quit", f"{prefix}{name}\n"))
|
|
158
|
+
else:
|
|
159
|
+
if is_selected:
|
|
160
|
+
lines.append(("class:item-selected", f"{prefix}{name}"))
|
|
161
|
+
lines.append(("class:desc-selected", f" {desc}\n"))
|
|
162
|
+
else:
|
|
163
|
+
lines.append(("class:item", f"{prefix}{name}"))
|
|
164
|
+
lines.append(("class:desc", f" {desc}\n"))
|
|
165
|
+
|
|
166
|
+
lines.append(("class:hint", "\n↑/↓ navigate • Enter select • q quit"))
|
|
167
|
+
return lines
|
|
168
|
+
|
|
169
|
+
style = Style.from_dict({
|
|
170
|
+
"title": "#00ccff bold",
|
|
171
|
+
"subtitle": "#888888",
|
|
172
|
+
"item": "#00ccff",
|
|
173
|
+
"item-selected": "#00cc66 bold",
|
|
174
|
+
"desc": "#666666",
|
|
175
|
+
"desc-selected": "#00cc66",
|
|
176
|
+
"quit": "#888888",
|
|
177
|
+
"quit-selected": "#ff6666 bold",
|
|
178
|
+
"hint": "#888888 italic",
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
layout = Layout(
|
|
182
|
+
HSplit([
|
|
183
|
+
Window(
|
|
184
|
+
FormattedTextControl(get_formatted_menu),
|
|
185
|
+
height=len(options) + 5,
|
|
186
|
+
),
|
|
187
|
+
])
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
app = Application(
|
|
191
|
+
layout=layout,
|
|
192
|
+
key_bindings=kb,
|
|
193
|
+
style=style,
|
|
194
|
+
full_screen=False,
|
|
195
|
+
)
|
|
122
196
|
|
|
123
|
-
console.print(" [dim]q[/dim]. Quit setup wizard")
|
|
124
197
|
console.print()
|
|
125
198
|
|
|
126
199
|
try:
|
|
127
|
-
|
|
128
|
-
choice = ps.prompt("Select [1-4, q]: ").strip().lower()
|
|
129
|
-
|
|
130
|
-
if choice == 'q' or choice == 'quit':
|
|
131
|
-
return None
|
|
132
|
-
elif choice == '1':
|
|
133
|
-
return SetupMode.RULES
|
|
134
|
-
elif choice == '2':
|
|
135
|
-
return SetupMode.AGENTS
|
|
136
|
-
elif choice == '3':
|
|
137
|
-
return SetupMode.SKILLS
|
|
138
|
-
elif choice == '4':
|
|
139
|
-
return SetupMode.VERIFIERS
|
|
140
|
-
else:
|
|
141
|
-
console.print("[yellow]Invalid choice[/yellow]")
|
|
142
|
-
return show_setup_menu()
|
|
200
|
+
app.run()
|
|
143
201
|
except (KeyboardInterrupt, EOFError):
|
|
144
|
-
|
|
202
|
+
result[0] = None
|
|
203
|
+
|
|
204
|
+
console.print()
|
|
205
|
+
return result[0]
|
|
145
206
|
|
|
146
207
|
|
|
147
208
|
def show_action_menu(mode: SetupMode) -> str | None:
|
|
148
209
|
"""Show action menu for a mode (add/edit/list/delete)."""
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
210
|
+
from prompt_toolkit import Application
|
|
211
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
212
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
213
|
+
from prompt_toolkit.styles import Style
|
|
214
|
+
|
|
215
|
+
options = [
|
|
216
|
+
("add", "Add new", f"Create a new {mode.value[:-1]}"),
|
|
217
|
+
("edit", "Edit existing", f"Modify an existing {mode.value[:-1]}"),
|
|
218
|
+
("list", "List all", f"Show all configured {mode.value}"),
|
|
219
|
+
("delete", "Delete", f"Remove a {mode.value[:-1]}"),
|
|
220
|
+
("back", "Back", "Return to main menu"),
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
selected_index = [0]
|
|
224
|
+
result = [None]
|
|
225
|
+
|
|
226
|
+
kb = KeyBindings()
|
|
227
|
+
|
|
228
|
+
@kb.add("up")
|
|
229
|
+
@kb.add("k")
|
|
230
|
+
def move_up(event):
|
|
231
|
+
selected_index[0] = (selected_index[0] - 1) % len(options)
|
|
232
|
+
|
|
233
|
+
@kb.add("down")
|
|
234
|
+
@kb.add("j")
|
|
235
|
+
def move_down(event):
|
|
236
|
+
selected_index[0] = (selected_index[0] + 1) % len(options)
|
|
237
|
+
|
|
238
|
+
@kb.add("enter")
|
|
239
|
+
def select(event):
|
|
240
|
+
result[0] = options[selected_index[0]][0]
|
|
241
|
+
event.app.exit()
|
|
242
|
+
|
|
243
|
+
@kb.add("c-c")
|
|
244
|
+
@kb.add("escape")
|
|
245
|
+
@kb.add("b")
|
|
246
|
+
def go_back(event):
|
|
247
|
+
result[0] = "back"
|
|
248
|
+
event.app.exit()
|
|
249
|
+
|
|
250
|
+
def get_formatted_menu():
|
|
251
|
+
lines = [
|
|
252
|
+
("class:title", f"{mode.value.title()} Configuration\n\n"),
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
for i, (action, name, desc) in enumerate(options):
|
|
256
|
+
is_selected = i == selected_index[0]
|
|
257
|
+
prefix = "❯ " if is_selected else " "
|
|
258
|
+
|
|
259
|
+
if action == "back":
|
|
260
|
+
if is_selected:
|
|
261
|
+
lines.append(("class:back-selected", f"{prefix}{name}\n"))
|
|
262
|
+
else:
|
|
263
|
+
lines.append(("class:back", f"{prefix}{name}\n"))
|
|
264
|
+
else:
|
|
265
|
+
if is_selected:
|
|
266
|
+
lines.append(("class:item-selected", f"{prefix}{name}"))
|
|
267
|
+
lines.append(("class:desc-selected", f" {desc}\n"))
|
|
268
|
+
else:
|
|
269
|
+
lines.append(("class:item", f"{prefix}{name}"))
|
|
270
|
+
lines.append(("class:desc", f" {desc}\n"))
|
|
271
|
+
|
|
272
|
+
lines.append(("class:hint", "\n↑/↓ navigate • Enter select • b back"))
|
|
273
|
+
return lines
|
|
274
|
+
|
|
275
|
+
style = Style.from_dict({
|
|
276
|
+
"title": "#00ccff bold",
|
|
277
|
+
"item": "#00ccff",
|
|
278
|
+
"item-selected": "#00cc66 bold",
|
|
279
|
+
"desc": "#666666",
|
|
280
|
+
"desc-selected": "#00cc66",
|
|
281
|
+
"back": "#888888",
|
|
282
|
+
"back-selected": "#ffcc00 bold",
|
|
283
|
+
"hint": "#888888 italic",
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
layout = Layout(
|
|
287
|
+
HSplit([
|
|
288
|
+
Window(
|
|
289
|
+
FormattedTextControl(get_formatted_menu),
|
|
290
|
+
height=len(options) + 4,
|
|
291
|
+
),
|
|
292
|
+
])
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
app = Application(
|
|
296
|
+
layout=layout,
|
|
297
|
+
key_bindings=kb,
|
|
298
|
+
style=style,
|
|
299
|
+
full_screen=False,
|
|
300
|
+
)
|
|
301
|
+
|
|
157
302
|
console.print()
|
|
158
303
|
|
|
159
304
|
try:
|
|
160
|
-
|
|
161
|
-
choice = ps.prompt("Select [1-4, b]: ").strip().lower()
|
|
162
|
-
|
|
163
|
-
if choice == 'b' or choice == 'back':
|
|
164
|
-
return 'back'
|
|
165
|
-
elif choice == '1':
|
|
166
|
-
return 'add'
|
|
167
|
-
elif choice == '2':
|
|
168
|
-
return 'edit'
|
|
169
|
-
elif choice == '3':
|
|
170
|
-
return 'list'
|
|
171
|
-
elif choice == '4':
|
|
172
|
-
return 'delete'
|
|
173
|
-
else:
|
|
174
|
-
console.print("[yellow]Invalid choice[/yellow]")
|
|
175
|
-
return show_action_menu(mode)
|
|
305
|
+
app.run()
|
|
176
306
|
except (KeyboardInterrupt, EOFError):
|
|
177
|
-
|
|
307
|
+
result[0] = "back"
|
|
308
|
+
|
|
309
|
+
console.print()
|
|
310
|
+
return result[0]
|
|
178
311
|
|
|
179
312
|
|
|
180
313
|
def get_existing_items(mode: SetupMode) -> list[str]:
|