cognautic-cli 1.1.3__tar.gz → 1.1.4__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.

Potentially problematic release.


This version of cognautic-cli might be problematic. Click here for more details.

Files changed (34) hide show
  1. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/PKG-INFO +2 -2
  2. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/__init__.py +1 -1
  3. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/ai_engine.py +161 -15
  4. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/auto_continuation.py +88 -93
  5. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/cli.py +45 -46
  6. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/__init__.py +5 -1
  7. cognautic_cli-1.1.4/cognautic/tools/code_navigation.py +642 -0
  8. cognautic_cli-1.1.4/cognautic/tools/directory_context.py +340 -0
  9. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/registry.py +4 -0
  10. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic_cli.egg-info/PKG-INFO +2 -2
  11. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic_cli.egg-info/SOURCES.txt +3 -0
  12. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/pyproject.toml +2 -2
  13. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/setup.py +1 -1
  14. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/LICENSE +0 -0
  15. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/README.md +0 -0
  16. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/config.py +0 -0
  17. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/file_tagger.py +0 -0
  18. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/memory.py +0 -0
  19. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/provider_endpoints.py +0 -0
  20. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/rules.py +0 -0
  21. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/base.py +0 -0
  22. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/code_analysis.py +0 -0
  23. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/command_runner.py +0 -0
  24. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/file_operations.py +0 -0
  25. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/file_reader.py +0 -0
  26. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/response_control.py +0 -0
  27. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/tools/web_search.py +0 -0
  28. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/utils.py +0 -0
  29. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic/websocket_server.py +0 -0
  30. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic_cli.egg-info/dependency_links.txt +0 -0
  31. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic_cli.egg-info/entry_points.txt +0 -0
  32. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic_cli.egg-info/requires.txt +0 -0
  33. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/cognautic_cli.egg-info/top_level.txt +0 -0
  34. {cognautic_cli-1.1.3 → cognautic_cli-1.1.4}/setup.cfg +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognautic-cli
3
- Version: 1.1.3
3
+ Version: 1.1.4
4
4
  Summary: A Python-based CLI AI coding agent that provides agentic development capabilities with multi-provider AI support and real-time interaction
5
5
  Home-page: https://github.com/cognautic/cognautic-cli
6
6
  Author: Cognautic
7
7
  Author-email: Cognautic <cognautic@gmail.com>
8
8
  Maintainer-email: Cognautic <cognautic@gmail.com>
9
- License: Proprietary - All Rights Reserved
9
+ License-Expression: MIT
10
10
  Project-URL: Homepage, https://github.com/cognautic/cli
11
11
  Project-URL: Documentation, https://cognautic.vercel.app/cognautic-cli.html
12
12
  Project-URL: Repository, https://github.com/cognautic/cli.git
@@ -2,6 +2,6 @@
2
2
  Cognautic CLI - A Python-based CLI AI coding agent
