deepagent-code 0.1.1__tar.gz → 0.1.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagent-code
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A Claude Code-style CLI for running LangGraph agents from the terminal
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -8,16 +8,29 @@ import json
8
8
  import os
9
9
  import re
10
10
  import sys
11
- import termios
12
11
  import threading
13
12
  import time
14
- import tty
15
13
  import uuid
16
14
  from pathlib import Path
17
15
  from typing import Any, Dict, List, Optional, Tuple
18
16
 
17
+ # Platform-specific imports for keyboard input
18
+ IS_WINDOWS = sys.platform == "win32"
19
+ if IS_WINDOWS:
20
+ import msvcrt
21
+ else:
22
+ import termios
23
+ import tty
24
+
19
25
  import click
20
26
 
27
+ # Try to import readline for tab completion (not available on all platforms)
28
+ try:
29
+ import readline
30
+ HAS_READLINE = True
31
+ except ImportError:
32
+ HAS_READLINE = False
33
+
21
34
  from deepagent_code.utils import (
22
35
  prepare_agent_input,
23
36
  stream_graph_updates,
@@ -27,12 +40,130 @@ from deepagent_code.utils import (
27
40
 
28
41
  # ANSI color codes (matching nanocode style)
29
42
  RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
43
+ ITALIC, UNDERLINE = "\033[3m", "\033[4m"
30
44
  BLUE, CYAN, GREEN, YELLOW, RED = "\033[34m", "\033[36m", "\033[32m", "\033[33m", "\033[31m"
45
+ MAGENTA, WHITE, GRAY = "\033[35m", "\033[37m", "\033[90m"
46
+
47
+ # Bright variants for gradient effects
48
+ BRIGHT_CYAN, BRIGHT_BLUE = "\033[96m", "\033[94m"
49
+ BRIGHT_GREEN, BRIGHT_YELLOW = "\033[92m", "\033[93m"
31
50
 
32
51
  # Spinner frames for thinking animation
33
52
  SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
34
53
 
35
54
 
55
+ # Version info
56
+ __version__ = "0.1.3"
57
+
58
+
59
+ # Slash command registry
60
+ class SlashCommand:
61
+ """Represents a slash command with its handler and metadata."""
62
+
63
+ def __init__(
64
+ self,
65
+ name: str,
66
+ handler: callable,
67
+ description: str,
68
+ aliases: Optional[List[str]] = None,
69
+ usage: Optional[str] = None,
70
+ ):
71
+ self.name = name
72
+ self.handler = handler
73
+ self.description = description
74
+ self.aliases = aliases or []
75
+ self.usage = usage or f"/{name}"
76
+
77
+ def execute(self, args: str, context: Dict[str, Any]) -> Optional[str]:
78
+ """Execute the command with given arguments and context."""
79
+ return self.handler(args, context)
80
+
81
+
82
+ class CommandRegistry:
83
+ """Registry for slash commands."""
84
+
85
+ def __init__(self):
86
+ self._commands: Dict[str, SlashCommand] = {}
87
+ self._alias_map: Dict[str, str] = {}
88
+
89
+ def register(self, command: SlashCommand):
90
+ """Register a slash command."""
91
+ self._commands[command.name] = command
92
+ for alias in command.aliases:
93
+ self._alias_map[alias] = command.name
94
+
95
+ def get(self, name: str) -> Optional[SlashCommand]:
96
+ """Get a command by name or alias."""
97
+ # Check if it's an alias
98
+ if name in self._alias_map:
99
+ name = self._alias_map[name]
100
+ return self._commands.get(name)
101
+
102
+ def all_commands(self) -> List[SlashCommand]:
103
+ """Get all registered commands."""
104
+ return list(self._commands.values())
105
+
106
+ def parse_input(self, user_input: str) -> Tuple[Optional[str], str]:
107
+ """Parse user input to extract command name and arguments.
108
+
109
+ Returns:
110
+ Tuple of (command_name, arguments) or (None, original_input) if not a command
111
+ """
112
+ if not user_input.startswith("/"):
113
+ return None, user_input
114
+
115
+ # Split into command and args
116
+ parts = user_input[1:].split(maxsplit=1)
117
+ cmd_name = parts[0].lower() if parts else ""
118
+ args = parts[1] if len(parts) > 1 else ""
119
+
120
+ return cmd_name, args
121
+
122
+
123
+ # Global command registry
124
+ command_registry = CommandRegistry()
125
+
126
+
127
+ def rl_wrap(code: str) -> str:
128
+ """Wrap ANSI escape code for readline to ignore in length calculations.
129
+
130
+ On terminals, ANSI codes are invisible but counted in string length.
131
+ This causes issues with line wrapping when using input().
132
+ Wrapping with \\001 and \\002 tells readline to ignore these characters.
133
+ """
134
+ if HAS_READLINE:
135
+ return f"\001{code}\002"
136
+ return code
137
+
138
+
139
+ def make_prompt(text: str = "❯", color: str = BRIGHT_BLUE) -> str:
140
+ """Create a prompt string with proper readline escaping for ANSI codes.
141
+
142
+ This prevents line wrapping issues on Windows and other terminals.
143
+ """
144
+ return f"{rl_wrap(BOLD)}{rl_wrap(color)}{text}{rl_wrap(RESET)} "
145
+
146
+
147
+ def register_command(
148
+ name: str,
149
+ description: str,
150
+ aliases: Optional[List[str]] = None,
151
+ usage: Optional[str] = None,
152
+ ):
153
+ """Decorator to register a slash command handler."""
154
+ def decorator(func):
155
+ command = SlashCommand(
156
+ name=name,
157
+ handler=func,
158
+ description=description,
159
+ aliases=aliases or [],
160
+ usage=usage,
161
+ )
162
+ command_registry.register(command)
163
+ return func
164
+ return decorator
165
+
166
+
36
167
  class Spinner:
37
168
  """A simple terminal spinner for showing activity with elapsed time."""
38
169
 
@@ -69,13 +200,44 @@ class Spinner:
69
200
  print("\r\033[2K", end="", flush=True)
70
201
 
71
202
 
72
- def separator() -> str:
73
- """Return a dim separator line."""
203
+ def get_terminal_width() -> int:
204
+ """Get terminal width, capped at 100 for readability."""
74
205
  try:
75
- width = min(os.get_terminal_size().columns, 80)
206
+ return min(os.get_terminal_size().columns, 100)
76
207
  except OSError:
77
- width = 80
78
- return f"{DIM}{'─' * width}{RESET}"
208
+ return 80
209
+
210
+
211
+ def separator(style: str = "light") -> str:
212
+ """Return a styled separator line.
213
+
214
+ Args:
215
+ style: 'light' for thin line, 'heavy' for thick line, 'dots' for dotted
216
+ """
217
+ width = get_terminal_width()
218
+ if style == "heavy":
219
+ return f"{DIM}{'━' * width}{RESET}"
220
+ elif style == "dots":
221
+ return f"{DIM}{'·' * width}{RESET}"
222
+ else:
223
+ return f"{DIM}{'─' * width}{RESET}"
224
+
225
+
226
+ def print_welcome():
227
+ """Print a welcome message with tips."""
228
+ tips = [
229
+ f"Type {CYAN}/help{RESET} for commands",
230
+ f"Use {CYAN}/c{RESET} to clear conversation",
231
+ f"Press {CYAN}Ctrl+C{RESET} to exit",
232
+ f"Press {CYAN}Tab{RESET} to autocomplete commands",
233
+ ]
234
+ tip = tips[int(time.time()) % len(tips)] # Rotate tips
235
+ print(f"\n{DIM}Tip: {tip}{RESET}\n")
236
+
237
+
238
+ def print_goodbye():
239
+ """Print a goodbye message."""
240
+ print(f"\n{DIM}Goodbye!{RESET}\n")
79
241
 
80
242
 
81
243
  def get_agent_name(graph) -> str:
@@ -95,11 +257,8 @@ def get_agent_name(graph) -> str:
95
257
 
96
258
 
97
259
  def print_header_box(agent_name: str, cwd: str):
98
- """Print a box-drawn header with the agent name."""
99
- try:
100
- term_width = min(os.get_terminal_size().columns, 80)
101
- except OSError:
102
- term_width = 80
260
+ """Print an elegant header with the agent name and version."""
261
+ term_width = get_terminal_width()
103
262
 
104
263
  # Box drawing characters
105
264
  TL, TR, BL, BR = "╭", "╮", "╰", "╯" # corners
@@ -113,17 +272,28 @@ def print_header_box(agent_name: str, cwd: str):
113
272
  cwd_display = cwd if len(cwd) <= inner_width else "..." + cwd[-(inner_width - 3):]
114
273
  cwd_line = cwd_display.center(inner_width)
115
274
 
116
- # Print the box
117
- print(f"{CYAN}{TL}{H * (term_width - 2)}{TR}{RESET}")
118
- print(f"{CYAN}{V}{RESET} {BOLD}{title_line}{RESET} {CYAN}{V}{RESET}")
275
+ # Print the box with gradient-style coloring
276
+ print()
277
+ print(f"{BRIGHT_CYAN}{TL}{H * (term_width - 2)}{TR}{RESET}")
278
+ print(f"{BRIGHT_CYAN}{V}{RESET} {BOLD}{BRIGHT_CYAN}{title_line}{RESET} {BRIGHT_CYAN}{V}{RESET}")
119
279
  print(f"{CYAN}{V}{RESET} {DIM}{cwd_line}{RESET} {CYAN}{V}{RESET}")
120
280
  print(f"{CYAN}{BL}{H * (term_width - 2)}{BR}{RESET}")
121
- print()
122
281
 
123
282
 
124
283
  def render_markdown(text: str) -> str:
125
- """Simple markdown rendering for **bold** text."""
126
- return re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
284
+ """Render markdown formatting for terminal display.
285
+
286
+ Supports: **bold**, *italic*, `code`, [links](url)
287
+ """
288
+ # Bold: **text**
289
+ text = re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
290
+ # Italic: *text* (but not inside **)
291
+ text = re.sub(r"(?<!\*)\*([^*]+?)\*(?!\*)", f"{ITALIC}\\1{RESET}", text)
292
+ # Inline code: `code`
293
+ text = re.sub(r"`([^`]+?)`", f"{CYAN}\\1{RESET}", text)
294
+ # Links: [text](url) - show text in underline
295
+ text = re.sub(r"\[([^\]]+?)\]\([^)]+?\)", f"{UNDERLINE}\\1{RESET}", text)
296
+ return text
127
297
 
128
298
 
129
299
  def parse_agent_spec(agent_spec: str) -> Tuple[str, str]:
@@ -329,66 +499,87 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
329
499
  if verbose:
330
500
  print(f"{DIM}[{node}]{RESET} {text}", end="")
331
501
  else:
332
- # Print text output with cyan bullet (only on first chunk or after newline)
502
+ # Print text output with cyan bullet
333
503
  print(f"{CYAN}⏺{RESET} {render_markdown(text)}", end="")
334
504
 
335
- # Handle tool calls - green bullet with tool name
505
+ # Handle tool calls - green tool name
336
506
  elif "tool_calls" in chunk:
337
507
  for tool_call in chunk["tool_calls"]:
338
508
  tool_name = tool_call["name"]
339
509
  args = tool_call.get("args", {})
340
510
  arg_preview = get_tool_arg_preview(args)
341
511
 
342
- print(f"\n{GREEN} {tool_name.capitalize()}{RESET}({DIM}{arg_preview}{RESET})")
512
+ print(f"\n{GREEN} {tool_name}{RESET}")
513
+ if arg_preview:
514
+ print(f" {DIM}└─ {arg_preview}{RESET}")
343
515
 
344
516
  # Handle tool results - indented with result preview
345
517
  elif "tool_result" in chunk:
346
518
  result = chunk.get("tool_result", "")
347
519
  preview = format_result_preview(str(result))
348
- print(f" {DIM}{preview}{RESET}")
520
+ print(f" {DIM}{preview}{RESET}")
349
521
 
350
522
  elif status == "interrupt":
351
523
  interrupt_data = chunk.get("interrupt", {})
352
524
  action_requests = interrupt_data.get("action_requests", [])
353
525
 
354
- print(f"\n{YELLOW} Interrupt{RESET}")
526
+ print(f"\n{YELLOW} Action Required{RESET}")
355
527
  if action_requests:
356
528
  for i, action in enumerate(action_requests):
357
529
  tool = action.get('tool', 'unknown')
358
530
  args_preview = get_tool_arg_preview(action.get('args', {}))
359
- print(f" {DIM}{i + 1}. {tool}({args_preview}){RESET}")
531
+ print(f" {DIM}{i + 1}. {tool}{RESET}")
532
+ if args_preview:
533
+ print(f" {DIM}└─ {args_preview}{RESET}")
360
534
 
361
535
  elif status == "complete":
362
536
  pass # No output on complete (nanocode style)
363
537
 
364
538
  elif status == "error":
365
539
  error_msg = chunk.get("error", "Unknown error")
366
- print(f"\n{RED} Error: {error_msg}{RESET}")
540
+ print(f"\n{RED} Error: {error_msg}{RESET}")
367
541
 
368
542
 
369
543
  def get_key() -> str:
370
- """Read a single keypress from stdin."""
371
- fd = sys.stdin.fileno()
372
- old_settings = termios.tcgetattr(fd)
373
- try:
374
- tty.setraw(fd)
375
- ch = sys.stdin.read(1)
376
- # Handle escape sequences (arrow keys)
377
- if ch == '\x1b':
378
- ch2 = sys.stdin.read(1)
379
- if ch2 == '[':
380
- ch3 = sys.stdin.read(1)
381
- if ch3 == 'A':
382
- return 'up'
383
- elif ch3 == 'B':
384
- return 'down'
385
- elif ch == '\r' or ch == '\n':
544
+ """Read a single keypress from stdin (cross-platform)."""
545
+ if IS_WINDOWS:
546
+ # Windows implementation using msvcrt
547
+ ch = msvcrt.getch()
548
+ if ch in (b'\x00', b'\xe0'): # Special keys (arrows, function keys)
549
+ ch2 = msvcrt.getch()
550
+ if ch2 == b'H':
551
+ return 'up'
552
+ elif ch2 == b'P':
553
+ return 'down'
554
+ return ch2.decode('utf-8', errors='ignore')
555
+ elif ch == b'\r':
386
556
  return 'enter'
387
- elif ch == '\x03': # Ctrl+C
557
+ elif ch == b'\x03': # Ctrl+C
388
558
  return 'ctrl-c'
389
- return ch
390
- finally:
391
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
559
+ return ch.decode('utf-8', errors='ignore')
560
+ else:
561
+ # Unix implementation using termios/tty
562
+ fd = sys.stdin.fileno()
563
+ old_settings = termios.tcgetattr(fd)
564
+ try:
565
+ tty.setraw(fd)
566
+ ch = sys.stdin.read(1)
567
+ # Handle escape sequences (arrow keys)
568
+ if ch == '\x1b':
569
+ ch2 = sys.stdin.read(1)
570
+ if ch2 == '[':
571
+ ch3 = sys.stdin.read(1)
572
+ if ch3 == 'A':
573
+ return 'up'
574
+ elif ch3 == 'B':
575
+ return 'down'
576
+ elif ch == '\r' or ch == '\n':
577
+ return 'enter'
578
+ elif ch == '\x03': # Ctrl+C
579
+ return 'ctrl-c'
580
+ return ch
581
+ finally:
582
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
392
583
 
393
584
 
394
585
  def select_option(options: List[str], prompt: str = "Select an option:") -> int:
@@ -476,7 +667,7 @@ def handle_interrupt_input(num_actions: int = 1) -> List[Dict[str, Any]]:
476
667
  return [{"type": "reject"} for _ in range(num_actions)]
477
668
  elif choice == 2:
478
669
  print("Enter your decision as JSON (will be applied to all actions):")
479
- json_str = input(f"{BOLD}{BLUE}{RESET} ").strip()
670
+ json_str = input(make_prompt("❯", BLUE)).strip()
480
671
  try:
481
672
  decision = json.loads(json_str)
482
673
  return [decision for _ in range(num_actions)]
@@ -581,6 +772,320 @@ def run_single_turn_sync(
581
772
  return time.time() - start_time
582
773
 
583
774
 
775
+ def print_help():
776
+ """Print formatted help information."""
777
+ print(f"\n{BOLD}{BRIGHT_CYAN}Commands{RESET}")
778
+ print(f"{DIM}{'─' * 40}{RESET}")
779
+
780
+ # Get all registered commands and display them
781
+ commands = command_registry.all_commands()
782
+ for cmd in sorted(commands, key=lambda c: c.name):
783
+ aliases_str = ""
784
+ if cmd.aliases:
785
+ aliases_str = f", {CYAN}/{RESET}, {CYAN}/".join([""] + cmd.aliases)[4:]
786
+ print(f" {CYAN}/{cmd.name}{RESET}{aliases_str}")
787
+ print(f" {DIM}{cmd.description}{RESET}")
788
+
789
+ print()
790
+ print(f"{BOLD}{BRIGHT_CYAN}Shortcuts{RESET}")
791
+ print(f"{DIM}{'─' * 40}{RESET}")
792
+ print(f" {CYAN}Tab{RESET} Autocomplete commands")
793
+ print(f" {CYAN}Ctrl+C{RESET} Exit at any time")
794
+ print(f" {CYAN}↑/↓{RESET} Navigate options")
795
+ print()
796
+
797
+
798
+ # --- Built-in Slash Commands ---
799
+
800
+ @register_command(
801
+ name="help",
802
+ description="Show this help message",
803
+ aliases=["h", "?"],
804
+ )
805
+ def cmd_help(args: str, context: Dict[str, Any]) -> Optional[str]:
806
+ """Display help information."""
807
+ if args:
808
+ # Show help for a specific command
809
+ cmd = command_registry.get(args)
810
+ if cmd:
811
+ print(f"\n{BOLD}{BRIGHT_CYAN}/{cmd.name}{RESET}")
812
+ print(f" {cmd.description}")
813
+ if cmd.aliases:
814
+ print(f" {DIM}Aliases: /{', /'.join(cmd.aliases)}{RESET}")
815
+ if cmd.usage:
816
+ print(f" {DIM}Usage: {cmd.usage}{RESET}")
817
+ print()
818
+ else:
819
+ print(f"{YELLOW}Unknown command: /{args}{RESET}")
820
+ else:
821
+ print_help()
822
+ return None
823
+
824
+
825
+ @register_command(
826
+ name="quit",
827
+ description="Exit the CLI",
828
+ aliases=["q", "exit"],
829
+ )
830
+ def cmd_quit(args: str, context: Dict[str, Any]) -> Optional[str]:
831
+ """Exit the CLI."""
832
+ return "exit" # Special return value to signal exit
833
+
834
+
835
+ @register_command(
836
+ name="clear",
837
+ description="Clear conversation history",
838
+ aliases=["c"],
839
+ )
840
+ def cmd_clear(args: str, context: Dict[str, Any]) -> Optional[str]:
841
+ """Clear the conversation history."""
842
+ context["config"]["configurable"]["thread_id"] = str(uuid.uuid4())
843
+ print(f"\n{GREEN}✓ Conversation cleared{RESET}\n")
844
+ return None
845
+
846
+
847
+ @register_command(
848
+ name="version",
849
+ description="Show version information",
850
+ aliases=["v"],
851
+ )
852
+ def cmd_version(args: str, context: Dict[str, Any]) -> Optional[str]:
853
+ """Display version information."""
854
+ print(f"\n{BOLD}{BRIGHT_CYAN}deepagent-code{RESET} v{__version__}")
855
+ agent_name = context.get("agent_name", "Unknown")
856
+ print(f"{DIM}Agent: {agent_name}{RESET}\n")
857
+ return None
858
+
859
+
860
+ @register_command(
861
+ name="status",
862
+ description="Show current session status",
863
+ aliases=["s"],
864
+ )
865
+ def cmd_status(args: str, context: Dict[str, Any]) -> Optional[str]:
866
+ """Display current session status."""
867
+ config = context.get("config", {})
868
+ thread_id = config.get("configurable", {}).get("thread_id", "N/A")
869
+ agent_name = context.get("agent_name", "Unknown")
870
+ verbose = context.get("verbose", False)
871
+ use_async = context.get("use_async", False)
872
+ stream_mode = context.get("stream_mode", "updates")
873
+
874
+ print(f"\n{BOLD}{BRIGHT_CYAN}Session Status{RESET}")
875
+ print(f"{DIM}{'─' * 30}{RESET}")
876
+ print(f" {DIM}Agent:{RESET} {agent_name}")
877
+ print(f" {DIM}Thread ID:{RESET} {thread_id[:8]}...")
878
+ print(f" {DIM}Mode:{RESET} {'async' if use_async else 'sync'}")
879
+ print(f" {DIM}Stream:{RESET} {stream_mode}")
880
+ print(f" {DIM}Verbose:{RESET} {'on' if verbose else 'off'}")
881
+ print(f" {DIM}CWD:{RESET} {os.getcwd()}")
882
+ print()
883
+ return None
884
+
885
+
886
+ @register_command(
887
+ name="config",
888
+ description="Show or set configuration",
889
+ aliases=["cfg"],
890
+ usage="/config [key] [value]",
891
+ )
892
+ def cmd_config(args: str, context: Dict[str, Any]) -> Optional[str]:
893
+ """Show or modify configuration."""
894
+ config = context.get("config", {})
895
+
896
+ if not args:
897
+ # Show current config
898
+ print(f"\n{BOLD}{BRIGHT_CYAN}Configuration{RESET}")
899
+ print(f"{DIM}{'─' * 30}{RESET}")
900
+ for key, value in config.items():
901
+ if isinstance(value, dict):
902
+ print(f" {CYAN}{key}:{RESET}")
903
+ for k, v in value.items():
904
+ # Truncate long values
905
+ v_str = str(v)
906
+ if len(v_str) > 30:
907
+ v_str = v_str[:30] + "..."
908
+ print(f" {DIM}{k}:{RESET} {v_str}")
909
+ else:
910
+ print(f" {CYAN}{key}:{RESET} {value}")
911
+ print()
912
+ else:
913
+ parts = args.split(maxsplit=1)
914
+ if len(parts) == 1:
915
+ # Show specific config key
916
+ key = parts[0]
917
+ if key in config:
918
+ print(f"\n{CYAN}{key}:{RESET} {config[key]}\n")
919
+ elif "configurable" in config and key in config["configurable"]:
920
+ print(f"\n{CYAN}{key}:{RESET} {config['configurable'][key]}\n")
921
+ else:
922
+ print(f"{YELLOW}Unknown config key: {key}{RESET}")
923
+ else:
924
+ # Set config value
925
+ key, value = parts
926
+ if key == "verbose":
927
+ context["verbose"] = value.lower() in ("true", "1", "on", "yes")
928
+ print(f"{GREEN}✓ Set verbose = {context['verbose']}{RESET}")
929
+ else:
930
+ print(f"{YELLOW}Cannot modify {key} at runtime{RESET}")
931
+ return None
932
+
933
+
934
+ @register_command(
935
+ name="history",
936
+ description="Show recent messages (if available)",
937
+ aliases=["hist"],
938
+ )
939
+ def cmd_history(args: str, context: Dict[str, Any]) -> Optional[str]:
940
+ """Display conversation history if available."""
941
+ graph = context.get("graph")
942
+ config = context.get("config", {})
943
+
944
+ if graph is None:
945
+ print(f"{YELLOW}No graph available{RESET}")
946
+ return None
947
+
948
+ try:
949
+ # Try to get state from the graph's checkpointer
950
+ if hasattr(graph, "get_state"):
951
+ state = graph.get_state(config)
952
+ if state and hasattr(state, "values"):
953
+ messages = state.values.get("messages", [])
954
+ if messages:
955
+ print(f"\n{BOLD}{BRIGHT_CYAN}Conversation History{RESET}")
956
+ print(f"{DIM}{'─' * 40}{RESET}")
957
+
958
+ # Show last N messages
959
+ limit = 10
960
+ if args:
961
+ try:
962
+ limit = int(args)
963
+ except ValueError:
964
+ pass
965
+
966
+ for msg in messages[-limit:]:
967
+ role = getattr(msg, "type", "unknown")
968
+ content = getattr(msg, "content", str(msg))
969
+
970
+ if role == "human":
971
+ print(f"\n {BRIGHT_BLUE}You:{RESET}")
972
+ elif role == "ai":
973
+ print(f"\n {BRIGHT_CYAN}Agent:{RESET}")
974
+ else:
975
+ print(f"\n {DIM}{role}:{RESET}")
976
+
977
+ # Truncate long content
978
+ if len(content) > 200:
979
+ content = content[:200] + "..."
980
+ print(f" {DIM}{content}{RESET}")
981
+ print()
982
+ else:
983
+ print(f"{DIM}No messages in history{RESET}")
984
+ else:
985
+ print(f"{DIM}No state available{RESET}")
986
+ else:
987
+ print(f"{DIM}Graph does not support state retrieval{RESET}")
988
+ except Exception as e:
989
+ print(f"{DIM}Could not retrieve history: {e}{RESET}")
990
+
991
+ return None
992
+
993
+
994
+ @register_command(
995
+ name="reset",
996
+ description="Reset the session (clear history and restart)",
997
+ aliases=["restart"],
998
+ )
999
+ def cmd_reset(args: str, context: Dict[str, Any]) -> Optional[str]:
1000
+ """Reset the session."""
1001
+ context["config"]["configurable"]["thread_id"] = str(uuid.uuid4())
1002
+ print(f"\n{GREEN}✓ Session reset{RESET}")
1003
+ print(f"{DIM}New thread ID: {context['config']['configurable']['thread_id'][:8]}...{RESET}\n")
1004
+ return None
1005
+
1006
+
1007
+ @register_command(
1008
+ name="verbose",
1009
+ description="Toggle verbose output mode",
1010
+ usage="/verbose [on|off]",
1011
+ )
1012
+ def cmd_verbose(args: str, context: Dict[str, Any]) -> Optional[str]:
1013
+ """Toggle or show verbose output mode."""
1014
+ verbose = context.get("verbose", False)
1015
+ if args:
1016
+ if args.lower() in ("on", "true", "1"):
1017
+ context["verbose"] = True
1018
+ print(f"{GREEN}✓ Verbose mode enabled{RESET}")
1019
+ elif args.lower() in ("off", "false", "0"):
1020
+ context["verbose"] = False
1021
+ print(f"{GREEN}✓ Verbose mode disabled{RESET}")
1022
+ else:
1023
+ print(f"{DIM}Verbose mode: {'on' if verbose else 'off'}{RESET}")
1024
+ print(f"{DIM}Use /verbose on or /verbose off to change{RESET}")
1025
+ return None
1026
+
1027
+
1028
+ def get_command_suggestions(partial: str) -> List[str]:
1029
+ """Get command suggestions based on partial input.
1030
+
1031
+ Args:
1032
+ partial: Partial command name (without leading /)
1033
+
1034
+ Returns:
1035
+ List of matching command names
1036
+ """
1037
+ partial_lower = partial.lower()
1038
+ suggestions = []
1039
+
1040
+ for cmd in command_registry.all_commands():
1041
+ # Check main command name
1042
+ if cmd.name.startswith(partial_lower):
1043
+ suggestions.append(cmd.name)
1044
+ # Check aliases
1045
+ for alias in cmd.aliases:
1046
+ if alias.startswith(partial_lower) and cmd.name not in suggestions:
1047
+ suggestions.append(cmd.name)
1048
+
1049
+ return sorted(suggestions)
1050
+
1051
+
1052
+ def command_completer(text: str, state: int) -> Optional[str]:
1053
+ """Readline completer for slash commands.
1054
+
1055
+ Args:
1056
+ text: Current text being completed
1057
+ state: State index for multiple completions
1058
+
1059
+ Returns:
1060
+ Next completion or None
1061
+ """
1062
+ # Only complete if starting with /
1063
+ if not text.startswith("/"):
1064
+ return None
1065
+
1066
+ partial = text[1:] # Remove leading /
1067
+ suggestions = ["/" + s for s in get_command_suggestions(partial)]
1068
+
1069
+ if state < len(suggestions):
1070
+ return suggestions[state]
1071
+ return None
1072
+
1073
+
1074
+ def setup_readline_completion():
1075
+ """Set up readline for tab completion of slash commands."""
1076
+ if not HAS_READLINE:
1077
+ return
1078
+
1079
+ readline.set_completer(command_completer)
1080
+ readline.set_completer_delims(" \t\n")
1081
+
1082
+ # Use tab for completion
1083
+ if sys.platform == "darwin":
1084
+ readline.parse_and_bind("bind ^I rl_complete")
1085
+ else:
1086
+ readline.parse_and_bind("tab: complete")
1087
+
1088
+
584
1089
  def run_conversation_loop(
585
1090
  graph,
586
1091
  config: Dict[str, Any],
@@ -595,14 +1100,31 @@ def run_conversation_loop(
595
1100
  Run a continuous conversation loop with the LangGraph agent.
596
1101
  Styled after Claude Code / nanocode.
597
1102
  """
1103
+ # Set up tab completion for slash commands
1104
+ setup_readline_completion()
1105
+
598
1106
  # Print box-drawn header with agent name
599
1107
  print_header_box(agent_name, os.getcwd())
600
1108
 
1109
+ # Print welcome message with tips
1110
+ print_welcome()
1111
+
1112
+ # Create command context (mutable dict that commands can modify)
1113
+ command_context = {
1114
+ "graph": graph,
1115
+ "config": config,
1116
+ "agent_name": agent_name,
1117
+ "use_async": use_async,
1118
+ "interactive": interactive,
1119
+ "verbose": verbose,
1120
+ "stream_mode": stream_mode,
1121
+ }
1122
+
601
1123
  # Process initial message if provided
602
1124
  if initial_message:
603
- print(separator())
604
- print(f"{BOLD}{BLUE}❯{RESET} {initial_message}")
605
- print(separator())
1125
+ print(f"\n{BOLD}{BRIGHT_BLUE}You{RESET}")
1126
+ print(f"{initial_message}")
1127
+ print()
606
1128
 
607
1129
  if use_async:
608
1130
  duration = asyncio.run(
@@ -616,29 +1138,40 @@ def run_conversation_loop(
616
1138
  # Main conversation loop
617
1139
  while True:
618
1140
  try:
619
- print(separator())
620
- user_input = input(f"{BOLD}{BLUE}❯{RESET} ").strip()
621
- print(separator())
1141
+ print(separator("dots"))
1142
+ user_input = input(make_prompt()).strip()
622
1143
 
623
1144
  if not user_input:
624
1145
  continue
625
1146
 
626
- # Handle special commands
627
- if user_input in ("/q", "/quit", "/exit", "exit"):
628
- break
629
-
630
- if user_input == "/c":
631
- # Generate new thread_id to start fresh conversation
632
- config["configurable"]["thread_id"] = str(uuid.uuid4())
633
- print(f"{GREEN}⏺ Cleared conversation{RESET}")
1147
+ # Check if it's a slash command
1148
+ cmd_name, cmd_args = command_registry.parse_input(user_input)
1149
+
1150
+ if cmd_name is not None:
1151
+ # It's a slash command
1152
+ cmd = command_registry.get(cmd_name)
1153
+ if cmd:
1154
+ result = cmd.execute(cmd_args, command_context)
1155
+ # Update local vars from context (commands may modify these)
1156
+ verbose = command_context.get("verbose", verbose)
1157
+ if result == "exit":
1158
+ break
1159
+ else:
1160
+ # Show suggestions for unknown commands
1161
+ suggestions = get_command_suggestions(cmd_name)
1162
+ print(f"{YELLOW}Unknown command: /{cmd_name}{RESET}")
1163
+ if suggestions:
1164
+ suggestion_str = ", ".join([f"/{s}" for s in suggestions[:3]])
1165
+ print(f"{DIM}Did you mean: {suggestion_str}?{RESET}")
1166
+ else:
1167
+ print(f"{DIM}Type /help to see available commands{RESET}")
634
1168
  continue
635
1169
 
636
- if user_input in ("/h", "/help"):
637
- print(f"\n{BOLD}Commands:{RESET}")
638
- print(f" /q, /quit, exit - Exit")
639
- print(f" /c - Clear conversation")
640
- print(f" /h, /help - Show this help\n")
641
- continue
1170
+ # Handle "exit" as a special case (without slash)
1171
+ if user_input.lower() == "exit":
1172
+ break
1173
+
1174
+ print() # Space before response
642
1175
 
643
1176
  # Run the agent
644
1177
  if use_async:
@@ -653,7 +1186,10 @@ def run_conversation_loop(
653
1186
  except (EOFError, KeyboardInterrupt):
654
1187
  break
655
1188
  except Exception as err:
656
- print(f"{RED} Error: {err}{RESET}")
1189
+ print(f"\n{RED} Error: {err}{RESET}\n")
1190
+
1191
+ # Print goodbye message
1192
+ print_goodbye()
657
1193
 
658
1194
 
659
1195
  @click.command()
@@ -761,9 +1297,12 @@ def main(
761
1297
  if workspace_path.exists():
762
1298
  os.chdir(workspace_path)
763
1299
 
764
- # Load the graph
765
- print(f"{DIM}Loading {final_spec}...{RESET}")
1300
+ # Load the graph with a spinner
1301
+ spinner = Spinner("Loading agent")
1302
+ spinner.start()
766
1303
  graph, final_graph_name = load_graph(final_spec, default_graph_name)
1304
+ spinner.stop()
1305
+ print(f"{GREEN}✓{RESET} {DIM}Loaded {final_spec}{RESET}")
767
1306
 
768
1307
  # Parse config
769
1308
  config_dict = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagent-code
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A Claude Code-style CLI for running LangGraph agents from the terminal
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepagent-code"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "A Claude Code-style CLI for running LangGraph agents from the terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes