nc1709 1.15.4__py3-none-any.whl → 1.18.8__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.
nc1709/cli_ui.py CHANGED
@@ -9,6 +9,7 @@ Provides rich, real-time visual feedback for CLI operations with:
9
9
  - Non-blocking output with line replacement
10
10
  - Streaming text support
11
11
  - Text wrapping for clean output
12
+ - Dynamic thinking messages for user engagement
12
13
  """
13
14
  import os
14
15
  import re
@@ -22,6 +23,16 @@ from enum import Enum
22
23
  from dataclasses import dataclass, field
23
24
  from contextlib import contextmanager
24
25
 
26
+ # Import dynamic thinking messages
27
+ try:
28
+ from .thinking_messages import (
29
+ ThinkingPhase, get_thinking_message, get_tool_message,
30
+ get_progress_message, start_thinking, set_phase, should_update_message
31
+ )
32
+ HAS_THINKING_MESSAGES = True
33
+ except ImportError:
34
+ HAS_THINKING_MESSAGES = False
35
+
25
36
 
26
37
  # =============================================================================
27
38
  # ANSI Color Codes
@@ -82,7 +93,7 @@ if not sys.stdout.isatty():
82
93
  # =============================================================================
83
94
 
84
95
  class Icons:
85
- """Unicode icons for status display"""
96
+ """Unicode icons for status display - Claude Code style"""
86
97
  # Status indicators
87
98
  THINKING = "✻"
88
99
  SUCCESS = "✓"
@@ -90,11 +101,16 @@ class Icons:
90
101
  WARNING = "⚠"
91
102
  INFO = "ℹ"
92
103
 
104
+ # Claude Code style
105
+ BULLET = "●" # Yellow bullet for tool calls
106
+ CORNER = "⎿" # Corner for output indentation
107
+ HOLLOW = "○" # Hollow circle for pending
108
+
93
109
  # Spinners
94
110
  DOTS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
95
111
  BRAILLE = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]
96
112
 
97
- # Tree
113
+ # Tree (kept for backward compat)
98
114
  TREE_BRANCH = "└─"
99
115
  TREE_VERTICAL = "│"
100
116
  TREE_TEE = "├─"
@@ -174,7 +190,8 @@ class ActionSpinner:
174
190
  self,
175
191
  message: str = "Processing",
176
192
  spinner_chars: List[str] = None,
177
- interval: float = 0.08
193
+ interval: float = 0.08,
194
+ dynamic_messages: bool = True
178
195
  ):
179
196
  """Initialize the action spinner.
180
197
 
@@ -182,10 +199,13 @@ class ActionSpinner:
182
199
  message: Initial status message
183
200
  spinner_chars: Characters for spinner animation
184
201
  interval: Animation interval in seconds