3
3
  """
4
4
 
5
- __version__ = "1.1.3"
5
+ __version__ = "1.1.4"
6
6
  __author__ = "Cognautic"
7
7
  __email__ = "cognautic@gmail.com"
@@ -960,7 +960,7 @@ class AIEngine:
960
960
  return
961
961
 
962
962
  # Only continue if the auto-continuation manager says so
963
- if self.auto_continuation.should_continue(tool_results, has_end_response):
963
+ if self.auto_continuation.should_continue(tool_results, has_end_response, buffer):
964
964
  yield "... \n"
965
965
 
966
966
  # Add assistant's response to messages
@@ -968,7 +968,7 @@ class AIEngine:
968
968
 
969
969
  # Build continuation prompt
970
970
  continuation_prompt = self.auto_continuation.build_continuation_prompt(
971
- tool_results
971
+ tool_results, buffer
972
972
  )
973
973
 
974
974
  # Add continuation prompt to messages
@@ -2255,6 +2255,79 @@ For web search:
2255
2255
  }
2256
2256
  ```
2257
2257
 
2258
+ For directory context and project structure:
2259
+
2260
+ ```json
2261
+ {
2262
+ "tool_code": "directory_context",
2263
+ "args": {
2264
+ "operation": "get_directory_summary"
2265
+ }
2266
+ }
2267
+ ```
2268
+
2269
+ ```json
2270
+ {
2271
+ "tool_code": "directory_context",
2272
+ "args": {
2273
+ "operation": "list_directory_tree",
2274
+ "max_depth": 3
2275
+ }
2276
+ }
2277
+ ```
2278
+
2279
+ ```json
2280
+ {
2281
+ "tool_code": "directory_context",
2282
+ "args": {
2283
+ "operation": "get_project_structure"
2284
+ }
2285
+ }
2286
+ ```
2287
+
2288
+ For code navigation (jump to definition, find references, search symbols):
2289
+
2290
+ ```json
2291
+ {
2292
+ "tool_code": "code_navigation",
2293
+ "args": {
2294
+ "operation": "jump_to_definition",
2295
+ "symbol": "MyClass"
2296
+ }
2297
+ }
2298
+ ```
2299
+
2300
+ ```json
2301
+ {
2302
+ "tool_code": "code_navigation",
2303
+ "args": {
2304
+ "operation": "find_references",
2305
+ "symbol": "myFunction"
2306
+ }
2307
+ }
2308
+ ```
2309
+
2310
+ ```json
2311
+ {
2312
+ "tool_code": "code_navigation",
2313
+ "args": {
2314
+ "operation": "search_symbols",
2315
+ "query": "handler",
2316
+ "symbol_type": "function"
2317
+ }
2318
+ }
2319
+ ```
2320
+
2321
+ ```json
2322
+ {
2323
+ "tool_code": "code_navigation",
2324
+ "args": {
2325
+ "operation": "list_symbols",
2326
+ "file_path": "src/main.py"
2327
+ }
2328
+ }
2329
+ ```
2330
+
2258
2331
  EXAMPLE WORKFLOWS:
2259
2332
 
2260
2333
  When user asks to ADD FEATURE to existing project (e.g., "add export button to cymox"):
@@ -2308,10 +2381,14 @@ Available tools:
2308
2381
  - command_runner: Execute shell commands (use run_async_command for long tasks)
2309
2382
  - web_search: Search the web for information
2310
2383
  - response_control: Use end_response when task is complete
2384
+ - directory_context: Get detailed directory structure and project information
2385
+ - code_navigation: Jump to definition, find references, search symbols in code
2386
+
2387
+ RESPONSE CONTINUATION (CRITICAL - SYSTEM WILL AUTO-CONTINUE):
2388
+ - ⚠️ IMPORTANT: The system will AUTOMATICALLY continue your response after EVERY message
2389
+ - This means you will KEEP GETTING called until you explicitly call end_response
2390
+ - YOU MUST call end_response when you finish ALL work, or you'll loop forever:
2311
2391
 
2312
- RESPONSE CONTINUATION (CRITICAL - ALWAYS USE end_response):
2313
- - By default, after executing tools, the AI will automatically continue to complete the task
2314
- - YOU MUST ALWAYS use the response_control tool when you finish ALL work:
2315
2392
  ```json
2316
2393
  {
2317
2394
  "tool_code": "response_control",
@@ -2320,16 +2397,21 @@ RESPONSE CONTINUATION (CRITICAL - ALWAYS USE end_response):
2320
2397
  }
2321
2398
  }
2322
2399
  ```
2323
- - WHEN TO USE end_response (ALWAYS use it in these cases):
2324
- * After creating/modifying ALL requested files
2325
- * After completing ALL steps of a multi-step task
2326
- * After providing final explanation or summary
2327
- * Basically: ALWAYS use it when you're done with everything
2328
- - Do NOT use end_response ONLY if:
2329
- * The task is incomplete
2330
- * You're waiting for user input
2331
- * You need to execute more tools
2332
- - IMPORTANT: Forgetting to use end_response causes the user to manually type "continue" - ALWAYS use it!
2400
+
2401
+ - HOW IT WORKS:
2402
+ * You respond System auto-continues You respond again → System auto-continues → ...
2403
+ * This loop ONLY stops when you call end_response
2404
+ * If you say "I will use a tool" but don't execute it, system will auto-continue and remind you
2405
+ * If you execute tools, system will auto-continue for next steps
2406
+ * If you do nothing, system will still auto-continue
2407
+
2408
+ - WHEN TO USE end_response (THE ONLY WAY TO STOP):
2409
+ * After completing ALL requested work
2410
+ * ✅ After providing final summary/instructions
2411
+ * ✅ When task is 100% complete
2412
+ * ❌ NEVER use it if task is incomplete - system will continue for you
2413
+
2414
+ - CRITICAL: If you forget end_response, you'll keep getting called in an infinite loop!
2333
2415
 
2334
2416
  REMEMBER:
2335
2417
  1. Use tools to actually perform actions, don't just provide code examples!
@@ -2459,6 +2541,70 @@ When creating or modifying files:
2459
2541
  if project_path:
2460
2542
  prompt += f"\n\nCurrent project path: {project_path}"
2461
2543
  prompt += "\nYou can analyze and modify files in this project."
2544
+
2545
+ # Add directory context automatically
2546
+ try:
2547
+ from pathlib import Path
2548
+ project_dir = Path(project_path)
2549
+ if project_dir.exists() and project_dir.is_dir():
2550
+ prompt += "\n\n" + "=" * 80
2551
+ prompt += "\n📁 CURRENT DIRECTORY CONTENTS (AUTO-PROVIDED FOR CONTEXT)"
2552
+ prompt += "\n" + "=" * 80
2553
+
2554
+ # List immediate directory contents
2555
+ contents = []
2556
+ files = []
2557
+ dirs = []
2558
+
2559
+ try:
2560
+ for item in sorted(project_dir.iterdir()):
2561
+ if item.name.startswith('.') and item.name not in ['.gitignore', '.env.example']:
2562
+ continue # Skip hidden files except important ones
2563
+
2564
+ if item.is_dir():
2565
+ # Count items in directory
2566
+ try:
2567
+ item_count = len(list(item.iterdir()))
2568
+ dirs.append(f" 📂 {item.name}/ ({item_count} items)")
2569
+ except (PermissionError, OSError):
2570
+ dirs.append(f" 📂 {item.name}/")
2571
+ else:
2572
+ # Get file size
2573
+ try:
2574
+ size = item.stat().st_size
2575
+ if size < 1024:
2576
+ size_str = f"{size}B"
2577
+ elif size < 1024 * 1024:
2578
+ size_str = f"{size/1024:.1f}KB"
2579
+ else:
2580
+ size_str = f"{size/(1024*1024):.1f}MB"
2581
+ files.append(f" 📄 {item.name} ({size_str})")
2582
+ except (PermissionError, OSError):
2583
+ files.append(f" 📄 {item.name}")
2584
+
2585
+ prompt += f"\n\nDirectory: {project_path}"
2586
+ prompt += f"\n\nDirectories ({len(dirs)}):"
2587
+ if dirs:
2588
+ prompt += "\n" + "\n".join(dirs)
2589
+ else:
2590
+ prompt += "\n (none)"
2591
+
2592
+ prompt += f"\n\nFiles ({len(files)}):"
2593
+ if files:
2594
+ prompt += "\n" + "\n".join(files)
2595
+ else:
2596
+ prompt += "\n (none)"
2597
+
2598
+ prompt += "\n\n" + "=" * 80
2599
+ prompt += "\nℹ️ This directory listing is provided automatically for your context."
2600
+ prompt += "\nℹ️ You can use the 'directory_context' tool for more detailed information."
2601
+ prompt += "\n" + "=" * 80
2602
+
2603
+ except PermissionError:
2604
+ prompt += "\n\n⚠️ Permission denied accessing directory contents."
2605
+ except Exception:
2606
+ # Silently fail if directory context can't be added
2607
+ pass
2462
2608
 
2463
2609
  return prompt
2464
2610
 
@@ -26,129 +26,124 @@ class AutoContinuationManager:
26
26
  self.iteration_count = 0
27
27
  self.previous_tool_types = []
28
28
 
29
- def should_continue(self, tool_results: List[Dict[str, Any]], has_end_response: bool) -> bool:
29
+ def should_continue(
30
+ self,
31
+ tool_results: List[Dict[str, Any]],
32
+ has_end_response: bool,
33
+ ai_response: str = ""
34
+ ) -> bool:
30
35
  """
