IncludeCPP 3.3.11__py3-none-any.whl → 3.3.20__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.
includecpp/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from .core.cpp_api import CppApi
2
2
  import warnings
3
3
 
4
- __version__ = "3.3.11"
4
+ __version__ = "3.3.20"
5
5
  __all__ = ["CppApi"]
6
6
 
7
7
  # Module-level cache for C++ modules
@@ -10,6 +10,22 @@ import urllib.error
10
10
  from pathlib import Path
11
11
  from .config_parser import CppProjectConfig
12
12
 
13
+
14
+ def _is_experimental_enabled() -> bool:
15
+ """Check if experimental features (cppy, ai) are enabled."""
16
+ config_path = Path.home() / '.includecpp' / '.secret'
17
+ if config_path.exists():
18
+ try:
19
+ config = json.loads(config_path.read_text())
20
+ return config.get('experimental_features', False)
21
+ except:
22
+ pass
23
+ return False
24
+
25
+
26
+ # Check once at module load time
27
+ _EXPERIMENTAL_ENABLED = _is_experimental_enabled()
28
+
13
29
  # Unicode fallback for Windows terminals with limited encoding
14
30
  def _supports_unicode():
15
31
  """Check if terminal supports Unicode output."""
@@ -4816,7 +4832,8 @@ def fix(module_name, all_modules, exclude, undo, auto_fix, verbose, use_ai):
4816
4832
  click.echo(f"{'='*60}")
4817
4833
 
4818
4834
 
4819
- @cli.group(invoke_without_command=True)
4835
+ # AI group - conditionally registered based on experimental_features setting
4836
+ @click.group(invoke_without_command=True)
4820
4837
  @click.option('--info', is_flag=True, help='Show AI status and usage')
4821
4838
  @click.pass_context
4822
4839
  def ai(ctx, info):
