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.
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 +78 -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 +523 -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 +278 -47
  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 +980 -0
  30. emdash_cli/integrations/telegram/config.py +155 -0
  31. emdash_cli/integrations/telegram/formatter.py +392 -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.70.dist-info}/METADATA +2 -2
  35. emdash_cli-0.1.70.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.70.dist-info}/WHEEL +0 -0
  39. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,8 @@
1
1
  """Interactive REPL mode for the agent CLI."""
2
2
 
3
3
  import subprocess
4
+ import sys
5
+ import time
4
6
  import threading
5
7
  from pathlib import Path
6
8
 
@@ -8,7 +10,48 @@ from rich.console import Console
8
10
  from rich.panel import Panel
9
11
  from rich.markdown import Markdown
10
12
 
11
- from .constants import AgentMode, SLASH_COMMANDS
13
+ from .constants import AgentMode, SLASH_COMMANDS, SLASH_SUBCOMMANDS
14
+ from .onboarding import is_first_run, run_onboarding
15
+ from .help import show_command_help
16
+ from .session_restore import get_recent_session, show_session_restore_prompt
17
+ from ...design import (
18
+ header, footer, Colors, STATUS_ACTIVE, DOT_BULLET,
19
+ ARROW_PROMPT, SEPARATOR_WIDTH,
20
+ )
21
+
22
+
23
+ def show_welcome_banner(
24
+ version: str,
25
+ git_repo: str | None,
26
+ git_branch: str | None,
27
+ mode: str,
28
+ model: str,
29
+ console: Console,
30
+ ) -> None:
31
+ """Display clean welcome banner with zen styling."""
32
+ console.print()
33
+
34
+ # Simple header
35
+ console.print(f"[{Colors.MUTED}]{header('emdash', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
36
+ console.print(f" [{Colors.DIM}]v{version}[/{Colors.DIM}]")
37
+ console.print()
38
+
39
+ # Info section
40
+ if git_repo:
41
+ branch_display = f" [{Colors.WARNING}]{git_branch}[/{Colors.WARNING}]" if git_branch else ""
42
+ console.print(f" [{Colors.DIM}]repo[/{Colors.DIM}] [{Colors.SUCCESS}]{git_repo}[/{Colors.SUCCESS}]{branch_display}")
43
+
44
+ mode_color = Colors.WARNING if mode == "plan" else Colors.SUCCESS
45
+ console.print(f" [{Colors.DIM}]mode[/{Colors.DIM}] [{mode_color}]{mode}[/{mode_color}]")
46
+ console.print(f" [{Colors.DIM}]model[/{Colors.DIM}] [{Colors.MUTED}]{model}[/{Colors.MUTED}]")
47
+ console.print()
48
+
49
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
50
+ console.print()
51
+
52
+ # Quick tips
53
+ console.print(f" [{Colors.DIM}]› /help commands › @file include files › Ctrl+C cancel[/{Colors.DIM}]")
54
+ console.print()
12
55
  from .file_utils import expand_file_references, fuzzy_find_files
13
56
  from .menus import (
14
57
  get_clarification_response,
@@ -23,7 +66,9 @@ from .handlers import (
23
66
  handle_hooks,
24
67
  handle_rules,
25
68
  handle_skills,
69
+ handle_index,
26
70
  handle_mcp,
71
+ handle_registry,
27
72
  handle_auth,
28
73
  handle_doctor,
29
74
  handle_verify,
@@ -34,6 +79,9 @@ from .handlers import (
34
79
  handle_projectmd,
35
80
  handle_research,
36
81
  handle_context,
82
+ handle_compact,
83
+ handle_diff,
84
+ handle_telegram,
37
85
  )
38
86
 
39
87
  console = Console()
@@ -129,6 +177,7 @@ def run_interactive(
129
177
  from prompt_toolkit.completion import Completer, Completion
130
178
  from prompt_toolkit.styles import Style
131
179
  from prompt_toolkit.key_binding import KeyBindings
180
+ from prompt_toolkit.formatted_text import FormattedText
132
181
 
133
182
  # Current mode
134
183
  current_mode = AgentMode(options.get("mode", "code"))
@@ -141,18 +190,36 @@ def run_interactive(
141
190
  # Pending todos to add when session starts
142
191
  pending_todos: list[str] = []
143
192
 
144
- # Style for prompt
193
+ # Style for prompt (emdash signature style)
194
+ # Toolbar info (will be set later, but need closure access)
195
+ toolbar_branch: str | None = None
196
+ toolbar_model: str = "unknown"
197
+
145
198
  PROMPT_STYLE = Style.from_dict({
146
- "prompt.mode.plan": "#ffcc00 bold",
147
- "prompt.mode.code": "#00cc66 bold",
148
- "prompt.prefix": "#888888",
149
- "prompt.image": "#00ccff",
150
- "completion-menu": "bg:#1a1a2e #ffffff",
151
- "completion-menu.completion": "bg:#1a1a2e #ffffff",
152
- "completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
153
- "completion-menu.meta.completion": "bg:#1a1a2e #888888",
154
- "completion-menu.meta.completion.current": "bg:#4a4a6e #aaaaaa",
155
- "command": "#00ccff bold",
199
+ "prompt.mode.plan": f"{Colors.WARNING} bold",
200
+ "prompt.mode.code": f"{Colors.PRIMARY} bold",
201
+ "prompt.prefix": Colors.MUTED,
202
+ "prompt.cursor": f"{Colors.PRIMARY}",
203
+ "prompt.image": Colors.ACCENT,
204
+ "completion-menu": "bg:#1a1a2e #e8ecf0",
205
+ "completion-menu.completion": "bg:#1a1a2e #e8ecf0",
206
+ "completion-menu.completion.current": f"bg:#2a2a3e {Colors.SUCCESS} bold",
207
+ "completion-menu.meta.completion": f"bg:#1a1a2e {Colors.MUTED}",
208
+ "completion-menu.meta.completion.current": f"bg:#2a2a3e {Colors.SUBTLE}",
209
+ "command": f"{Colors.PRIMARY} bold",
210
+ # Styled completion text
211
+ "slash-cmd": f"{Colors.ACCENT} bold",
212
+ "sub-cmd": f"{Colors.SUCCESS}",
213
+ "file-ref": f"{Colors.WARNING}",
214
+ # Zen bottom toolbar styles
215
+ "bottom-toolbar": f"bg:#1a1a1a {Colors.DIM}",
216
+ "bottom-toolbar.brand": f"bg:#1a1a1a {Colors.PRIMARY}",
217
+ "bottom-toolbar.branch": f"bg:#1a1a1a {Colors.WARNING}",
218
+ "bottom-toolbar.model": f"bg:#1a1a1a {Colors.ACCENT}",
219
+ "bottom-toolbar.mode-code": f"bg:#1a1a1a {Colors.SUCCESS}",
220
+ "bottom-toolbar.mode-plan": f"bg:#1a1a1a {Colors.WARNING}",
221
+ "bottom-toolbar.session": f"bg:#1a1a1a {Colors.SUCCESS}",
222
+ "bottom-toolbar.no-session": f"bg:#1a1a1a {Colors.MUTED}",
156
223
  })
157
224
 
158
225
  class SlashCommandCompleter(Completer):
@@ -176,11 +243,14 @@ def run_interactive(
176
243
  rel_path = match.relative_to(cwd)
177
244
  except ValueError:
178
245
  rel_path = match
179
- # Replace from @ onwards
246
+ # Replace from @ onwards - styled display
247
+ styled_display = FormattedText([
248
+ ("class:file-ref", f"@{rel_path}")
249
+ ])
180
250
  yield Completion(
181
251
  f"@{rel_path}",
182
252
  start_position=-(len(query) + 1), # +1 for @
183
- display=str(rel_path),
253
+ display=styled_display,
184
254
  display_meta="file",
185
255
  )
186
256
  return
@@ -188,14 +258,45 @@ def run_interactive(
188
258
  # Handle slash commands
189
259
  if not text.startswith("/"):
190
260
  return
261
+
262
+ # Check if we're completing a subcommand (e.g., "/telegram " or "/telegram c")
263
+ parts = text.split(maxsplit=1)
264
+ base_cmd = parts[0]
265
+
266
+ # If we have a space after base command, show subcommands
267
+ if len(parts) > 1 or text.endswith(" "):
268
+ subcommand_prefix = parts[1] if len(parts) > 1 else ""
269
+ if base_cmd in SLASH_SUBCOMMANDS:
270
+ subcommands = SLASH_SUBCOMMANDS[base_cmd]
271
+ for subcmd, description in subcommands.items():
272
+ if subcmd.startswith(subcommand_prefix):
273
+ # Styled display for subcommand
274
+ styled_display = FormattedText([
275
+ ("class:sub-cmd", subcmd)
276
+ ])
277
+ yield Completion(
278
+ subcmd,
279
+ start_position=-len(subcommand_prefix),
280
+ display=styled_display,
281
+ display_meta=description,
282
+ )
283
+ return
284
+
285
+ # Otherwise show main slash commands
191
286
  for cmd, description in SLASH_COMMANDS.items():
192
287
  # Extract base command (e.g., "/pr" from "/pr [url]")
193
- base_cmd = cmd.split()[0]
194
- if base_cmd.startswith(text):
288
+ cmd_base = cmd.split()[0]
289
+ if cmd_base.startswith(text):
290
+ # Add space suffix for commands with subcommands to trigger subcommand completion
291
+ completion_text = cmd_base + " " if cmd_base in SLASH_SUBCOMMANDS else cmd_base
292
+ # Styled display for slash command
293
+ styled_display = FormattedText([
294
+ ("class:slash-cmd", cmd)
295
+ ])
195
296
  yield Completion(
196
- base_cmd,
297
+ completion_text,
197
298
  start_position=-len(text),
198
- display=cmd,
299
+ display=styled_display,
199
300
  display_meta=description,
200
301
  )
201
302
 
@@ -207,10 +308,17 @@ def run_interactive(
207
308
  # Key bindings: Enter submits, Alt+Enter inserts newline
208
309
  kb = KeyBindings()
209
310
 
210
- @kb.add("enter", eager=True)
311
+ @kb.add("enter")
211
312
  def submit_on_enter(event):
212
- """Submit on Enter."""
213
- event.current_buffer.validate_and_handle()
313
+ """Submit on Enter, or select completion if menu is open."""
314
+ buffer = event.current_buffer
315
+ # If completion menu is open, accept the selected completion
316
+ if buffer.complete_state and buffer.complete_state.current_completion:
317
+ completion = buffer.complete_state.current_completion
318
+ buffer.apply_completion(completion)
319
+ return
320
+ # Otherwise submit the input
321
+ buffer.validate_and_handle()
214
322
 
215
323
  @kb.add("escape", "enter") # Alt+Enter (Escape then Enter)
216
324
  @kb.add("c-j") # Ctrl+J as alternative for newline
@@ -229,6 +337,8 @@ def run_interactive(
229
337
  if image_data:
230
338
  base64_data, img_format = image_data
231
339
  attached_images.append({"data": base64_data, "format": img_format})
340
+ # Show feedback that image was attached
341
+ console.print(f" [{Colors.SUCCESS}]✓ Image {len(attached_images)} attached[/{Colors.SUCCESS}]")
232
342
  # Refresh prompt to show updated image list
233
343
  event.app.invalidate()
234
344
  return
@@ -275,39 +385,88 @@ def run_interactive(
275
385
  except Exception:
276
386
  pass
277
387
 
388
+ def get_bottom_toolbar():
389
+ """Bottom status bar with zen aesthetic - em-dashes and warm colors."""
390
+ nonlocal current_mode, session_id, toolbar_branch, toolbar_model
391
+
392
+ # Zen symbols
393
+ em = "─"
394
+ dot = "∷"
395
+
396
+ # Build toolbar with zen aesthetic
397
+ parts = [
398
+ ("class:bottom-toolbar", f" {em}{em} "),
399
+ ("class:bottom-toolbar.brand", "◈ emdash"),
400
+ ]
401
+
402
+ # Branch with stippled bullet
403
+ if toolbar_branch:
404
+ parts.append(("class:bottom-toolbar", f" {dot} "))
405
+ parts.append(("class:bottom-toolbar.branch", toolbar_branch))
406
+
407
+ # Model with stippled bullet
408
+ if toolbar_model and toolbar_model != "unknown":
409
+ parts.append(("class:bottom-toolbar", f" {dot} "))
410
+ parts.append(("class:bottom-toolbar.model", toolbar_model))
411
+
412
+ # Mode indicator
413
+ parts.append(("class:bottom-toolbar", f" {em}{em} "))
414
+ if current_mode == AgentMode.PLAN:
415
+ parts.append(("class:bottom-toolbar.mode-plan", "▹ plan"))
416
+ else:
417
+ parts.append(("class:bottom-toolbar.mode-code", "▸ code"))
418
+
419
+ # Session indicator
420
+ if session_id:
421
+ parts.append(("class:bottom-toolbar.session", " ●"))
422
+ else:
423
+ parts.append(("class:bottom-toolbar.no-session", " ○"))
424
+
425
+ parts.append(("class:bottom-toolbar", " "))
426
+
427
+ return parts
428
+
278
429
  session = PromptSession(
279
430
  history=history,
280
431
  completer=SlashCommandCompleter(),
281
432
  style=PROMPT_STYLE,
282
433
  complete_while_typing=True,
283
434
  multiline=True,
284
- prompt_continuation="... ",
435
+ prompt_continuation=" ",
285
436
  key_bindings=kb,
437
+ bottom_toolbar=get_bottom_toolbar,
286
438
  )
287
439
 
288
440
  # Watch for image paths being pasted/dropped
289
441
  session.default_buffer.on_text_changed += check_for_image_path
290
442
 
291
443
  def get_prompt():
292
- """Get formatted prompt."""
293
- nonlocal attached_images
444
+ """Get formatted prompt with distinctive emdash styling."""
445
+ nonlocal attached_images, current_mode
294
446
  parts = []
295
447
  # Show attached images above prompt
296
448
  if attached_images:
297
- image_tags = " ".join(f"[Image #{i+1}]" for i in range(len(attached_images)))
298
- parts.append(("class:prompt.image", f" {image_tags}\n"))
299
- parts.append(("class:prompt.prefix", "> "))
449
+ image_tags = " ".join(f"[Image {i+1}]" for i in range(len(attached_images)))
450
+ parts.append(("class:prompt.image", f" {image_tags}\n"))
451
+ # Distinctive em-dash prompt with mode indicator
452
+ mode_class = "class:prompt.mode.plan" if current_mode == AgentMode.PLAN else "class:prompt.mode.code"
453
+ # Use em-dash as the signature prompt element
454
+ parts.append(("class:prompt.prefix", " "))
455
+ parts.append((mode_class, f"─── "))
456
+ parts.append(("class:prompt.cursor", "█ "))
300
457
  return parts
301
458
 
302
459
  def show_help():
303
- """Show available commands."""
460
+ """Show available commands with zen styling."""
304
461
  console.print()
305
- console.print("[bold cyan]Available Commands[/bold cyan]")
462
+ console.print(f"[{Colors.MUTED}]{header('Commands', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
306
463
  console.print()
307
464
  for cmd, desc in SLASH_COMMANDS.items():
308
- console.print(f" [cyan]{cmd:12}[/cyan] {desc}")
465
+ console.print(f" [{Colors.PRIMARY}]{cmd:18}[/{Colors.PRIMARY}] [{Colors.DIM}]{desc}[/{Colors.DIM}]")
466
+ console.print()
467
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
309
468
  console.print()
310
- console.print("[dim]Type your task or question to interact with the agent.[/dim]")
469
+ console.print(f" [{Colors.DIM}]Type your task or question to interact with the agent.[/{Colors.DIM}]")
311
470
  console.print()
312
471
 
313
472
  def handle_slash_command(cmd: str) -> bool:
@@ -322,33 +481,38 @@ def run_interactive(
322
481
  return False
323
482
 
324
483
  elif command == "/help":
325
- show_help()
484
+ if args:
485
+ # Show contextual help for specific command
486
+ show_command_help(args)
487
+ else:
488
+ show_help()
326
489
 
327
490
  elif command == "/plan":
328
491
  current_mode = AgentMode.PLAN
329
492
  # Reset session so next chat creates a new session with plan mode
330
493
  if session_id:
331
494
  session_id = None
332
- console.print("[bold green] Plan mode activated[/bold green] [dim](session reset)[/dim]")
495
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.WARNING}]plan mode[/{Colors.WARNING}] [{Colors.DIM}](session reset)[/{Colors.DIM}]")
333
496
  else:
334
- console.print("[bold green] Plan mode activated[/bold green]")
497
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.WARNING}]plan mode[/{Colors.WARNING}]")
335
498
 
336
499
  elif command == "/code":
337
500
  current_mode = AgentMode.CODE
338
501
  # Reset session so next chat creates a new session with code mode
339
502
  if session_id:
340
503
  session_id = None
341
- console.print("[green]Switched to code mode (session reset)[/green]")
504
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.SUCCESS}]code mode[/{Colors.SUCCESS}] [{Colors.DIM}](session reset)[/{Colors.DIM}]")
342
505
  else:
343
- console.print("[green]Switched to code mode[/green]")
506
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.SUCCESS}]code mode[/{Colors.SUCCESS}]")
344
507
 
345
508
  elif command == "/mode":
346
- console.print(f"Current mode: [bold]{current_mode.value}[/bold]")
509
+ mode_color = Colors.WARNING if current_mode == AgentMode.PLAN else Colors.SUCCESS
510
+ console.print(f" [{Colors.MUTED}]current mode:[/{Colors.MUTED}] [{mode_color}]{current_mode.value}[/{mode_color}]")
347
511
 
348
512
  elif command == "/reset":
349
513
  session_id = None
350
514
  current_spec = None
351
- console.print("[dim]Session reset[/dim]")
515
+ console.print(f" [{Colors.DIM}]session reset[/{Colors.DIM}]")
352
516
 
353
517
  elif command == "/spec":
354
518
  if current_spec:
@@ -368,6 +532,9 @@ def run_interactive(
368
532
  elif command == "/status":
369
533
  handle_status(client)
370
534
 
535
+ elif command == "/diff":
536
+ handle_diff(args)
537
+
371
538
  elif command == "/agents":
372
539
  handle_agents(args, client, renderer, model, max_iterations, render_with_interrupt)
373
540
 
@@ -404,12 +571,33 @@ def run_interactive(
404
571
  elif command == "/skills":
405
572
  handle_skills(args, client, renderer, model, max_iterations, render_with_interrupt)
406
573
 
574
+ elif command == "/index":
575
+ handle_index(args, client)
576
+
407
577
  elif command == "/context":
408
578
  handle_context(renderer)
409
579
 
580
+ elif command == "/paste" or command == "/image":
581
+ # Attach image from clipboard
582
+ from ...clipboard import get_clipboard_image
583
+ image_data = get_clipboard_image()
584
+ if image_data:
585
+ base64_data, img_format = image_data
586
+ attached_images.append({"data": base64_data, "format": img_format})
587
+ console.print(f" [{Colors.SUCCESS}]✓ Image {len(attached_images)} attached[/{Colors.SUCCESS}]")
588
+ else:
589
+ console.print(f" [{Colors.WARNING}]No image in clipboard[/{Colors.WARNING}]")
590
+ console.print(f" [{Colors.DIM}]Copy an image first (Cmd+Shift+4 for screenshot)[/{Colors.DIM}]")
591
+
592
+ elif command == "/compact":
593
+ handle_compact(client, session_id)
594
+
410
595
  elif command == "/mcp":
411
596
  handle_mcp(args)
412
597
 
598
+ elif command == "/registry":
599
+ handle_registry(args)
600
+
413
601
  elif command == "/auth":
414
602
  handle_auth(args)
415
603
 
@@ -448,12 +636,31 @@ def run_interactive(
448
636
  handle_setup(args, client, renderer, model)
449
637
  return True
450
638
 
639
+ elif command == "/telegram":
640
+ handle_telegram(args)
641
+ return True
642
+
451
643
  else:
452
644
  console.print(f"[yellow]Unknown command: {command}[/yellow]")
453
645
  console.print("[dim]Type /help for available commands[/dim]")
454
646
 
455
647
  return True
456
648
 
649
+ # Check for first run and show onboarding
650
+ if is_first_run():
651
+ run_onboarding()
652
+
653
+ # Check for recent session to restore
654
+ recent_session = get_recent_session(client)
655
+ if recent_session:
656
+ choice, session_data = show_session_restore_prompt(recent_session)
657
+ if choice == "restore" and session_data:
658
+ session_id = session_data.get("name")
659
+ if session_data.get("mode"):
660
+ current_mode = AgentMode(session_data["mode"])
661
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] Session restored: {session_id}")
662
+ console.print()
663
+
457
664
  # Show welcome message
458
665
  from ... import __version__
459
666
 
@@ -472,20 +679,44 @@ def run_interactive(
472
679
  except Exception:
473
680
  pass
474
681
 
475
- # Welcome banner
476
- console.print()
477
- console.print(f"[bold cyan] Emdash Code[/bold cyan] [dim]v{__version__}[/dim]")
682
+ # Get current git branch
683
+ git_branch = None
684
+ try:
685
+ result = subprocess.run(
686
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
687
+ capture_output=True,
688
+ text=True,
689
+ timeout=5,
690
+ )
691
+ if result.returncode == 0:
692
+ git_branch = result.stdout.strip()
693
+ except Exception:
694
+ pass
695
+
478
696
  # Get display model name
479
697
  if model:
480
698
  display_model = model
481
699
  else:
482
700
  from emdash_core.agent.providers.factory import DEFAULT_MODEL
483
701
  display_model = DEFAULT_MODEL
484
- if git_repo:
485
- console.print(f"[dim]Repo:[/dim] [bold green]{git_repo}[/bold green] [dim]| Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {display_model}")
486
- else:
487
- console.print(f"[dim]Mode:[/dim] [bold]{current_mode.value}[/bold] [dim]| Model:[/dim] {display_model}")
488
- console.print()
702
+
703
+ # Shorten model name for display
704
+ if "/" in display_model:
705
+ display_model = display_model.split("/")[-1]
706
+
707
+ # Update toolbar variables for the bottom bar
708
+ toolbar_branch = git_branch
709
+ toolbar_model = display_model
710
+
711
+ # Welcome banner
712
+ show_welcome_banner(
713
+ version=__version__,
714
+ git_repo=git_repo,
715
+ git_branch=git_branch,
716
+ mode=current_mode.value,
717
+ model=display_model,
718
+ console=console,
719
+ )
489
720
 
490
721
  while True:
491
722
  try:
@@ -606,7 +837,7 @@ def run_interactive(
606
837
  if choice == "approve":
607
838
  current_mode = AgentMode.PLAN
608
839
  console.print()
609
- console.print("[bold green] Plan mode activated[/bold green]")
840
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.WARNING}]plan mode activated[/{Colors.WARNING}]")
610
841
  console.print()
611
842
  # Use the planmode approve endpoint
612
843
  stream = client.planmode_approve_stream(session_id)