31
36
  Determine if AI should automatically continue
32
37
 
38
+ SIMPLIFIED LOGIC: Always continue EXCEPT when end_response is called
39
+ This ensures AI completes tasks without manual intervention
40
+
33
41
  Args:
34
42
  tool_results: List of tool execution results
35
43
  has_end_response: Whether end_response tool was called
44
+ ai_response: The AI's text response (to detect promises without execution)
36
45
 
37
46
  Returns:
38
47
  True if should continue, False otherwise
39
48
  """
40
- # Don't continue if end_response was explicitly called
49
+ # RULE 1: Don't continue if end_response was explicitly called
50
+ # This is the ONLY condition that stops auto-continuation
41
51
  if has_end_response:
42
52
  return False
43
53
 
44
- # Don't continue if max iterations reached
54
+ # RULE 2: Don't continue if max iterations reached (safety limit)
45
55
  if self.iteration_count >= self.max_iterations:
46
56
  return False
47
57
 
48
- # Don't continue if no tools were executed
49
- if not tool_results:
50
- return False
51
-
52
- # Check if there were any errors
53
- has_errors = any(r.get('type') == 'error' for r in tool_results)
54
-
55
- # Check what types of tools were executed
56
- has_file_ops = any(r.get('type') in ['file_op', 'file_write'] for r in tool_results) # Exclude file_read from has_file_ops
57
- has_file_read = any(r.get('type') in ['file_read', 'file_reader'] for r in tool_results) # Include both file reading types
58
- has_commands = any(r.get('type') == 'command' for r in tool_results)
59
- has_background_commands = any(
60
- r.get('type') == 'command' and 'background' in str(r.get('command', ''))
61
- for r in tool_results
62
- )
63
-
64
- # Check for web searches (which might indicate looking for information before proceeding)
65
- has_web_search = any(r.get('type') in ['web_search', 'web_fetch'] for r in tool_results)
66
-
67
- # Check if commands were exploratory (ls, pwd, dir, etc.)
68
- exploratory_commands = ['ls', 'pwd', 'dir', 'cd', 'find', 'tree', 'cat', 'head', 'tail']
69
- has_exploratory_commands = any(
70
- r.get('type') == 'command' and
71
- any(cmd in str(r.get('command', '')).lower() for cmd in exploratory_commands)
72
- for r in tool_results
73
- )
74
-
75
- # Track current tool types for repetition detection
76
- current_tool_types = []
77
- for result in tool_results:
78
- tool_type = result.get('type', 'unknown')
79
- current_tool_types.append(tool_type)
80
-
81
- # Check for repeated operations of the same type (especially web searches)
82
- is_repeating_search = (
83
- has_web_search and
84
- self.previous_tool_types and
85
- all('web_search' in prev_type or 'web_fetch' in prev_type for prev_type in self.previous_tool_types[-3:])
86
- )
87
-
88
- # Store current types for next check (keep last 5 to detect patterns)
89
- self.previous_tool_types.extend(current_tool_types)
90
- self.previous_tool_types = self.previous_tool_types[-5:] # Keep only last 5
91
-
92
- # Smart continuation logic:
93
- # 1. If there are errors, continue to let AI handle them
94
- if has_errors:
95
- self.iteration_count += 1
96
- return True
97
-
98
- # 2. If file was read, always continue so AI can act on the content
99
- if has_file_read:
100
- self.iteration_count += 1
101
- return True
102
-
103
- # 3. If repeating the same operation (like web searches), don't continue to break the loop
104
- if is_repeating_search and self.iteration_count > 3:
105
- return False # Break the loop if repeating searches for more than 3 iterations
106
-
107
- # 4. If only file operations were done, likely need to run commands next
108
- if has_file_ops and not has_commands:
109
- self.iteration_count += 1
110
- return True
111
-
112
- # 5. If only exploratory commands (ls, pwd, etc.), continue to do actual work
113
- if has_exploratory_commands and not (has_file_ops or has_file_read):
114
- self.iteration_count += 1
115
- return True
116
-
117
- # 6. If web search was performed, often AI should continue to create files/execute commands with the information
118
- if has_web_search and not has_file_ops:
119
- self.iteration_count += 1
120
- return True
121
-
122
- # 7. If background commands were started AND files created, task likely complete
123
- if has_background_commands and (has_file_ops or has_file_read):
124
- return False
125
-
126
- # 8. If only background commands (no files), don't continue (they're running)
127
- if has_background_commands and not (has_file_ops or has_file_read):
128
- return False
129
-
130
- # 9. If regular commands executed with file ops, task likely complete
131
- if has_commands and has_file_ops and not has_exploratory_commands:
132
- return False
58
+ # RULE 3: Check if AI said it would use tools but didn't execute them
59
+ # This handles the scenario where AI says "I will use X tool" but doesn't
60
+ if not tool_results and ai_response:
61
+ promise_indicators = [
62
+ "i will use",
63
+ "i can use",
64
+ "let me use",
65
+ "i'll use",
66
+ "using the",
67
+ "i will call",
68
+ "i'll call",
69
+ "let me call",
70
+ "i can help you with that",
71
+ "i will list",
72
+ "i will find",
73
+ "i will search",
74
+ "i will check",
75
+ "i will analyze",
76
+ "i will show",
77
+ "i will count"
78
+ ]
79
+
80
+ ai_lower = ai_response.lower()
81
+
82
+ # Check if AI promised to use tools
83
+ promised_to_use_tools = any(indicator in ai_lower for indicator in promise_indicators)
84
+
85
+ # Check if AI mentioned specific tools
86
+ tool_mentions = [
87
+ "code_navigation",
88
+ "directory_context",
89
+ "file_operations",
90
+ "web_search",
91
+ "command_runner",
92
+ "tool"
93
+ ]
94
+ mentioned_tools = any(tool in ai_lower for tool in tool_mentions)
95
+
96
+ # If AI promised to use tools or mentioned specific tools but didn't execute any
97
+ if promised_to_use_tools or mentioned_tools:
98
+ self.iteration_count += 1
99
+ return True
133
100
 
134
- # 10. Default: Continue if we're early in the process
135
- # This ensures AI completes the full task
136
- if self.iteration_count < 3:
101
+ # RULE 4: If no tools were executed and no promises detected, still continue
102
+ # The AI might need another chance to complete the task
103
+ if not tool_results:
137
104
  self.iteration_count += 1
138
105
  return True
139
106
 
140
- return False
107
+ # RULE 5: If ANY tools were executed, ALWAYS continue
108
+ # Let the AI decide when it's done by calling end_response
109
+ self.iteration_count += 1
110
+ return True
141
111
 
142
- def build_continuation_prompt(self, tool_results: List[Dict[str, Any]]) -> str:
112
+ def build_continuation_prompt(
113
+ self,
114
+ tool_results: List[Dict[str, Any]],
115
+ ai_response: str = ""
116
+ ) -> str:
143
117
  """