@@ -5306,7 +5323,7 @@ def ai_edit(task, module_name, files, all_modules, exclude, think_mode, think_tw
5306
5323
  question = None # Continuation doesn't ask more questions
5307
5324
 
5308
5325
  if not changes:
5309
- verbose.status("No changes needed", success=True)
5326
+ verbose.status("No changes needed", phase='complete')
5310
5327
  verbose.end(success=True, message="No changes required")
5311
5328
  return
5312
5329
 
@@ -5520,7 +5537,7 @@ def ai_generate(task, files, think_mode, think_twice, think_three, use_websearch
5520
5537
  verbose.api_call(endpoint='chat/completions', tokens_out=len(str(changes)) // 4 if changes else len(response) // 4)
5521
5538
 
5522
5539
  if not changes:
5523
- verbose.status("No file changes needed", success=True)
5540
+ verbose.status("No file changes needed", phase='complete')
5524
5541
  # Show response if any
5525
5542
  if response:
5526
5543
  _safe_echo(f"\n{response[:2000]}")
@@ -5598,7 +5615,7 @@ def ai_generate(task, files, think_mode, think_twice, think_three, use_websearch
5598
5615
  shell=True, capture_output=True, text=True, cwd=project_root
5599
5616
  )
5600
5617
  if result.returncode == 0:
5601
- verbose.status("Plugin generated", success=True)
5618
+ verbose.status("Plugin generated", phase='complete')
5602
5619
  else:
5603
5620
  verbose.error(f"Plugin error: {result.stderr[:100]}")
5604
5621
  verbose.end(success=False, message="Plugin generation failed")
@@ -5611,7 +5628,7 @@ def ai_generate(task, files, think_mode, think_twice, think_three, use_websearch
5611
5628
  shell=True, capture_output=True, text=True, cwd=project_root
5612
5629
  )
5613
5630
  if result.returncode == 0:
5614
- verbose.status("Build successful", success=True)
5631
+ verbose.status("Build successful", phase='complete')
5615
5632
  else:
5616
5633
  verbose.error(f"Build error: {result.stderr[:100]}")
5617
5634
 
@@ -5719,7 +5736,7 @@ def ai_optimize(module_name, files, agent_task, auto_confirm, no_verbose):
5719
5736
 
5720
5737
  verbose.phase('analyzing', 'Analyzing code for optimization opportunities')
5721
5738
  for fp in files_to_optimize.keys():
5722
- verbose.status(f"Scanning {Path(fp).name}", success=True)
5739
+ verbose.status(f"Scanning {Path(fp).name}", phase='analyzing')
5723
5740
 
5724
5741
  verbose.phase('thinking', 'AI is analyzing and planning optimizations')
5725
5742
  verbose.api_call(endpoint='chat/completions', tokens_in=total_lines * 4)
@@ -5734,7 +5751,7 @@ def ai_optimize(module_name, files, agent_task, auto_confirm, no_verbose):
5734
5751
  verbose.api_call(endpoint='chat/completions', tokens_out=len(str(changes)) // 4 if changes else 0)
5735
5752
 
5736
5753
  if not changes:
5737
- verbose.status("No optimizations needed", success=True)
5754
+ verbose.status("No optimizations needed", phase='complete')
5738
5755
  verbose.end(success=True, message="Code is already optimal")
5739
5756
  return
5740
5757
 
@@ -5787,7 +5804,7 @@ def ai_optimize(module_name, files, agent_task, auto_confirm, no_verbose):
5787
5804
  click.secho("CONFIRM REQUIRED:", fg='yellow', bold=True)
5788
5805
  click.echo(f"\n{confirm}\n")
5789
5806
  if not click.confirm("Apply this change?"):
5790
- verbose.status(f"Skipped: {change['file']}", success=False)
5807
+ verbose.status(f"Skipped: {change['file']}", phase='warning')
5791
5808
  changes = [c for c in changes if c != change]
5792
5809
  if not click.confirm("Apply all changes?"):
5793
5810
  verbose.warning("Changes aborted by user")
@@ -5848,11 +5865,59 @@ def settings():
5848
5865
  click.echo(" includecpp ai key <YOUR_KEY>")
5849
5866
 
5850
5867
 
5868
+ @click.command()
5869
+ @click.option('--path', '-p', type=click.Path(), default=None,
5870
+ help='Path to project directory (default: current directory)')
5871
+ def project(path):
5872
+ """Open the Project Interface with CodeMaker.
5873
+
5874
+ Opens a professional visual mindmap tool for planning and designing
5875
+ your C++ project structure.
5876
+
5877
+ Features:
5878
+ - Visual node-based system design
5879
+ - Class, Function, Object, and Definition nodes
5880
+ - Connectable nodes with bezier curves
5881
+ - Pan/zoom navigation (middle mouse button)
5882
+ - Auto-save functionality
5883
+ - .ma map file management
5884
+
5885
+ Controls:
5886
+ - Right-click canvas: Create new nodes
5887
+ - Right-click node: Edit/Connect/Delete
5888
+ - Middle mouse + drag: Pan view
5889
+ - Middle mouse + scroll: Zoom in/out
5890
+ - ESC: Cancel connection mode
5891
+ - DELETE: Remove selected nodes
5892
+
5893
+ Requires: pip install PyQt6
5894
+ """
5895
+ from pathlib import Path as PathLib
5896
+ project_path = PathLib(path) if path else PathLib.cwd()
5897
+
5898
+ if not project_path.exists():
5899
+ click.secho(f"Path does not exist: {project_path}", fg='red', err=True)
5900
+ return
5901
+
5902
+ try:
5903
+ from ..core.project_ui import show_project, PYQT_AVAILABLE
5904
+ if not PYQT_AVAILABLE:
5905
+ click.secho("PyQt6 not installed.", fg='red', err=True)
5906
+ click.echo("Install with: pip install PyQt6")
5907
+ return
5908
+ success, msg = show_project(str(project_path))
5909
+ if not success:
5910
+ click.secho(msg, fg='red', err=True)
5911
+ except Exception as e:
5912
+ click.secho(f"Error opening project interface: {e}", fg='red', err=True)
5913
+
5914
+
5851
5915
  # ============================================================================
5852
- # CPPY - Code Conversion Tools
5916
+ # CPPY - Code Conversion Tools (Experimental)
5853
5917
  # ============================================================================
5854
5918
 
5855
- @cli.group(invoke_without_command=True)
5919
+ # CPPY group - conditionally registered based on experimental_features setting
5920
+ @click.group(invoke_without_command=True)
5856
5921
  @click.pass_context
5857
5922
  def cppy(ctx):
5858
5923
  """Code conversion tools for Python <-> C++.
@@ -6358,6 +6423,38 @@ def _convert_with_ai(content: str, module_name: str, source_file,
6358
6423
  return _convert_to_python(content, module_name, source_file, output_dir, verbose)
6359
6424
 
6360
6425
  ai_verbose.api_call(endpoint='chat/completions', tokens_out=len(response) // 4)
6426
+
6427
+ # Check if AI needs clarification on unconvertible modules
6428
+ if 'CLARIFICATION_NEEDED:' in response:
6429
+ ai_verbose.phase('clarification', 'AI needs clarification')
6430
+ clarification_match = re.search(r'CLARIFICATION_NEEDED:\n((?:- .+\n?)+)', response)
6431
+ if clarification_match:
6432
+ questions = clarification_match.group(1).strip()
6433
+ click.echo("\n" + "=" * 60)
6434
+ click.secho("AI NEEDS CLARIFICATION:", fg='yellow', bold=True)
6435
+ click.echo(questions)
6436
+ click.echo("=" * 60)
6437
+ user_input = click.prompt("Your response (or 'skip' to use defaults)")
6438
+ if user_input.lower() != 'skip':
6439
+ # Re-run AI with user's clarification
6440
+ clarified_prompt = ai_prompt + f"\n\nUSER CLARIFICATION:\n{user_input}\n\nNow convert with this clarification:"
6441
+ success, response, _ = ai_manager.generate(
6442
+ task=clarified_prompt,
6443
+ files={str(source_file): content},
6444
+ project_root=project_root,
6445
+ think=think,
6446
+ think_twice=think_twice,
6447
+ think_three=think_three,
6448
+ use_websearch=use_websearch,
6449
+ skip_tool_execution=True
6450
+ )
6451
+ if not success:
6452
+ ai_verbose.warning("Clarified conversion failed, using standard converter")
6453
+ if to_cpp:
6454
+ return _convert_to_cpp(content, module_name, source_file, output_dir, no_header, namespace, verbose)
6455
+ else:
6456
+ return _convert_to_python(content, module_name, source_file, output_dir, verbose)
6457
+
6361
6458
  ai_verbose.phase('parsing', 'Extracting converted code from AI response')
6362
6459
 
6363
6460
  # Parse AI response
@@ -6367,7 +6464,7 @@ def _convert_with_ai(content: str, module_name: str, source_file,
6367
6464
  converted_code = _extract_ai_converted_code(response, to_cpp, module_name, namespace)
6368
6465
 
6369
6466
  if not converted_code:
6370
- ai_verbose.status("Using hybrid conversion (AI + standard)", success=True)
6467
+ ai_verbose.status("Using hybrid conversion (AI + standard)", phase='complete')
6371
6468
  # Fall back to standard conversion with AI enhancements
6372
6469
  if to_cpp:
6373
6470
  converter = PythonToCppConverter()
@@ -6460,22 +6557,95 @@ def _convert_with_ai(content: str, module_name: str, source_file,
6460
6557
  return result
6461
6558
 
6462
6559
 
6560
+ def _get_includecpp_readme() -> str:
6561
+ """Load IncludeCPP README.md for AI context."""
6562
+ try:
6563
+ import importlib.resources
6564
+ import includecpp
6565
+ readme_path = Path(includecpp.__file__).parent.parent / 'README.md'
6566
+ if readme_path.exists():
6567
+ readme = readme_path.read_text(encoding='utf-8')
6568
+ # Truncate to first sections (keep it focused)
6569
+ sections = readme.split('# Changelog')[0] # Everything before changelog
6570
+ return sections[:8000] # Limit to ~8k chars
6571
+ except Exception:
6572
+ pass
6573
+ return ""
6574
+
6463
6575
  def _build_cppy_ai_prompt(source: str, mode: str, module_name: str, namespace: str, analysis: dict, no_header: bool = False) -> str:
6464
6576
  """Build AI prompt with IncludeCPP-specific instructions."""
6465
6577
 
6578
+ # Load README for AI context (dynamically, not hardcoded)
6579
+ readme_context = _get_includecpp_readme()
6580
+ includecpp_docs = ""
6581
+ if readme_context:
6582
+ includecpp_docs = f'''
6583
+ === INCLUDECPP DOCUMENTATION (from README.md) ===
6584
+ {readme_context}
6585
+ === END DOCUMENTATION ===
6586
+ '''
6587
+
6588
+ # Critical file extension rules
6589
+ file_rules = f'''
6590
+ CRITICAL FILE EXTENSION RULES:
6591
+ - C++ implementation file MUST be: {module_name}.cpp (NOT .cp, NOT .cc, NOT .cxx)
6592
+ - C++ header file MUST be: {module_name}.h (NOT .hpp, NOT .hh)
6593
+ - Python file MUST be: {module_name}.py
6594
+ FOLLOW THESE EXTENSIONS EXACTLY.
6595
+ '''
6596
+
6597
+ # Unconvertible module handling
6598
+ unconvertible_info = ""
6599
+ for item, reason, line in getattr(analysis.get('converter', None), 'unconvertible', []) if isinstance(analysis, dict) else []:
6600
+ unconvertible_info += f"- Line {line}: {item} ({reason})\n"
6601
+
6602
+ unconvertible_guidance = ""
6603
+ if unconvertible_info:
6604
+ unconvertible_guidance = f'''
6605
+ UNCONVERTIBLE MODULES DETECTED:
6606
+ {unconvertible_info}
6607
+
6608
+ For unconvertible modules (tkinter, pygame, ursina, etc.):
6609
+ 1. COMMENT OUT: Add /* UNCONVERTIBLE: tkinter */ comment
6610
+ 2. ALTERNATIVE: Suggest C++ alternatives (Qt for GUI, SDL2 for games, etc.)
6611
+ 3. SKIP: Remove the code with explanation comment
6612
+
6613
+ If you need clarification, output:
6614
+ CLARIFICATION_NEEDED:
6615
+ - <what you need clarified>
6616
+ '''
6617
+
6466
6618
  if mode == 'py_to_cpp':
6467
6619
  direction = "Python to C++"
6468
6620
  source_lang = "python"
6469
6621
  target_lang = "cpp"
6470
6622
 
6623
+ # CRITICAL: No pybind11 - IncludeCPP handles bindings automatically
6624
+ includecpp_note = '''
6625
+ **CRITICAL - IncludeCPP HANDLES BINDINGS AUTOMATICALLY:**
6626
+ - DO NOT include pybind11 headers
6627
+ - DO NOT write PYBIND11_MODULE macros
6628
+ - DO NOT use py:: namespace or py::object
6629
+ - Just write CLEAN C++ code in namespace includecpp
6630
+ - IncludeCPP will auto-generate Python bindings via: includecpp plugin <name> <file.cpp>
6631
+
6632
+ The user runs:
6633
+ 1. includecpp cppy convert file.py --cpp (generates .cpp)
6634
+ 2. includecpp plugin mymod file.cpp (auto-generates pybind11 bindings)
6635
+ 3. includecpp rebuild (compiles to Python module)
6636
+ '''
6637
+
6471
6638
  if no_header:
6472
6639
  # No header mode - put everything in .cpp
6473
6640
  specific_rules = f'''
6474
6641
  INCLUDECPP-SPECIFIC REQUIREMENTS:
6475
6642
  1. ALL code MUST be wrapped in: namespace {namespace} {{ ... }}
6476
- 2. DO NOT create a header file - put ALL code in the .cpp file only
6477
- 3. Include declarations at the top of the .cpp file (no separate .h)
6478
- 4. For Python features without C++ equivalent, use pybind11 wrappers
6643
+ 2. **CRITICAL: DO NOT create a header file** - --no-h flag is set
6644
+ 3. Put ALL declarations AND implementations in the .cpp file only
6645
+ 4. NO separate .h file should be created
6646
+ {includecpp_note}
6647
+
6648
+ {file_rules}
6479
6649
 
6480
6650
  TYPE CONVERSIONS:
6481
6651
  - int -> int
@@ -6487,8 +6657,14 @@ TYPE CONVERSIONS:
6487
6657
  - Set[T] -> std::unordered_set<T>
6488
6658
  - Optional[T] -> std::optional<T>
6489
6659
  - Callable -> std::function<R(Args...)>
6490
- - Any -> auto or py::object
6491
- '''
6660
+ - bytes -> std::vector<uint8_t>
6661
+
6662
+ GENERIC/TEMPLATE FUNCTIONS:
6663
+ - For functions that accept any type (like choices), use C++ templates:
6664
+ template<typename T> T getRandomChoice(const std::vector<T>& choices);
6665
+ - NOT: auto getRandomChoice(const std::vector<auto>& choices) // INVALID!
6666
+
6667
+ {unconvertible_guidance}'''
6492
6668
  else:
6493
6669
  # Normal mode with header
6494
6670
  specific_rules = f'''
@@ -6497,22 +6673,30 @@ INCLUDECPP-SPECIFIC REQUIREMENTS:
6497
6673
  2. Create BOTH .cpp implementation AND .h header file
6498
6674
  3. Use #include "{module_name}.h" at top of .cpp file
6499
6675
  4. Header guard: #ifndef {module_name.upper()}_H / #define / #endif
6500
- 5. For Python features without C++ equivalent, use pybind11 wrappers:
6501
- - Generators: Use callback pattern or py::iterator
6502
- - Dynamic attributes: Use py::object
6503
- - Context managers: Use RAII pattern
6504
- - Duck typing: Use templates with concepts
6676
+ {includecpp_note}
6677
+
6678
+ {file_rules}
6505
6679
 
6506
- PYBIND11 WRAPPER PATTERN (for unconvertible features):
6680
+ GENERIC/TEMPLATE FUNCTIONS:
6681
+ - For functions that accept any type, use C++ templates:
6682
+ template<typename T> T getRandomChoice(const std::vector<T>& choices);
6683
+ - NOT: auto getRandomChoice(const std::vector<auto>& choices) // INVALID!
6684
+
6685
+ TEMPLATE EXAMPLE (for generic functions):
6507
6686
  ```cpp
6508
- #include <pybind11/pybind11.h>
6509
- namespace py = pybind11;
6687
+ // In header (.h):
6688
+ template<typename T>
6689
+ T getRandomChoice(const std::vector<T>& choices);
6510
6690
 
6511
- // For Python module calls that can't be converted:
6512
- py::object call_python_func(const std::string& module, const std::string& func) {{
6513
- py::module_ mod = py::module_::import(module.c_str());
6514
- return mod.attr(func.c_str())();
6691
+ // In implementation (.cpp):
6692
+ template<typename T>
6693
+ T Manager::getRandomChoice(const std::vector<T>& choices) {{
6694
+ // implementation
6515
6695
  }}
6696
+ // Explicit instantiations for common types:
6697
+ template int Manager::getRandomChoice<int>(const std::vector<int>&);
6698
+ template double Manager::getRandomChoice<double>(const std::vector<double>&);
6699
+ template std::string Manager::getRandomChoice<std::string>(const std::vector<std::string>&);
6516
6700
  ```
6517
6701
 
6518
6702
  TYPE CONVERSIONS:
@@ -6520,13 +6704,14 @@ TYPE CONVERSIONS:
6520
6704
  - float -> double
6521
6705
  - str -> std::string
6522
6706
  - bool -> bool
6707
+ - bytes -> std::vector<uint8_t>
6523
6708
  - List[T] -> std::vector<T>
6524
6709
  - Dict[K,V] -> std::unordered_map<K,V>
6525
6710
  - Set[T] -> std::unordered_set<T>
6526
6711
  - Optional[T] -> std::optional<T>
6527
6712
  - Callable -> std::function<R(Args...)>
6528
- - Any -> auto or py::object
6529
- '''
6713
+
6714
+ {unconvertible_guidance}'''
6530
6715
  else:
6531
6716
  direction = "C++ to Python"
6532
6717
  source_lang = "cpp"
@@ -6559,37 +6744,60 @@ TYPE CONVERSIONS:
6559
6744
  # Build output format based on mode and no_header flag
6560
6745
  if mode == 'py_to_cpp':
6561
6746
  if no_header:
6562
- output_format = f'''OUTPUT FORMAT (NO HEADER - .cpp only):
6563
- 1. Output ONLY the .cpp file content:
6747
+ output_format = f'''OUTPUT FORMAT (--no-h flag is set, NO HEADER FILE):
6748
+ **CRITICAL**: Output ONLY a single .cpp file. NO .h file allowed.
6749
+
6750
+ Output the implementation file (extension MUST be .cpp):
6564
6751
  ```cpp
6565
6752
  // {module_name}.cpp
6566
- <full cpp content with all declarations and implementations>
6753
+ #include <string>
6754
+ #include <vector>
6755
+ // ... other includes ...
6756
+
6757
+ namespace {namespace} {{
6758
+
6759
+ // All declarations AND implementations go here
6760
+ // No forward declarations needed since everything is in one file
6761
+
6762
+ }} // namespace {namespace}
6567
6763
  ```
6568
6764
 
6569
- IMPORTANT: Do NOT output a header file. Put everything in .cpp.'''
6765
+ REMEMBER:
6766
+ - File extension is .cpp (NOT .cp, NOT .cc)
6767
+ - DO NOT create a .h header file
6768
+ - Put ALL code in the single .cpp file'''
6570
6769
  else:
6571
6770
  output_format = f'''OUTPUT FORMAT (with header):
6572
- 1. First output the .cpp file content:
6771
+ 1. First output the implementation file (extension MUST be .cpp):
6573
6772
  ```cpp
6574
6773
  // {module_name}.cpp
6575
- <full cpp content>
6774
+ #include "{module_name}.h"
6775
+ // ... implementations ...
6576
6776
  ```
6577
6777
 
6578
- 2. Then the header file (REQUIRED):
6778
+ 2. Then output the header file (extension MUST be .h):
6579
6779
  ```cpp
6580
6780
  // {module_name}.h
6581
- <full header content>
6582
- ```'''
6781
+ #ifndef {module_name.upper()}_H
6782
+ #define {module_name.upper()}_H
6783
+ // ... declarations ...
6784
+ #endif // {module_name.upper()}_H
6785
+ ```
6786
+
6787
+ REMEMBER:
6788
+ - Implementation file extension is .cpp (NOT .cp, NOT .cc)
6789
+ - Header file extension is .h (NOT .hpp, NOT .hh)'''
6583
6790
  else:
6584
6791
  output_format = f'''OUTPUT FORMAT:
6585
- Output the Python file:
6792
+ Output the Python file (extension MUST be .py):
6586
6793
  ```python
6587
6794
  # {module_name}.py
6588
6795
  <full python content>
6589
6796
  ```'''
6590
6797
 
6591
6798
  prompt = f'''Convert the following {direction}.
6592
-
6799
+ You are IncludeCPP's AI code converter. Follow all requirements precisely.
6800
+ {includecpp_docs}
6593
6801
  MODULE NAME: {module_name}
6594
6802
  NAMESPACE: {namespace}
6595
6803
  {specific_rules}
@@ -6602,6 +6810,10 @@ SOURCE CODE ({source_lang}):
6602
6810
 
6603
6811
  {output_format}
6604
6812
 
6813
+ If you need clarification about how to convert specific modules or patterns:
6814
+ CLARIFICATION_NEEDED:
6815
+ - <specific question about conversion approach>
6816
+
6605
6817
  At the end, list any API changes:
6606
6818
  API_CHANGES:
6607
6819
  - <change description>
@@ -6921,5 +7133,17 @@ def cppy_types():
6921
7133
  click.echo("=" * 60)
6922
7134
 
6923
7135
 
7136
+ # ============================================================================
7137
+ # Conditional Registration of Experimental Commands
7138
+ # ============================================================================
7139
+ # AI and CPPY commands are only available when experimental features are enabled
7140
+ # Enable via: includecpp settings -> "Enable Experimental Features" checkbox
7141
+
7142
+ if _EXPERIMENTAL_ENABLED:
7143
+ cli.add_command(ai)
7144
+ cli.add_command(cppy)
7145
+ cli.add_command(project)
7146
+
7147
+
6924
7148
  if __name__ == '__main__':
6925
7149
  cli()
@@ -1,11 +1,33 @@
1
1
  import json
2
2
  import os
3
+ import sys
3
4
  import stat
4
5
  import requests
5
6
  from pathlib import Path
6
7
  from datetime import datetime
7
8
  from typing import Optional, Dict, List, Tuple, Any
8
9
 
10
+
11
+ def _supports_unicode():
12
+ """Check if terminal supports Unicode output."""
13
+ if sys.platform == 'win32':
14
+ try:
15
+ '✓✗❌'.encode(sys.stdout.encoding or 'utf-8')
16
+ return True
17
+ except (UnicodeEncodeError, LookupError, AttributeError):
18
+ return False
19
+ return True
20
+
21
+
22
+ _UNICODE_OK = _supports_unicode()
23
+
24
+ # Unicode symbols with ASCII fallbacks
25
+ SYM_CHECK = '✓' if _UNICODE_OK else '[OK]'
26
+ SYM_CROSS = '✗' if _UNICODE_OK else '[X]'
27
+ SYM_ERROR = '❌' if _UNICODE_OK else '[ERR]'
28
+ SYM_ARROW = '→' if _UNICODE_OK else '->'
29
+ SYM_BULLET = '•' if _UNICODE_OK else '*'
30
+
9
31
  MODELS = {
10
32
  'gpt-3.5-turbo': {'context': 16385, 'endpoint': 'gpt-3.5-turbo'},
11
33
  'gpt-4-turbo': {'context': 128000, 'endpoint': 'gpt-4-turbo'},
@@ -1759,29 +1781,29 @@ class AIVerboseOutput:
1759
1781
  'white': '\033[37m',
1760
1782
  }
1761
1783
 
1762
- # Status icons and messages
1784
+ # Status icons and messages - with ASCII fallbacks for Windows
1763
1785
  PHASES = {
1764
- 'init': ('⚙', 'cyan', 'Initializing'),
1765
- 'context': ('📋', 'blue', 'Building context'),
1766
- 'thinking': ('🧠', 'magenta', 'Thinking'),
1767
- 'planning': ('📝', 'yellow', 'Planning'),
1768
- 'analyzing': ('🔍', 'cyan', 'Analyzing'),
1769
- 'generating': ('✨', 'green', 'Generating'),
1770
- 'writing': ('📄', 'blue', 'Writing'),
1771
- 'editing': ('✏️', 'yellow', 'Editing'),
1772
- 'reading': ('👁', 'cyan', 'Reading'),
1773
- 'searching': ('🔎', 'blue', 'Searching'),
1774
- 'executing': ('⚡', 'magenta', 'Executing'),
1775
- 'converting': ('🔄', 'cyan', 'Converting'),
1776
- 'optimizing': ('⚡', 'green', 'Optimizing'),
1777
- 'websearch': ('🌐', 'blue', 'Web searching'),
1778
- 'parsing': ('📊', 'cyan', 'Parsing response'),
1779
- 'applying': ('💾', 'green', 'Applying changes'),
1780
- 'complete': ('✅', 'green', 'Complete'),
1781
- 'error': ('❌', 'red', 'Error'),
1782
- 'warning': ('⚠️', 'yellow', 'Warning'),
1783
- 'waiting': ('⏳', 'dim', 'Waiting for API'),
1784
- 'tool': ('🔧', 'cyan', 'Running tool'),
1786
+ 'init': ('*' if not _UNICODE_OK else '⚙', 'cyan', 'Initializing'),
1787
+ 'context': ('*' if not _UNICODE_OK else '📋', 'blue', 'Building context'),
1788
+ 'thinking': ('*' if not _UNICODE_OK else '🧠', 'magenta', 'Thinking'),
1789
+ 'planning': ('*' if not _UNICODE_OK else '📝', 'yellow', 'Planning'),
1790
+ 'analyzing': ('>' if not _UNICODE_OK else '🔍', 'cyan', 'Analyzing'),
1791
+ 'generating': ('+' if not _UNICODE_OK else '✨', 'green', 'Generating'),
1792
+ 'writing': ('>' if not _UNICODE_OK else '📄', 'blue', 'Writing'),
1793
+ 'editing': ('>' if not _UNICODE_OK else '✏️', 'yellow', 'Editing'),
1794
+ 'reading': ('>' if not _UNICODE_OK else '👁', 'cyan', 'Reading'),
1795
+ 'searching': ('>' if not _UNICODE_OK else '🔎', 'blue', 'Searching'),
1796
+ 'executing': ('!' if not _UNICODE_OK else '⚡', 'magenta', 'Executing'),
1797
+ 'converting': ('~' if not _UNICODE_OK else '🔄', 'cyan', 'Converting'),
1798
+ 'optimizing': ('!' if not _UNICODE_OK else '⚡', 'green', 'Optimizing'),
1799
+ 'websearch': ('@' if not _UNICODE_OK else '🌐', 'blue', 'Web searching'),
1800
+ 'parsing': ('>' if not _UNICODE_OK else '📊', 'cyan', 'Parsing response'),
1801
+ 'applying': ('+' if not _UNICODE_OK else '💾', 'green', 'Applying changes'),
1802
+ 'complete': ('[OK]' if not _UNICODE_OK else '✅', 'green', 'Complete'),
1803
+ 'error': ('[ERR]' if not _UNICODE_OK else '❌', 'red', 'Error'),
1804
+ 'warning': ('[!]' if not _UNICODE_OK else '⚠️', 'yellow', 'Warning'),
1805
+ 'waiting': ('...' if not _UNICODE_OK else '⏳', 'dim', 'Waiting for API'),
1806
+ 'tool': ('#' if not _UNICODE_OK else '🔧', 'cyan', 'Running tool'),
1785
1807
  }
1786
1808
 
1787
1809
  def __init__(self, enabled: bool = True, use_colors: bool = True):
@@ -1893,7 +1915,9 @@ class AIVerboseOutput:
1893
1915
  pct = int((current / total) * 100) if total > 0 else 0
1894
1916
  bar_width = 30
1895
1917
  filled = int(bar_width * current / total) if total > 0 else 0
1896
- bar = "█" * filled + "░" * (bar_width - filled)
1918
+ fill_char = '#' if not _UNICODE_OK else '█'
1919
+ empty_char = '-' if not _UNICODE_OK else '░'
1920
+ bar = fill_char * filled + empty_char * (bar_width - filled)
1897
1921
  label_str = f" {label}" if label else ""
1898
1922
  print(f"\r{indent} [{self._color(bar, 'cyan')}] {pct}%{label_str}", end='', flush=True)
1899
1923
  if current >= total:
@@ -1917,10 +1941,10 @@ class AIVerboseOutput:
1917
1941
  return
1918
1942
  indent = self._get_indent()
1919
1943
  if success:
1920
- icon = "✓"
1944
+ icon = SYM_CHECK
1921
1945
  color = 'green'
1922
1946
  else:
1923
- icon = "✗"
1947
+ icon = SYM_CROSS
1924
1948
  color = 'red'
1925
1949
  msg = message[:60] + "..." if message and len(message) > 60 else (message or "")
1926
1950
  print(f"{indent} {self._color(icon, color)} {msg}")
@@ -2064,15 +2088,26 @@ class AIVerboseOutput:
2064
2088
  if not self.enabled:
2065
2089
  return
2066
2090
  indent = self._get_indent()
2067
- ops = {
2068
- 'read': ('👁', 'Reading'),
2069
- 'write': ('📝', 'Writing'),
2070
- 'edit': ('✏️', 'Editing'),
2071
- 'delete': ('🗑', 'Deleting'),
2072
- 'create': ('📁', 'Creating'),
2073
- }
2074
- icon, label = ops.get(operation, ('📄', operation))
2075
- status = self._color('', 'green') if success else self._color('✗', 'red')
2091
+ if _UNICODE_OK:
2092
+ ops = {
2093
+ 'read': ('👁', 'Reading'),
2094
+ 'write': ('📝', 'Writing'),
2095
+ 'edit': ('✏️', 'Editing'),
2096
+ 'delete': ('🗑', 'Deleting'),
2097
+ 'create': ('📁', 'Creating'),
2098
+ }
2099
+ default_icon = '📄'
2100
+ else:
2101
+ ops = {
2102
+ 'read': ('>', 'Reading'),
2103
+ 'write': ('>', 'Writing'),
2104
+ 'edit': ('>', 'Editing'),
2105
+ 'delete': ('x', 'Deleting'),
2106
+ 'create': ('+', 'Creating'),
2107
+ }
2108
+ default_icon = '>'
2109
+ icon, label = ops.get(operation, (default_icon, operation))
2110
+ status = self._color(SYM_CHECK, 'green') if success else self._color(SYM_CROSS, 'red')
2076
2111
  # Truncate path if too long
2077
2112
  display_path = path if len(path) <= 40 else "..." + path[-37:]
2078
2113
  print(f"{indent} {icon} {label}: {display_path} {status}")