emdash-cli 0.1.46__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 (39) hide show
  1. emdash_cli/client.py +12 -28
  2. emdash_cli/commands/__init__.py +2 -2
  3. emdash_cli/commands/agent/constants.py +10 -0
  4. emdash_cli/commands/agent/handlers/__init__.py +10 -0
  5. emdash_cli/commands/agent/handlers/agents.py +67 -39
  6. emdash_cli/commands/agent/handlers/index.py +183 -0
  7. emdash_cli/commands/agent/handlers/misc.py +119 -0
  8. emdash_cli/commands/agent/handlers/registry.py +72 -0
  9. emdash_cli/commands/agent/handlers/rules.py +48 -31
  10. emdash_cli/commands/agent/handlers/sessions.py +1 -1
  11. emdash_cli/commands/agent/handlers/setup.py +187 -54
  12. emdash_cli/commands/agent/handlers/skills.py +42 -4
  13. emdash_cli/commands/agent/handlers/telegram.py +475 -0
  14. emdash_cli/commands/agent/handlers/todos.py +55 -34
  15. emdash_cli/commands/agent/handlers/verify.py +10 -5
  16. emdash_cli/commands/agent/help.py +236 -0
  17. emdash_cli/commands/agent/interactive.py +222 -37
  18. emdash_cli/commands/agent/menus.py +116 -84
  19. emdash_cli/commands/agent/onboarding.py +619 -0
  20. emdash_cli/commands/agent/session_restore.py +210 -0
  21. emdash_cli/commands/index.py +111 -13
  22. emdash_cli/commands/registry.py +635 -0
  23. emdash_cli/commands/skills.py +72 -6
  24. emdash_cli/design.py +328 -0
  25. emdash_cli/diff_renderer.py +438 -0
  26. emdash_cli/integrations/__init__.py +1 -0
  27. emdash_cli/integrations/telegram/__init__.py +15 -0
  28. emdash_cli/integrations/telegram/bot.py +402 -0
  29. emdash_cli/integrations/telegram/bridge.py +865 -0
  30. emdash_cli/integrations/telegram/config.py +155 -0
  31. emdash_cli/integrations/telegram/formatter.py +385 -0
  32. emdash_cli/main.py +52 -2
  33. emdash_cli/sse_renderer.py +632 -171
  34. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -2
  35. emdash_cli-0.1.67.dist-info/RECORD +63 -0
  36. emdash_cli/commands/swarm.py +0 -86
  37. emdash_cli-0.1.46.dist-info/RECORD +0 -49
  38. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
  39. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,475 @@