202
+ dynamic_messages: Whether to use dynamic thinking messages
185
203
  """
186
204
  self.message = message
205
+ self.initial_message = message
187
206
  self.spinner_chars = spinner_chars or Icons.DOTS
188
207
  self.interval = interval
208
+ self.dynamic_messages = dynamic_messages and HAS_THINKING_MESSAGES
189
209
 
190
210
  self.running = False
191
211
  self.frame_index = 0
@@ -195,10 +215,16 @@ class ActionSpinner:
195
215
  self._thread: Optional[threading.Thread] = None
196
216
  self._lock = threading.Lock()
197
217
  self._last_render_lines = 0
218
+ self._start_time: Optional[float] = None
219
+ self._last_message_update: float = 0
220
+ self._message_update_interval: float = 4.0 # Update message every 4 seconds
198
221
 
199
222
  def start(self) -> "ActionSpinner":
200
223
  """Start the spinner animation."""
201
224
  self.running = True
225
+ self._start_time = time.time()
226
+ if self.dynamic_messages:
227
+ start_thinking()
202
228
  sys.stdout.write(Color.HIDE_CURSOR)
203
229
  sys.stdout.flush()
204
230
  self._thread = threading.Thread(target=self._animate, daemon=True)
@@ -216,6 +242,13 @@ class ActionSpinner:
216
242
  def _animate(self) -> None:
217
243
  """Animation loop running in background thread."""
218
244
  while self.running:
245
+ # Update message dynamically if enabled
246
+ if self.dynamic_messages and self._start_time:
247
+ now = time.time()
248
+ if now - self._last_message_update >= self._message_update_interval:
249
+ self._last_message_update = now
250
+ self.message = get_progress_message()
251
+
219
252
  self._render()
220
253
  self.frame_index = (self.frame_index + 1) % len(self.spinner_chars)
221
254
  time.sleep(self.interval)
@@ -515,8 +548,17 @@ def status(message: str, state: str = "info") -> None:
515
548
  print(f"{icon} {message}")
516
549
 
517
550
 
518
- def thinking(message: str) -> None:
519
- """Print a thinking/processing status."""
551
+ def thinking(message: str = None, dynamic: bool = True) -> None:
552
+ """Print a thinking/processing status.
553
+
554
+ Args:
555
+ message: Custom message (optional, uses dynamic message if None)
556
+ dynamic: Whether to use dynamic thinking messages
557
+ """
558
+ if message is None and dynamic and HAS_THINKING_MESSAGES:
559
+ message = get_thinking_message()
560
+ elif message is None:
561
+ message = "Thinking..."
520
562
  status(message, "thinking")
521
563
 
522
564
 
@@ -541,25 +583,67 @@ def info(message: str) -> None:
541
583
 
542
584
 
543
585
  # =============================================================================
544
- # Action Logging
586
+ # Action Logging - Claude Code Style
545
587
  # =============================================================================
546
588
 
547
589
  def log_action(action: str, target: str, state: str = "running") -> None:
548
- """Log a tool/action with its target.
590
+ """Log a tool/action with its target in Claude Code style.
591
+
592
+ Format: ● Action(target)
549
593
 
550
594
  Args:
551
595
  action: Action name (Read, Write, Bash, etc.)
552
596
  target: Action target (filename, command, etc.)
553
597
  state: Action state (running, success, error)
554
598
  """
555
- states = {
556
- "running": f"{Color.BLUE}{Color.RESET}",
557
- "success": f"{Color.GREEN}{Icons.SUCCESS}{Color.RESET}",
558
- "error": f"{Color.RED}{Icons.FAILURE}{Color.RESET}",
559
- "pending": f"{Color.DIM}○{Color.RESET}",
560
- }
561
- icon = states.get(state, states["running"])
562
- print(f" {Icons.TREE_BRANCH} {icon} {action}({Color.CYAN}{target}{Color.RESET})")
599
+ # Yellow bullet for all tool calls (Claude Code style)
600
+ bullet = f"{Color.YELLOW}{Icons.BULLET}{Color.RESET}"
601
+ print(f"{bullet} {Color.BOLD}{action}{Color.RESET}({Color.CYAN}{target}{Color.RESET})")
602
+
603
+
604
+ def log_output(output: str, is_error: bool = False, max_lines: int = 10) -> None:
605
+ """Log tool output with Claude Code style corner indentation.
606
+
607
+ Format:
608
+ ⎿ Output line 1
609
+ Output line 2
610
+
611
+ Args:
612
+ output: Output text to display
613
+ is_error: Whether this is error output
614
+ max_lines: Max lines to show before truncating (0 = no limit)
615
+ """
616
+ if not output or not output.strip():
617
+ return
618
+
619
+ lines = output.strip().split('\n')
620
+ show_truncation = max_lines > 0 and len(lines) > max_lines
621
+
622
+ if show_truncation:
623
+ display_lines = lines[:max_lines]
624
+ else:
625
+ display_lines = lines
626
+
627
+ # Color for output
628
+ text_color = Color.RED if is_error else Color.DIM
629
+
630
+ for i, line in enumerate(display_lines):
631
+ if i == 0:
632
+ # First line gets the corner symbol
633
+ prefix = f" {Color.DIM}{Icons.CORNER}{Color.RESET} "
634
+ else:
635
+ # Subsequent lines get spacing to align
636
+ prefix = " "
637
+
638
+ if is_error and i == 0:
639
+ # Error prefix for first line
640
+ print(f"{prefix}{Color.RED}Error:{Color.RESET} {text_color}{line}{Color.RESET}")
641
+ else:
642
+ print(f"{prefix}{text_color}{line}{Color.RESET}")
643
+
644
+ if show_truncation:
645
+ remaining = len(lines) - max_lines
646
+ print(f" {Color.DIM}... (+{remaining} more lines){Color.RESET}")
563
647
 