144
118
  Build an appropriate continuation prompt based on tool results
145
119
 
146
120
  Args:
147
121
  tool_results: List of tool execution results
122
+ ai_response: The AI's text response (to provide context)
148
123
 
149
124
  Returns:
150
125
  Continuation prompt string
151
126
  """
127
+ # Special case: AI said it would use tools but didn't
128
+ if not tool_results and ai_response:
129
+ return """You said you would use a tool, but you didn't actually execute it.
130
+
131
+ CRITICAL: You MUST include the actual JSON tool call in your response, not just say you will do it.
132
+
133
+ For example, if you want to use code_navigation to list symbols, you MUST include:
134
+
135
+ ```json
136
+ {
137
+ "tool_code": "code_navigation",
138
+ "args": {
139
+ "operation": "list_symbols",
140
+ "file_path": "main.py"
141
+ }
142
+ }
143
+ ```
144
+
145
+ Now execute the tool you mentioned:"""
146
+
152
147
  # Categorize tool results
153
148
  has_file_ops = any(r.get('type') in ['file_op', 'file_write', 'file_read'] for r in tool_results)
154
149
  has_commands = any(r.get('type') == 'command' for r in tool_results)
@@ -75,19 +75,8 @@ from .rules import RulesManager
75
75
 
76
76
  console = Console()
77
77
 
78
- # Global flag to stop AI response
79
- stop_response = False
80
-
81
- def signal_handler(signum, frame):
82
- """Handle Ctrl+X (SIGQUIT) to stop AI response"""
83
- global stop_response
84
- stop_response = True
85
-
86
-
87
-
88
-
89
78
  @click.group(invoke_without_command=True)
90
- @click.version_option(version="1.1.3", prog_name="Cognautic CLI")
79
+ @click.version_option(version="1.1.4", prog_name="Cognautic CLI")
91
80
  @click.pass_context
92
81
  def main(ctx):
93
82
  """Cognautic CLI - AI-powered development assistant"""
@@ -187,16 +176,16 @@ def chat(provider, model, project_path, websocket_port, session):
187
176
  websocket_server = WebSocketServer(ai_engine, port=websocket_port)
188
177
 
189
178
  async def run_chat():
190
- global stop_response
191
-
192
- # Set up Ctrl+X signal handler (SIGQUIT)
193
- signal.signal(signal.SIGQUIT, signal_handler)
194
-
195
179
  # Start WebSocket server
196
180
  server_task = asyncio.create_task(websocket_server.start())
197
181
 
182
+ # Track last Ctrl+C time for double-tap detection
183
+ import time
184
+ last_ctrl_c_time = [0] # Use list to make it mutable in nested function
185
+
198
186
  try:
199
- console.print("INFO: Type '/help' for commands, 'exit' to quit")
187
+ console.print("INFO: Type '/help' for commands, or press Ctrl+C twice to exit")
188
+ console.print("INFO: Press Enter to send, Alt+Enter for new line")
200
189
  console.print("INFO: Press Shift+Tab to toggle Terminal mode")
201
190
  if project_path:
202
191
  console.print(f"DIR: Working in: {project_path}")
@@ -246,7 +235,7 @@ def chat(provider, model, project_path, websocket_port, session):
246
235
  # Terminal mode state
247
236
  terminal_mode = [False] # Use list to make it mutable in nested function
248
237
 
249
- # Setup key bindings for Shift+Tab toggle
238
+ # Setup key bindings for Shift+Tab toggle and multi-line support
250
239
  bindings = KeyBindings()
251
240
 
252
241
  @bindings.add('s-tab') # Shift+Tab
@@ -257,8 +246,19 @@ def chat(provider, model, project_path, websocket_port, session):
257
246
  event.app.current_buffer.text = ''
258
247
  event.app.exit(result='__MODE_TOGGLE__')
259
248
 
260
- # Create prompt session
261
- session = PromptSession(key_bindings=bindings)
249
+ @bindings.add('enter') # Enter key submits
250
+ def submit_message(event):
251
+ event.current_buffer.validate_and_handle()
252
+
253
+ @bindings.add('escape', 'enter') # Meta+Enter (Alt+Enter) adds new line
254
+ def new_line(event):
255
+ event.current_buffer.insert_text('\n')
256
+
257
+ # Create prompt session with multi-line support
258
+ session = PromptSession(
259
+ key_bindings=bindings,
260
+ multiline=True # Allow multi-line editing
261
+ )
262
262
 
263
263
  console.print("[dim]INFO: Press Shift+Tab to toggle between Chat and Terminal modes[/dim]\n")
264
264
 
@@ -339,7 +339,7 @@ def chat(provider, model, project_path, websocket_port, session):
339
339
  console.print(f"[yellow]Exit code: {returncode}[/yellow]")
340
340
 
341
341
  except KeyboardInterrupt:
342
- # Handle Ctrl+C - terminate the process
342
+ # Handle Ctrl+C - terminate the process but don't exit CLI
343
343
  console.print("\n[yellow]^C[/yellow]")
344
344
  process.terminate()
345
345
  try:
@@ -419,8 +419,10 @@ def chat(provider, model, project_path, websocket_port, session):
419
419
  console.print("[bold magenta]─[/bold magenta]" * 50)
420
420
  console.print("[bold magenta]AI:[/bold magenta] ", end="")
421
421
  full_response = ""
422
- response_stopped = False
423
- stop_response = False # Reset stop flag
422
+
423
+ # Disable Ctrl+C during AI response
424
+ import signal
425
+ original_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
424
426
 
425
427
  try:
426
428
  async for chunk in ai_engine.process_message_stream(
@@ -430,42 +432,36 @@ def chat(provider, model, project_path, websocket_port, session):
430
432
  project_path=current_workspace,
431
433
  conversation_history=conversation_history
432
434
  ):
433
- # Check if stop was requested
434
- if stop_response:
435
- response_stopped = True
436
- console.print("\n\n[yellow]⏸️ AI response stopped by user (Ctrl+X)[/yellow]")
437
- break
438
-
439
435
  # Display character by character for typewriter effect
440
436
  for char in chunk:
441
- # Check stop flag during character display too
442
- if stop_response:
443
- response_stopped = True
444
- console.print("\n\n[yellow]⏸️ AI response stopped by user (Ctrl+X)[/yellow]")
445
- break
446
437
  console.print(char, end="")
447
438
  full_response += char
448
439
  if typing_delay > 0:
449
440
  await asyncio.sleep(typing_delay)
450
-
451
- if response_stopped:
452
- break
453
- except KeyboardInterrupt:
454
- # Ctrl+C pressed - exit chat
455
- break
441
+ finally:
442
+ # Re-enable Ctrl+C after AI response
443
+ signal.signal(signal.SIGINT, original_handler)
456
444
 
457
445
  console.print() # New line after streaming
458
446
  console.print("[bold magenta]─[/bold magenta]" * 50) # Border after AI response
459
447
 
460
- # Add AI response to memory (even if stopped)
448
+ # Add AI response to memory
461
449
  if full_response:
462
450
  memory_manager.add_message("assistant", full_response)
463
451
 
464
- # Don't break - continue to next prompt
465
-
466
452
  except KeyboardInterrupt:
467
- # Ctrl+C pressed while waiting for user input - exit chat
468
- break
453
+ # Ctrl+C pressed while waiting for user input
454
+ current_time = time.time()
455
+ time_since_last = current_time - last_ctrl_c_time[0]
456
+
457
+ if time_since_last < 2.0: # Double tap within 2 seconds
458
+ console.print("\n[yellow]Exiting...[/yellow]")
459
+ break
460
+ else:
461
+ # First Ctrl+C - show hint
462
+ console.print("\n[dim]Press Ctrl+C again within 2 seconds to exit, or type 'exit'[/dim]")
463
+ last_ctrl_c_time[0] = current_time
464
+ continue
469
465
  except Exception as e:
470
466
  console.print(f"ERROR: {str(e)}", style="red")
471
467
 
@@ -1200,6 +1196,9 @@ def show_help():
1200
1196
  """Show help information"""
1201
1197
  help_text = Text()
1202
1198
  help_text.append("Available commands:\n", style="bold")
1199
+ help_text.append("• Press Enter - Send message\n", style="bold green")
1200
+ help_text.append("• Press Alt+Enter - New line (multi-line input)\n", style="bold green")
1201
+ help_text.append("• Press Ctrl+C twice - Exit CLI\n", style="bold yellow")
1203
1202
  help_text.append("• Press Shift+Tab - Toggle between Chat and Terminal modes\n", style="bold yellow")
1204
1203
  help_text.append("• /help - Show this help message\n")
1205
1204
  help_text.append("• /workspace <path> or /ws <path> - Change working directory\n")
@@ -8,6 +8,8 @@ from .command_runner import CommandRunnerTool
8
8
  from .web_search import WebSearchTool
9
9
  from .code_analysis import CodeAnalysisTool
10
10
  from .response_control import ResponseControlTool
11
+ from .directory_context import DirectoryContextTool
12
+ from .code_navigation import CodeNavigationTool
11
13
 
12
14
  __all__ = [
13
15
  'ToolRegistry',
@@ -15,5 +17,7 @@ __all__ = [
15
17
  'CommandRunnerTool',
16
18
  'WebSearchTool',
17
19
  'CodeAnalysisTool',
18
- 'ResponseControlTool'
20
+ 'ResponseControlTool',
21
+ 'DirectoryContextTool',
22
+ 'CodeNavigationTool'
19
23
  ]