1
+ """Handler for /telegram command."""
2
+
3
+ import asyncio
4
+ from datetime import datetime
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from ....design import Colors, header, footer, SEPARATOR_WIDTH
11
+
12
+ console = Console()
13
+
14
+
15
+ def handle_telegram(args: str) -> None:
16
+ """Handle /telegram command.
17
+
18
+ Args:
19
+ args: Command arguments (setup, connect, status, test, disconnect)
20
+ """
21
+ from ....integrations.telegram import TelegramConfig, get_config, save_config
22
+ from ....integrations.telegram.bot import verify_token, TelegramBot
23
+
24
+ # Parse subcommand
25
+ subparts = args.split(maxsplit=1) if args else []
26
+ subcommand = subparts[0].lower() if subparts else ""
27
+ subargs = subparts[1] if len(subparts) > 1 else ""
28
+
29
+ if subcommand == "" or subcommand == "help":
30
+ _show_telegram_help()
31
+
32
+ elif subcommand == "setup":
33
+ _handle_setup()
34
+
35
+ elif subcommand == "status":
36
+ _handle_status()
37
+
38
+ elif subcommand == "test":
39
+ _handle_test()
40
+
41
+ elif subcommand == "connect":
42
+ _handle_connect()
43
+
44
+ elif subcommand == "disconnect":
45
+ _handle_disconnect()
46
+
47
+ elif subcommand == "settings":
48
+ _handle_settings(subargs)
49
+
50
+ else:
51
+ console.print(f"[{Colors.WARNING}]Unknown subcommand: {subcommand}[/{Colors.WARNING}]")
52
+ console.print(f"[{Colors.DIM}]Run /telegram help for usage[/{Colors.DIM}]")
53
+
54
+
55
+ def _show_telegram_help() -> None:
56
+ """Show help for /telegram command."""
57
+ from ....integrations.telegram import get_config
58
+
59
+ config = get_config()
60
+
61
+ console.print()
62
+ console.print(f"[{Colors.MUTED}]{header('Telegram Integration', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
63
+ console.print()
64
+
65
+ # Status indicator
66
+ if config.is_configured():
67
+ status = f"[{Colors.SUCCESS}]configured[/{Colors.SUCCESS}]"
68
+ if config.state.enabled:
69
+ status += f" [{Colors.SUCCESS}](enabled)[/{Colors.SUCCESS}]"
70
+ else:
71
+ status = f"[{Colors.WARNING}]not configured[/{Colors.WARNING}]"
72
+
73
+ console.print(f" [{Colors.DIM}]Status:[/{Colors.DIM}] {status}")
74
+ console.print()
75
+
76
+ # Commands table
77
+ commands = [
78
+ ("/telegram setup", "Configure bot token and authorize chats"),
79
+ ("/telegram status", "Show current configuration and state"),
80
+ ("/telegram test", "Send a test message to authorized chats"),
81
+ ("/telegram connect", "Start the Telegram bridge (foreground)"),
82
+ ("/telegram disconnect", "Disable Telegram integration"),
83
+ ("/telegram settings", "View/modify settings"),
84
+ ]
85
+
86
+ console.print(f" [{Colors.DIM}]Commands:[/{Colors.DIM}]")
87
+ console.print()
88
+ for cmd, desc in commands:
89
+ console.print(f" [{Colors.PRIMARY}]{cmd:24}[/{Colors.PRIMARY}] [{Colors.DIM}]{desc}[/{Colors.DIM}]")
90
+
91
+ console.print()
92
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
93
+ console.print()
94
+
95
+
96
+ def _handle_setup() -> None:
97
+ """Interactive setup wizard for Telegram bot."""
98
+ from prompt_toolkit import prompt
99
+ from prompt_toolkit.styles import Style
100
+
101
+ from ....integrations.telegram import TelegramConfig, get_config, save_config
102
+ from ....integrations.telegram.bot import verify_token, TelegramBot, TelegramAPIError
103
+
104
+ console.print()
105
+ console.print(f"[{Colors.MUTED}]{header('Telegram Setup', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
106
+ console.print()
107
+ console.print(f" [{Colors.DIM}]To create a Telegram bot:[/{Colors.DIM}]")
108
+ console.print()
109
+ console.print(f" [{Colors.SUBTLE}]1. Open Telegram and search for @BotFather[/{Colors.SUBTLE}]")
110
+ console.print(f" [{Colors.SUBTLE}]2. Send /newbot and follow the prompts[/{Colors.SUBTLE}]")
111
+ console.print(f" [{Colors.SUBTLE}]3. Copy the bot token and paste below[/{Colors.SUBTLE}]")
112
+ console.print()
113
+
114
+ # Get current config
115
+ config = get_config()
116
+
117
+ # Prompt for bot token
118
+ style = Style.from_dict({"": Colors.PRIMARY})
119
+
120
+ try:
121
+ token = prompt(
122
+ " Bot Token: ",
123
+ style=style,
124
+ default=config.bot_token or "",
125
+ ).strip()
126
+ except (KeyboardInterrupt, EOFError):
127
+ console.print(f"\n [{Colors.DIM}]Setup cancelled[/{Colors.DIM}]")
128
+ return
129
+
130
+ if not token:
131
+ console.print(f" [{Colors.WARNING}]No token provided, setup cancelled[/{Colors.WARNING}]")
132
+ return
133
+
134
+ # Verify token
135
+ console.print()
136
+ console.print(f" [{Colors.DIM}]Verifying token...[/{Colors.DIM}]")
137
+
138
+ bot_info = asyncio.run(verify_token(token))
139
+
140
+ if not bot_info:
141
+ console.print(f" [{Colors.ERROR}]Invalid token. Please check and try again.[/{Colors.ERROR}]")
142
+ return
143
+
144
+ console.print(f" [{Colors.SUCCESS}]Bot verified: @{bot_info.username}[/{Colors.SUCCESS}]")
145
+ console.print()
146
+
147
+ # Save config with token
148
+ config.bot_token = token
149
+ save_config(config)
150
+
151
+ # Now wait for a user to message the bot to authorize
152
+ console.print(f" [{Colors.ACCENT}]Now send /start to your bot from Telegram to authorize.[/{Colors.ACCENT}]")
153
+ console.print(f" [{Colors.DIM}]Waiting for connection... (Ctrl+C to skip)[/{Colors.DIM}]")
154
+ console.print()
155
+
156
+ try:
157
+ authorized_chat = asyncio.run(_wait_for_authorization(token))
158
+ if authorized_chat:
159
+ config.add_authorized_chat(authorized_chat["id"])
160
+ config.state.enabled = True
161
+ config.state.last_connected = datetime.now().isoformat()
162
+ save_config(config)
163
+
164
+ console.print(f" [{Colors.SUCCESS}]Authorized: {authorized_chat['name']} (ID: {authorized_chat['id']})[/{Colors.SUCCESS}]")
165
+ console.print()
166
+ console.print(f" [{Colors.SUCCESS}]Setup complete![/{Colors.SUCCESS}]")
167
+ console.print(f" [{Colors.DIM}]Run /telegram connect to start the bridge[/{Colors.DIM}]")
168
+ except KeyboardInterrupt:
169
+ # Save config without authorization (user can authorize later)
170
+ save_config(config)
171
+ console.print()
172
+ console.print(f" [{Colors.DIM}]Token saved. Run /telegram setup again to authorize chats.[/{Colors.DIM}]")
173
+
174
+ console.print()
175
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
176
+ console.print()
177
+
178
+
179
+ async def _wait_for_authorization(token: str, timeout: int = 60) -> dict | None:
180
+ """Wait for a user to send /start to the bot.
181
+
182
+ Args:
183
+ token: Bot token
184
+ timeout: Maximum seconds to wait
185
+
186
+ Returns:
187
+ Dict with chat id and name, or None if timeout
188
+ """
189
+ from ....integrations.telegram.bot import TelegramBot
190
+
191
+ async with TelegramBot(token) as bot:
192
+ start_time = asyncio.get_event_loop().time()
193
+
194
+ while (asyncio.get_event_loop().time() - start_time) < timeout:
195
+ try:
196
+ updates = await bot.get_updates(timeout=5)
197
+
198
+ for update in updates:
199
+ if update.message and update.message.text:
200
+ text = update.message.text.strip()
201
+ # Accept /start or any message as authorization
202
+ if text.startswith("/start") or text:
203
+ chat = update.message.chat
204
+ user = update.message.from_user
205
+
206
+ name = chat.display_name
207
+ if user:
208
+ name = user.display_name
209
+
210
+ return {
211
+ "id": chat.id,
212
+ "name": name,
213
+ }
214
+ except Exception:
215
+ await asyncio.sleep(1)
216
+ continue
217
+
218
+ return None
219
+
220
+
221
+ def _handle_status() -> None:
222
+ """Show current Telegram configuration and status."""
223
+ from ....integrations.telegram import get_config
224
+ from ....integrations.telegram.bot import verify_token
225
+
226
+ config = get_config()
227
+
228
+ console.print()
229
+ console.print(f"[{Colors.MUTED}]{header('Telegram Status', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
230
+ console.print()
231
+
232
+ # Configuration
233
+ if config.is_configured():
234
+ console.print(f" [{Colors.DIM}]Bot Token:[/{Colors.DIM}] [{Colors.SUCCESS}]configured[/{Colors.SUCCESS}]")
235
+
236
+ # Verify token is still valid
237
+ bot_info = asyncio.run(verify_token(config.bot_token))
238
+ if bot_info:
239
+ console.print(f" [{Colors.DIM}]Bot:[/{Colors.DIM}] @{bot_info.username}")
240
+ else:
241
+ console.print(f" [{Colors.DIM}]Bot:[/{Colors.DIM}] [{Colors.ERROR}]token invalid[/{Colors.ERROR}]")
242
+ else:
243
+ console.print(f" [{Colors.DIM}]Bot Token:[/{Colors.DIM}] [{Colors.WARNING}]not configured[/{Colors.WARNING}]")
244
+ console.print()
245
+ console.print(f" [{Colors.DIM}]Run /telegram setup to configure[/{Colors.DIM}]")
246
+ console.print()
247
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
248
+ console.print()
249
+ return
250
+
251
+ # Authorized chats
252
+ if config.authorized_chats:
253
+ console.print(f" [{Colors.DIM}]Authorized:[/{Colors.DIM}] {len(config.authorized_chats)} chat(s)")
254
+ for chat_id in config.authorized_chats:
255
+ console.print(f" [{Colors.SUBTLE}]{chat_id}[/{Colors.SUBTLE}]")
256
+ else:
257
+ console.print(f" [{Colors.DIM}]Authorized:[/{Colors.DIM}] [{Colors.WARNING}]no chats authorized[/{Colors.WARNING}]")
258
+
259
+ # State
260
+ console.print()
261
+ enabled_status = f"[{Colors.SUCCESS}]yes[/{Colors.SUCCESS}]" if config.state.enabled else f"[{Colors.DIM}]no[/{Colors.DIM}]"
262
+ console.print(f" [{Colors.DIM}]Enabled:[/{Colors.DIM}] {enabled_status}")
263
+
264
+ if config.state.last_connected:
265
+ console.print(f" [{Colors.DIM}]Last Active:[/{Colors.DIM}] {config.state.last_connected}")
266
+
267
+ # Settings
268
+ console.print()
269
+ console.print(f" [{Colors.DIM}]Settings:[/{Colors.DIM}]")
270
+ console.print(f" [{Colors.SUBTLE}]Streaming mode:[/{Colors.SUBTLE}] {config.settings.streaming_mode}")
271
+ console.print(f" [{Colors.SUBTLE}]Update interval:[/{Colors.SUBTLE}] {config.settings.update_interval_ms}ms")
272
+ console.print(f" [{Colors.SUBTLE}]Show thinking:[/{Colors.SUBTLE}] {config.settings.show_thinking}")
273
+ console.print(f" [{Colors.SUBTLE}]Show tools:[/{Colors.SUBTLE}] {config.settings.show_tool_calls}")
274
+
275
+ console.print()
276
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
277
+ console.print()
278
+
279
+
280
+ def _handle_test() -> None:
281
+ """Send a test message to authorized chats."""
282
+ from ....integrations.telegram import get_config
283
+ from ....integrations.telegram.bot import TelegramBot
284
+
285
+ config = get_config()
286
+
287
+ if not config.is_configured():
288
+ console.print(f"[{Colors.WARNING}]Telegram not configured. Run /telegram setup first.[/{Colors.WARNING}]")
289
+ return
290
+
291
+ if not config.authorized_chats:
292
+ console.print(f"[{Colors.WARNING}]No authorized chats. Run /telegram setup to authorize.[/{Colors.WARNING}]")
293
+ return
294
+
295
+ console.print()
296
+ console.print(f" [{Colors.DIM}]Sending test message...[/{Colors.DIM}]")
297
+
298
+ async def send_test():
299
+ async with TelegramBot(config.bot_token) as bot:
300
+ bot_info = await bot.get_me()
301
+ for chat_id in config.authorized_chats:
302
+ try:
303
+ await bot.send_message(
304
+ chat_id,
305
+ f"*EmDash Test*\n\nBot `@{bot_info.username}` is connected and working.",
306
+ parse_mode="Markdown",
307
+ )
308
+ console.print(f" [{Colors.SUCCESS}]Sent to chat {chat_id}[/{Colors.SUCCESS}]")
309
+ except Exception as e:
310
+ console.print(f" [{Colors.ERROR}]Failed to send to {chat_id}: {e}[/{Colors.ERROR}]")
311
+
312
+ asyncio.run(send_test())
313
+ console.print()
314
+
315
+
316
+ def _handle_connect() -> None:
317
+ """Start the Telegram bridge in foreground mode."""
318
+ from ....integrations.telegram import get_config, save_config
319
+
320
+ config = get_config()
321
+
322
+ if not config.is_configured():
323
+ console.print(f"[{Colors.WARNING}]Telegram not configured. Run /telegram setup first.[/{Colors.WARNING}]")
324
+ return
325
+
326
+ console.print()
327
+ console.print(f"[{Colors.MUTED}]{header('Telegram Bridge', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
328
+ console.print()
329
+ console.print(f" [{Colors.SUCCESS}]Bridge starting...[/{Colors.SUCCESS}]")
330
+ console.print(f" [{Colors.DIM}]Listening for messages. Press Ctrl+C to stop.[/{Colors.DIM}]")
331
+ console.print()
332
+
333
+ # Update state
334
+ config.state.enabled = True
335
+ config.state.last_connected = datetime.now().isoformat()
336
+ save_config(config)
337
+
338
+ try:
339
+ asyncio.run(_run_bridge(config))
340
+ except KeyboardInterrupt:
341
+ console.print()
342
+ console.print(f" [{Colors.DIM}]Bridge stopped[/{Colors.DIM}]")
343
+
344
+ console.print()
345
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
346
+ console.print()
347
+
348
+
349
+ async def _run_bridge(config) -> None:
350
+ """Run the Telegram bridge (receives messages and forwards to agent).
351
+
352
+ Connects Telegram messages to the EmDash agent and streams
353
+ SSE responses back as Telegram messages.
354
+ """
355
+ from ....integrations.telegram.bridge import TelegramBridge
356
+ from ....integrations.telegram import save_config
357
+ from ....server_manager import get_server_manager
358
+
359
+ # Get server URL from server manager (starts server if needed)
360
+ server = get_server_manager()
361
+ server_url = server.get_server_url()
362
+ console.print(f" [{Colors.SUBTLE}]Server: {server_url}[/{Colors.SUBTLE}]")
363
+
364
+ def on_message(event_type: str, data: dict) -> None:
365
+ """Log bridge events to the console."""
366
+ if event_type == "bridge_started":
367
+ console.print(f" [{Colors.SUBTLE}]Bot: @{data.get('bot', 'unknown')}[/{Colors.SUBTLE}]")
368
+ console.print()
369
+ elif event_type == "message_received":
370
+ user = data.get("user", "Unknown")
371
+ text = data.get("text", "")[:50]
372
+ if len(data.get("text", "")) > 50:
373
+ text += "..."
374
+ console.print(f" [{Colors.ACCENT}]{user}:[/{Colors.ACCENT}] {text}")
375
+ elif event_type == "error":
376
+ console.print(f" [{Colors.ERROR}]Error: {data.get('error', 'unknown')}[/{Colors.ERROR}]")
377
+ elif event_type == "send_error":
378
+ console.print(f" [{Colors.WARNING}]Send failed: {data.get('error', 'unknown')}[/{Colors.WARNING}]")
379
+
380
+ bridge = TelegramBridge(config, server_url=server_url, on_message=on_message)
381
+
382
+ try:
383
+ await bridge.start()
384
+ finally:
385
+ # Save config with updated state (last_update_id, etc.)
386
+ save_config(config)
387
+
388
+
389
+ def _handle_disconnect() -> None:
390
+ """Disable Telegram integration."""
391
+ from ....integrations.telegram import get_config, save_config, delete_config
392
+ from prompt_toolkit import prompt
393
+
394
+ config = get_config()
395
+
396
+ if not config.is_configured():
397
+ console.print(f"[{Colors.DIM}]Telegram is not configured.[/{Colors.DIM}]")
398
+ return
399
+
400
+ console.print()
401
+ console.print(f" [{Colors.WARNING}]This will remove your Telegram configuration.[/{Colors.WARNING}]")
402
+
403
+ try:
404
+ confirm = prompt(" Type 'yes' to confirm: ").strip().lower()
405
+ except (KeyboardInterrupt, EOFError):
406
+ console.print(f"\n [{Colors.DIM}]Cancelled[/{Colors.DIM}]")
407
+ return
408
+
409
+ if confirm == "yes":
410
+ delete_config()
411
+ console.print(f" [{Colors.SUCCESS}]Telegram configuration removed.[/{Colors.SUCCESS}]")
412
+ else:
413
+ console.print(f" [{Colors.DIM}]Cancelled[/{Colors.DIM}]")
414
+
415
+ console.print()
416
+
417
+
418
+ def _handle_settings(args: str) -> None:
419
+ """View or modify Telegram settings."""
420
+ from ....integrations.telegram import get_config, save_config
421
+
422
+ config = get_config()
423
+
424
+ if not config.is_configured():
425
+ console.print(f"[{Colors.WARNING}]Telegram not configured. Run /telegram setup first.[/{Colors.WARNING}]")
426
+ return
427
+
428
+ if not args:
429
+ # Show current settings
430
+ console.print()
431
+ console.print(f" [{Colors.DIM}]Current Settings:[/{Colors.DIM}]")
432
+ console.print()
433
+ console.print(f" [{Colors.PRIMARY}]streaming_mode[/{Colors.PRIMARY}] = {config.settings.streaming_mode}")
434
+ console.print(f" [{Colors.PRIMARY}]update_interval_ms[/{Colors.PRIMARY}] = {config.settings.update_interval_ms}")
435
+ console.print(f" [{Colors.PRIMARY}]show_thinking[/{Colors.PRIMARY}] = {config.settings.show_thinking}")
436
+ console.print(f" [{Colors.PRIMARY}]show_tool_calls[/{Colors.PRIMARY}] = {config.settings.show_tool_calls}")
437
+ console.print(f" [{Colors.PRIMARY}]compact_mode[/{Colors.PRIMARY}] = {config.settings.compact_mode}")
438
+ console.print()
439
+ console.print(f" [{Colors.DIM}]Usage: /telegram settings <key> <value>[/{Colors.DIM}]")
440
+ console.print()
441
+ return
442
+
443
+ # Parse key=value or key value
444
+ parts = args.replace("=", " ").split()
445
+ if len(parts) < 2:
446
+ console.print(f"[{Colors.WARNING}]Usage: /telegram settings <key> <value>[/{Colors.WARNING}]")
447
+ return
448
+
449
+ key = parts[0]
450
+ value = parts[1]
451
+
452
+ # Update setting
453
+ if key == "streaming_mode":
454
+ if value not in ("edit", "append"):
455
+ console.print(f"[{Colors.WARNING}]streaming_mode must be 'edit' or 'append'[/{Colors.WARNING}]")
456
+ return
457
+ config.settings.streaming_mode = value
458
+ elif key == "update_interval_ms":
459
+ try:
460
+ config.settings.update_interval_ms = int(value)
461
+ except ValueError:
462
+ console.print(f"[{Colors.WARNING}]update_interval_ms must be a number[/{Colors.WARNING}]")
463
+ return
464
+ elif key == "show_thinking":
465
+ config.settings.show_thinking = value.lower() in ("true", "1", "yes")
466
+ elif key == "show_tool_calls":
467
+ config.settings.show_tool_calls = value.lower() in ("true", "1", "yes")
468
+ elif key == "compact_mode":
469
+ config.settings.compact_mode = value.lower() in ("true", "1", "yes")
470
+ else:
471
+ console.print(f"[{Colors.WARNING}]Unknown setting: {key}[/{Colors.WARNING}]")
472
+ return
473
+
474
+ save_config(config)
475
+ console.print(f" [{Colors.SUCCESS}]Setting updated: {key} = {value}[/{Colors.SUCCESS}]")
@@ -2,6 +2,16 @@
2
2
 
3
3
  from rich.console import Console
4
4
 
5
+ from ....design import (
6
+ Colors,
7
+ header,
8
+ footer,
9
+ SEPARATOR_WIDTH,
10
+ STATUS_ACTIVE,
11
+ STATUS_INACTIVE,
12
+ DOT_BULLET,
13
+ )
14
+
5
15
  console = Console()
6
16
 
7
17
 
@@ -16,12 +26,19 @@ def handle_todos(args: str, client, session_id: str | None, pending_todos: list[
16
26
  """
17
27
  if not session_id:
18
28
  if pending_todos:
19
- console.print("\n[bold cyan]Pending Todos[/bold cyan] [dim](will be added when session starts)[/dim]\n")
20
- for i, todo in enumerate(pending_todos, 1):
21
- console.print(f" [dim][/dim] {todo}")
29
+ console.print()
30
+ console.print(f"[{Colors.MUTED}]{header('Pending Todos', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
31
+ console.print(f" [{Colors.DIM}]will be added when session starts[/{Colors.DIM}]")
32
+ console.print()
33
+ for todo in pending_todos:
34
+ console.print(f" [{Colors.MUTED}]○[/{Colors.MUTED}] {todo}")
35
+ console.print()
36
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
22
37
  console.print()
23
38
  else:
24
- console.print("\n[dim]No todos yet. Start a conversation and the agent will track its tasks here.[/dim]\n")
39
+ console.print()
40
+ console.print(f" [{Colors.DIM}]No todos yet. Start a conversation to track tasks.[/{Colors.DIM}]")
41
+ console.print()
25
42
  else:
26
43
  try:
27
44
  result = client.get_todos(session_id)
@@ -29,43 +46,41 @@ def handle_todos(args: str, client, session_id: str | None, pending_todos: list[
29
46
  summary = result.get("summary", {})
30
47
 
31
48
  if not todos:
32
- console.print("\n[dim]No todos in current session.[/dim]\n")
49
+ console.print()
50
+ console.print(f" [{Colors.DIM}]No todos in current session.[/{Colors.DIM}]")
51
+ console.print()
33
52
  else:
34
- console.print("\n[bold cyan]Agent Todo List[/bold cyan]\n")
35
-
36
- # Status icons
37
- icons = {
38
- "pending": "[dim]⬚[/dim]",
39
- "in_progress": "[yellow]🔄[/yellow]",
40
- "completed": "[green]✓[/green]",
41
- }
53
+ console.print()
54
+ console.print(f"[{Colors.MUTED}]{header('Todo List', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
55
+ console.print()
42
56
 
43
57
  for todo in todos:
44
- icon = icons.get(todo["status"], "?")
45
58
  title = todo["title"]
46
59
  status = todo["status"]
47
60
 
48
61
  if status == "completed":
49
- console.print(f" {icon} [dim strikethrough]{title}[/dim strikethrough]")
62
+ console.print(f" [{Colors.SUCCESS}]●[/{Colors.SUCCESS}] [{Colors.DIM} strike]{title}[/{Colors.DIM} strike]")
50
63
  elif status == "in_progress":
51
- console.print(f" {icon} [bold]{title}[/bold]")
64
+ console.print(f" [{Colors.WARNING}]◐[/{Colors.WARNING}] [{Colors.TEXT} bold]{title}[/{Colors.TEXT} bold]")
52
65
  else:
53
- console.print(f" {icon} {title}")
66
+ console.print(f" [{Colors.MUTED}]○[/{Colors.MUTED}] {title}")
54
67
 
55
68
  if todo.get("description"):
56
69
  desc = todo["description"]
57
- if len(desc) > 60:
58
- console.print(f" [dim]{desc[:60]}...[/dim]")
59
- else:
60
- console.print(f" [dim]{desc}[/dim]")
70
+ if len(desc) > 55:
71
+ desc = desc[:55] + "..."
72
+ console.print(f" [{Colors.DIM}]{desc}[/{Colors.DIM}]")
61
73
 
62
74
  console.print()
63
- console.print(
64
- f"[dim]Total: {summary.get('total', 0)} | "
65
- f"Pending: {summary.get('pending', 0)} | "
66
- f"In Progress: {summary.get('in_progress', 0)} | "
67
- f"Completed: {summary.get('completed', 0)}[/dim]"
68
- )
75
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
76
+
77
+ # Summary bar
78
+ total = summary.get('total', 0)
79
+ completed = summary.get('completed', 0)
80
+ in_progress = summary.get('in_progress', 0)
81
+ pending = summary.get('pending', 0)
82
+
83
+ console.print(f" [{Colors.DIM}]○ {pending}[/{Colors.DIM}] [{Colors.WARNING}]◐ {in_progress}[/{Colors.WARNING}] [{Colors.SUCCESS}]● {completed}[/{Colors.SUCCESS}] [{Colors.MUTED}]total {total}[/{Colors.MUTED}]")
69
84
  console.print()
70
85
 
71
86
  except Exception as e:
@@ -82,17 +97,23 @@ def handle_todo_add(args: str, client, session_id: str | None, pending_todos: li
82
97
  pending_todos: List of pending todos (before session starts)
83
98
  """
84
99
  if not args:
85
- console.print("[yellow]Usage: /todo-add <title>[/yellow]")
86
- console.print("[dim]Example: /todo-add Fix the failing tests[/dim]")
100
+ console.print()
101
+ console.print(f" [{Colors.WARNING}]Usage:[/{Colors.WARNING}] /todo-add <title>")
102
+ console.print(f" [{Colors.DIM}]Example: /todo-add Fix the failing tests[/{Colors.DIM}]")
103
+ console.print()
87
104
  elif not session_id:
88
105
  pending_todos.append(args)
89
- console.print(f"[green]Todo noted:[/green] {args}")
90
- console.print("[dim]This will be added to the agent's context when you start a conversation.[/dim]")
106
+ console.print()
107
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.MUTED}]noted:[/{Colors.MUTED}] {args}")
108
+ console.print(f" [{Colors.DIM}]will be added when session starts[/{Colors.DIM}]")
109
+ console.print()
91
110
  else:
92
111
  try:
93
112
  result = client.add_todo(session_id, args)
94
113
  task = result.get("task", {})
95
- console.print(f"[green]Added todo #{task.get('id')}: {task.get('title')}[/green]")
96
- console.print(f"[dim]Total tasks: {result.get('total_tasks', 0)}[/dim]")
114
+ console.print()
115
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.MUTED}]added:[/{Colors.MUTED}] {task.get('title')}")
116
+ console.print(f" [{Colors.DIM}]total: {result.get('total_tasks', 0)}[/{Colors.DIM}]")
117
+ console.print()
97
118
  except Exception as e:
98
- console.print(f"[red]Error adding todo: {e}[/red]")
119
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] {e}")
@@ -480,14 +480,17 @@ def _run_verifiers(manager: VerifierManager) -> None:
480
480
  return
481
481
 
482
482
  console.print(f"[bold]Running {len(manager.verifiers)} verifier(s)...[/bold]")
483
- console.print()
484
483
 
485
484
  context = {
486
485
  "git_diff": get_git_diff(),
487
486
  "files_changed": get_changed_files(),
488
487
  }
489
488
 
490
- report = manager.run_all(context)
489
+ # Run with spinner for better UX
490
+ with console.status("[cyan]Running verifiers...", spinner="dots"):
491
+ report = manager.run_all(context)
492
+
493
+ console.print()
491
494
  display_report(report)
492
495
  console.print()
493
496
 
@@ -548,7 +551,6 @@ def run_verification(goal: str | None = None) -> tuple[VerificationReport, bool]
548
551
 
549
552
  console.print()
550
553
  console.print(f"[bold cyan]Running {len(manager.verifiers)} verifier(s)...[/bold cyan]")
551
- console.print()
552
554
 
553
555
  # Build context
554
556
  context = {
@@ -557,8 +559,11 @@ def run_verification(goal: str | None = None) -> tuple[VerificationReport, bool]
557
559
  "files_changed": get_changed_files(),
558
560
  }
559
561
 
560
- # Run verifiers
561
- report = manager.run_all(context)
562
+ # Run verifiers with spinner
563
+ with console.status("[cyan]Running verifiers...", spinner="dots"):
564
+ report = manager.run_all(context)
565
+
566
+ console.print()
562
567
 
563
568
  # Display results
564
569
  display_report(report)