564
648
 
565
649
  # =============================================================================
@@ -1015,27 +1099,24 @@ def print_response(response: str, width_percentage: float = 0.75) -> None:
1015
1099
  # =============================================================================
1016
1100
 
1017
1101
  def demo():
1018
- """Demonstrate CLI UI components."""
1102
+ """Demonstrate CLI UI components - Claude Code style."""
1019
1103
  print("\n" + "=" * 60)
1020
- print("NC1709 CLI UI Demo")
1104
+ print("NC1709 CLI UI Demo - Claude Code Style")
1021
1105
  print("=" * 60 + "\n")
1022
1106
 
1023
- # Demo 1: Simple spinner
1024
- print("Demo 1: Action Spinner")
1107
+ # Demo 1: Claude Code style tool calls
1108
+ print("Demo 1: Tool Calls (Claude Code Style)")
1025
1109
  print("-" * 40)
1026
1110
 
1027
- with action_spinner("Analyzing your request") as spinner:
1028
- time.sleep(0.5)
1029
- spinner.update("Planning implementation steps")
1030
- time.sleep(0.5)
1031
- idx = spinner.add_action("Read", "main.py")
1032
- time.sleep(0.3)
1033
- spinner.complete_action(idx)
1034
- idx = spinner.add_action("Write", "feature.py")
1035
- time.sleep(0.3)
1036
- spinner.complete_action(idx)
1037
- time.sleep(0.2)
1038
- spinner.success("Implementation complete")
1111
+ # Simulate tool calls with output
1112
+ log_action("Read", "main.py")
1113
+ log_output("def main():\n print('Hello World')\n\nif __name__ == '__main__':\n main()")
1114
+
1115
+ log_action("Bash", "python main.py")
1116
+ log_output("Hello World")
1117
+
1118
+ log_action("Bash", "python broken.py")
1119
+ log_output("ModuleNotFoundError: No module named 'nonexistent'", is_error=True)
1039
1120
 
1040
1121
  print()
1041
1122
 
@@ -1049,29 +1130,33 @@ def demo():
1049
1130
  error("Failed to parse config.json")
1050
1131
  print()
1051
1132
 
1052
- # Demo 3: Task display
1053
- print("Demo 3: Task Display")
1133
+ # Demo 3: Multi-line output with truncation
1134
+ print("Demo 3: Multi-line Output")
1135
+ print("-" * 40)
1136
+ log_action("Grep", "TODO")
1137
+ long_output = "\n".join([f"src/file{i}.py:42: # TODO: implement feature {i}" for i in range(15)])
1138
+ log_output(long_output, max_lines=5)
1139
+
1140
+ print()
1141
+
1142
+ # Demo 4: Response formatting
1143
+ print("Demo 4: Agent Response")
1054
1144
  print("-" * 40)
1145
+ sample_response = """I've analyzed your codebase and found the following:
1146
+
1147
+ 1. The main entry point is in `main.py`
1148
+ 2. Configuration is loaded from `config.json`
1149
+ 3. There are 3 utility modules in the `utils/` directory
1150
+
1151
+ Here's a quick example of how to run it:
1152
+
1153
+ ```python
1154
+ from myapp import main
1155
+ main.run()
1156
+ ```
1055
1157
 
1056
- task = TaskDisplay("Implementing authentication feature")
1057
- task.start()
1058
-
1059
- task.step("Analyzing requirements")
1060
- idx = task.action("Read", "auth_spec.md")
1061
- time.sleep(0.3)
1062
- task.complete_action(idx)
1063
- task.complete_step("Requirements analyzed")
1064
-
1065
- task.step("Creating auth module")
1066
- idx1 = task.action("Write", "auth/login.py")
1067
- time.sleep(0.2)
1068
- task.complete_action(idx1)
1069
- idx2 = task.action("Write", "auth/logout.py")
1070
- time.sleep(0.2)
1071
- task.complete_action(idx2)
1072
- task.complete_step("Auth module created")
1073
-
1074
- task.finish()
1158
+ Let me know if you need any additional help!"""
1159
+ print_response(sample_response)
1075
1160
 
1076
1161
  print("Demo complete!")
1077
1162