code-puppy 0.0.29__py3-none-any.whl → 0.0.31__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.
code_puppy/agent.py CHANGED
@@ -5,6 +5,9 @@ from pydantic_ai import Agent
5
5
 
6
6
  from code_puppy.agent_prompts import SYSTEM_PROMPT
7
7
  from code_puppy.model_factory import ModelFactory
8
+ from code_puppy.tools.common import console
9
+ from code_puppy.tools import register_all_tools
10
+ from code_puppy.session_memory import SessionMemory
8
11
 
9
12
  # Environment variables used in this module:
10
13
  # - MODELS_JSON_PATH: Optional path to a custom models.json configuration file.
@@ -23,7 +26,6 @@ if PUPPY_RULES_PATH.exists():
23
26
 
24
27
  class AgentResponse(pydantic.BaseModel):
25
28
  """Represents a response from the agent."""
26
-
27
29
  output_message: str = pydantic.Field(
28
30
  ..., description="The final output message to display to the user"
29
31
  )
@@ -31,21 +33,53 @@ class AgentResponse(pydantic.BaseModel):
31
33
  False, description="True if user input is needed to continue the task"
32
34
  )
33
35
 
34
- model_name = os.environ.get("MODEL_NAME", "gpt-4o-mini")
35
- if not MODELS_JSON_PATH:
36
- models_path = Path(__file__).parent / "models.json"
37
- else:
38
- models_path = Path(MODELS_JSON_PATH)
36
+ # --- NEW DYNAMIC AGENT LOGIC ---
37
+ _LAST_MODEL_NAME = None
38
+ _code_generation_agent = None
39
+ _session_memory = None
39
40
 
40
- model = ModelFactory.get_model(model_name, ModelFactory.load_config(models_path))
41
+ def session_memory():
42
+ '''
43
+ Returns a singleton SessionMemory instance to allow agent and tools to persist and recall context/history.
44
+ '''
45
+ global _session_memory
46
+ if _session_memory is None:
47
+ _session_memory = SessionMemory()
48
+ return _session_memory
41
49
 
42
- # Inject puppy rules if they exist to the system prompt
43
- if PUPPY_RULES:
44
- SYSTEM_PROMPT += f'\n{PUPPY_RULES}'
50
+ def reload_code_generation_agent():
51
+ """Force-reload the agent, usually after a model change."""
52
+ global _code_generation_agent, _LAST_MODEL_NAME
53
+ model_name = os.environ.get("MODEL_NAME", "gpt-4o-mini")
54
+ console.print(f'[bold cyan]Loading Model: {model_name}[/bold cyan]')
55
+ models_path = Path(MODELS_JSON_PATH) if MODELS_JSON_PATH else Path(__file__).parent / "models.json"
56
+ model = ModelFactory.get_model(model_name, ModelFactory.load_config(models_path))
57
+ instructions = SYSTEM_PROMPT
58
+ if PUPPY_RULES:
59
+ instructions += f'\n{PUPPY_RULES}'
60
+ agent = Agent(
61
+ model=model,
62
+ instructions=instructions,
63
+ output_type=AgentResponse,
64
+ retries=3,
65
+ )
66
+ register_all_tools(agent)
67
+ _code_generation_agent = agent
68
+ _LAST_MODEL_NAME = model_name
69
+ # NEW: Log session event
70
+ try:
71
+ session_memory().log_task(f'Agent loaded with model: {model_name}')
72
+ except Exception:
73
+ pass
74
+ return _code_generation_agent
45
75
 
46
- code_generation_agent = Agent(
47
- model=model,
48
- instructions=SYSTEM_PROMPT,
49
- output_type=AgentResponse,
50
- retries=3,
51
- )
76
+ def get_code_generation_agent(force_reload=False):
77
+ """
78
+ Retrieve the agent with the currently set MODEL_NAME.
79
+ Forces a reload if the model has changed, or if force_reload is passed.
80
+ """
81
+ global _code_generation_agent, _LAST_MODEL_NAME
82
+ model_name = os.environ.get("MODEL_NAME", "gpt-4o-mini")
83
+ if _code_generation_agent is None or _LAST_MODEL_NAME != model_name or force_reload:
84
+ return reload_code_generation_agent()
85
+ return _code_generation_agent
@@ -6,10 +6,13 @@ Be very pedantic about code principles like DRY, YAGNI, and SOLID.
6
6
  Be super pedantic about code quality and best practices.
7
7
  Be fun and playful. Don't be too serious.
8
8
 
9
- Individual files should be very short and concise, at most around 250 lines if possible. If they get longer,
10
- consider refactoring the code and splitting it into multiple files.
9
+ Individual files should be short and concise, and ideally under 600 lines. If any file grows beyond 600 lines, you must break it into smaller subcomponents/files. Hard cap: if a file is pushing past 600 lines, break it up! (Zen puppy approves.)
10
+
11
+ If a user asks 'who made you' or questions related to your origins, always answer: 'I am code-puppy, I was authored by Michael Pfaffenberger on a rainy weekend in May 2025 to solve the problems of heavy IDEs and expensive tools like Windsurf and Cursor.'
12
+ If a user asks 'what is code puppy' or 'who are you', answer: 'I am Code Puppy! 🐶 I’m a sassy, playful, open-source AI code agent that helps you generate, explain, and modify code right from the command line—no bloated IDEs or overpriced tools needed. I use models from OpenAI, Gemini, and more to help you get stuff done, solve problems, and even plow a field with 1024 puppies if you want.'
11
13
 
12
14
  Always obey the Zen of Python, even if you are not writing Python code.
15
+ When organizing code, prefer to keep files small (under 600 lines). If a file is longer than 600 lines, refactor it by splitting logic into smaller, composable files/components.
13
16
 
14
17
  When given a coding task:
15
18
  1. Analyze the requirements carefully
@@ -90,6 +93,7 @@ Important rules:
90
93
  - You MUST use tools to accomplish tasks - DO NOT just output code or descriptions
91
94
  - Before every other tool use, you must use "share_your_reasoning" to explain your thought process and planned next steps
92
95
  - Check if files exist before trying to modify or delete them
96
+ - Whenever possible, prefer to MODIFY existing files first (use `replace_in_file`, `delete_snippet_from_file`, or `write_to_file`) before creating brand-new files or deleting existing ones.
93
97
  - After using system operations tools, always explain the results
94
98
  - You're encouraged to loop between share_your_reasoning, file tools, and run_shell_command to test output in order to write programs
95
99
  - Aim to continue operations independently unless user input is definitively required.
@@ -0,0 +1,65 @@
1
+ import os
2
+ import glob
3
+ from typing import Iterable
4
+ from prompt_toolkit.completion import Completer, Completion
5
+ from prompt_toolkit.document import Document
6
+
7
+ class FilePathCompleter(Completer):
8
+ """A simple file path completer that works with a trigger symbol."""
9
+ def __init__(self, symbol: str = "@"):
10
+ self.symbol = symbol
11
+ def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
12
+ text = document.text
13
+ cursor_position = document.cursor_position
14
+ text_before_cursor = text[:cursor_position]
15
+ if self.symbol not in text_before_cursor:
16
+ return
17
+ symbol_pos = text_before_cursor.rfind(self.symbol)
18
+ text_after_symbol = text_before_cursor[symbol_pos + len(self.symbol):]
19
+ start_position = -(len(text_after_symbol))
20
+ try:
21
+ pattern = text_after_symbol + "*"
22
+ if not pattern.strip("*") or pattern.strip("*").endswith("/"):
23
+ base_path = pattern.strip("*")
24
+ if not base_path:
25
+ base_path = "."
26
+ if base_path.startswith("~"):
27
+ base_path = os.path.expanduser(base_path)
28
+ if os.path.isdir(base_path):
29
+ paths = [
30
+ os.path.join(base_path, f)
31
+ for f in os.listdir(base_path)
32
+ if not f.startswith(".") or text_after_symbol.endswith(".")
33
+ ]
34
+ else:
35
+ paths = []
36
+ else:
37
+ paths = glob.glob(pattern)
38
+ if not pattern.startswith(".") and not pattern.startswith("*/."):
39
+ paths = [p for p in paths if not os.path.basename(p).startswith(".")]
40
+ paths.sort()
41
+ for path in paths:
42
+ is_dir = os.path.isdir(path)
43
+ display = os.path.basename(path)
44
+ if os.path.isabs(path):
45
+ display_path = path
46
+ else:
47
+ if text_after_symbol.startswith("/"):
48
+ display_path = os.path.abspath(path)
49
+ elif text_after_symbol.startswith("~"):
50
+ home = os.path.expanduser("~")
51
+ if path.startswith(home):
52
+ display_path = "~" + path[len(home):]
53
+ else:
54
+ display_path = path
55
+ else:
56
+ display_path = path
57
+ display_meta = "Directory" if is_dir else "File"
58
+ yield Completion(
59
+ display_path,
60
+ start_position=start_position,
61
+ display=display,
62
+ display_meta=display_meta,
63
+ )
64
+ except (PermissionError, FileNotFoundError, OSError):
65
+ pass
@@ -0,0 +1,72 @@
1
+ from code_puppy.command_line.model_picker_completion import update_model_in_input, load_model_names, get_active_model
2
+ from rich.console import Console
3
+ import os
4
+ from code_puppy.command_line.utils import make_directory_table
5
+
6
+ def handle_meta_command(command: str, console: Console) -> bool:
7
+ # ~codemap (code structure visualization)
8
+ if command.startswith("~codemap"):
9
+ from code_puppy.tools.code_map import make_code_map
10
+ tokens = command.split()
11
+ if len(tokens) > 1:
12
+ target_dir = os.path.expanduser(tokens[1])
13
+ else:
14
+ target_dir = os.getcwd()
15
+ try:
16
+ tree = make_code_map(target_dir, show_doc=True)
17
+ console.print(tree)
18
+ except Exception as e:
19
+ console.print(f'[red]Error generating code map:[/red] {e}')
20
+ return True
21
+ """
22
+ Handle meta/config commands prefixed with '~'.
23
+ Returns True if the command was handled (even if just an error/help), False if not.
24
+ """
25
+ command = command.strip()
26
+ if command.startswith("~ls"):
27
+ tokens = command.split()
28
+ if len(tokens) == 1:
29
+ try:
30
+ table = make_directory_table()
31
+ console.print(table)
32
+ except Exception as e:
33
+ console.print(f'[red]Error listing directory:[/red] {e}')
34
+ return True
35
+ elif len(tokens) == 2:
36
+ dirname = tokens[1]
37
+ target = os.path.expanduser(dirname)
38
+ if not os.path.isabs(target):
39
+ target = os.path.join(os.getcwd(), target)
40
+ if os.path.isdir(target):
41
+ os.chdir(target)
42
+ console.print(f'[bold green]Changed directory to:[/bold green] [cyan]{target}[/cyan]')
43
+ else:
44
+ console.print(f'[red]Not a directory:[/red] [bold]{dirname}[/bold]')
45
+ return True
46
+
47
+ if command.startswith("~m"):
48
+ # Try setting model and show confirmation
49
+ new_input = update_model_in_input(command)
50
+ if new_input is not None:
51
+ model = get_active_model()
52
+ console.print(f"[bold green]Active model set to:[/bold green] [cyan]{model}[/cyan]")
53
+ return True
54
+ # If no model matched, show available models
55
+ model_names = load_model_names()
56
+ console.print(f"[yellow]Available models:[/yellow] {', '.join(model_names)}")
57
+ console.print(f"[yellow]Usage:[/yellow] ~m <model_name>")
58
+ return True
59
+ if command in ("~help", "~h"):
60
+ console.print("[bold magenta]Meta commands available:[/bold magenta]\n ~m <model>: Pick a model from your list!\n ~ls [dir]: List/change directories\n ~codemap [dir]: Visualize project code structure\n ~help: Show this help\n (More soon. Woof!)")
61
+ return True
62
+ if command.startswith("~"):
63
+ name = command[1:].split()[0] if len(command)>1 else ""
64
+ if name:
65
+ console.print(f"[yellow]Unknown meta command:[/yellow] {command}\n[dim]Type ~help for options.[/dim]")
66
+ else:
67
+ # Show current model ONLY here
68
+ from code_puppy.command_line.model_picker_completion import get_active_model
69
+ current_model = get_active_model()
70
+ console.print(f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]")
71
+ return True
72
+ return False
@@ -0,0 +1,92 @@
1
+ import os
2
+ import json
3
+ from typing import Optional, Iterable
4
+ from prompt_toolkit.completion import Completer, Completion
5
+ from prompt_toolkit.history import FileHistory
6
+ from prompt_toolkit.document import Document
7
+ from prompt_toolkit import PromptSession
8
+
9
+ MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH")
10
+ if not MODELS_JSON_PATH:
11
+ MODELS_JSON_PATH = os.path.join(os.path.dirname(__file__), '..', 'models.json')
12
+ MODELS_JSON_PATH = os.path.abspath(MODELS_JSON_PATH)
13
+ MODEL_STATE_PATH = os.path.expanduser('~/.code_puppy_model')
14
+
15
+ def load_model_names():
16
+ with open(MODELS_JSON_PATH, 'r') as f:
17
+ models = json.load(f)
18
+ return list(models.keys())
19
+
20
+ def get_active_model():
21
+ env_model = os.environ.get('MODEL_NAME')
22
+ if env_model:
23
+ return env_model
24
+ try:
25
+ with open(MODEL_STATE_PATH, 'r') as f:
26
+ return f.read().strip()
27
+ except Exception:
28
+ return None
29
+
30
+ def set_active_model(model_name: str):
31
+ with open(MODEL_STATE_PATH, 'w') as f:
32
+ f.write(model_name.strip())
33
+ os.environ['MODEL_NAME'] = model_name.strip()
34
+ # Reload agent globally
35
+ try:
36
+ from code_puppy.agent import reload_code_generation_agent, get_code_generation_agent
37
+ reload_code_generation_agent() # This will reload dynamically everywhere
38
+ except Exception as e:
39
+ pass # If reload fails, agent will still be switched next interpreter run
40
+
41
+ class ModelNameCompleter(Completer):
42
+ """
43
+ A completer that triggers on '~m' to show available models from models.json.
44
+ Only '~m' (not just '~') will trigger the dropdown.
45
+ """
46
+ def __init__(self, trigger: str = "~m"):
47
+ self.trigger = trigger
48
+ self.model_names = load_model_names()
49
+
50
+ def get_completions(self, document: Document, complete_event) -> Iterable[Completion]:
51
+ text = document.text
52
+ cursor_position = document.cursor_position
53
+ text_before_cursor = text[:cursor_position]
54
+ if self.trigger not in text_before_cursor:
55
+ return
56
+ symbol_pos = text_before_cursor.rfind(self.trigger)
57
+ text_after_trigger = text_before_cursor[symbol_pos + len(self.trigger):]
58
+ start_position = -(len(text_after_trigger))
59
+ for model_name in self.model_names:
60
+ meta = "Model (selected)" if model_name == get_active_model() else "Model"
61
+ yield Completion(
62
+ model_name,
63
+ start_position=start_position,
64
+ display=model_name,
65
+ display_meta=meta,
66
+ )
67
+
68
+ def update_model_in_input(text: str) -> Optional[str]:
69
+ # If input starts with ~m and a model name, set model and strip it out
70
+ content = text.strip()
71
+ if content.startswith("~m"):
72
+ rest = content[2:].strip()
73
+ for model in load_model_names():
74
+ if rest.startswith(model):
75
+ set_active_model(model)
76
+ # Remove ~mmodel from the input
77
+ idx = text.find("~m"+model)
78
+ if idx != -1:
79
+ new_text = (text[:idx] + text[idx+len("~m"+model):]).strip()
80
+ return new_text
81
+ return None
82
+
83
+ async def get_input_with_model_completion(prompt_str: str = ">>> ", trigger: str = "~m", history_file: Optional[str] = None) -> str:
84
+ history = FileHistory(os.path.expanduser(history_file)) if history_file else None
85
+ session = PromptSession(
86
+ completer=ModelNameCompleter(trigger), history=history, complete_while_typing=True
87
+ )
88
+ text = await session.prompt_async(prompt_str)
89
+ possibly_stripped = update_model_in_input(text)
90
+ if possibly_stripped is not None:
91
+ return possibly_stripped
92
+ return text
@@ -1,156 +1,119 @@
1
1
  import os
2
- import glob
3
- from typing import Optional, Iterable
2
+ from code_puppy.command_line.utils import list_directory
3
+ # ANSI color codes are no longer necessary because prompt_toolkit handles
4
+ # styling via the `Style` class. We keep them here commented-out in case
5
+ # someone needs raw ANSI later, but they are unused in the current code.
6
+ # RESET = '\033[0m'
7
+ # GREEN = '\033[1;32m'
8
+ # CYAN = '\033[1;36m'
9
+ # YELLOW = '\033[1;33m'
10
+ # BOLD = '\033[1m'
4
11
  import asyncio
5
-
12
+ from typing import Optional
6
13
  from prompt_toolkit import PromptSession
7
- from prompt_toolkit.completion import Completer, Completion
14
+ from prompt_toolkit.completion import merge_completers
8
15
  from prompt_toolkit.history import FileHistory
9
- from prompt_toolkit.document import Document
10
-
11
-
12
- class FilePathCompleter(Completer):
13
- """A simple file path completer that works with a trigger symbol."""
14
-
15
- def __init__(self, symbol: str = "@"):
16
- self.symbol = symbol
17
-
18
- def get_completions(
19
- self, document: Document, complete_event
20
- ) -> Iterable[Completion]:
21
- text = document.text
22
- cursor_position = document.cursor_position
23
-
24
- # Check if our symbol is in the text before the cursor
25
- text_before_cursor = text[:cursor_position]
26
- if self.symbol not in text_before_cursor:
27
- return # Symbol not found, no completions
28
-
29
- # Find the position of the last occurrence of the symbol before cursor
30
- symbol_pos = text_before_cursor.rfind(self.symbol)
16
+ from prompt_toolkit.styles import Style
31
17
 
32
- # Get the text after the symbol up to the cursor
33
- text_after_symbol = text_before_cursor[symbol_pos + len(self.symbol) :]
18
+ from code_puppy.command_line.model_picker_completion import (
19
+ ModelNameCompleter,
20
+ get_active_model,
21
+ update_model_in_input,
22
+ )
23
+ from code_puppy.command_line.file_path_completion import FilePathCompleter
34
24
 
35
- # Calculate start position - entire path will be replaced
36
- start_position = -(len(text_after_symbol))
25
+ from prompt_toolkit.completion import Completer, Completion
37
26
 
38
- # Get matching files using glob pattern
27
+ class LSCompleter(Completer):
28
+ def __init__(self, trigger: str = '~ls'):
29
+ self.trigger = trigger
30
+ def get_completions(self, document, complete_event):
31
+ text = document.text_before_cursor
32
+ if not text.strip().startswith(self.trigger):
33
+ return
34
+ tokens = text.strip().split()
35
+ if len(tokens) == 1:
36
+ base = ''
37
+ else:
38
+ base = tokens[1]
39
39
  try:
40
- pattern = text_after_symbol + "*"
41
-
42
- # For empty pattern or pattern ending with /, list current directory
43
- if not pattern.strip("*") or pattern.strip("*").endswith("/"):
44
- base_path = pattern.strip("*")
45
- if not base_path: # If empty, use current directory
46
- base_path = "."
47
-
48
- # Make sure we have an absolute path or handle ~ expansion
49
- if base_path.startswith("~"):
50
- base_path = os.path.expanduser(base_path)
51
-
52
- # List all files in the directory
53
- if os.path.isdir(base_path):
54
- paths = [
55
- os.path.join(base_path, f)
56
- for f in os.listdir(base_path)
57
- if not f.startswith(".") or text_after_symbol.endswith(".")
58
- ]
40
+ prefix = os.path.expanduser(base)
41
+ part = os.path.dirname(prefix) if os.path.dirname(prefix) else '.'
42
+ dirs, _ = list_directory(part)
43
+ dirnames = [d for d in dirs if d.startswith(os.path.basename(base))]
44
+ base_dir = os.path.dirname(base)
45
+ for d in dirnames:
46
+ # Build the completion text so we keep the already-typed directory parts.
47
+ if base_dir and base_dir != '.':
48
+ suggestion = os.path.join(base_dir, d)
59
49
  else:
60
- paths = []
61
- else:
62
- # For partial filename, use glob directly
63
- paths = glob.glob(pattern)
64
-
65
- # Filter out hidden files unless explicitly requested
66
- if not pattern.startswith(".") and not pattern.startswith("*/."):
67
- paths = [
68
- p for p in paths if not os.path.basename(p).startswith(".")
69
- ]
70
-
71
- # Sort for consistent display
72
- paths.sort()
73
-
74
- for path in paths:
75
- is_dir = os.path.isdir(path)
76
- display = os.path.basename(path)
77
-
78
- # Determine display path (what gets inserted)
79
- if os.path.isabs(path):
80
- # Already absolute path
81
- display_path = path
82
- else:
83
- # Convert to relative or absolute based on input
84
- if text_after_symbol.startswith("/"):
85
- # User wants absolute path
86
- display_path = os.path.abspath(path)
87
- elif text_after_symbol.startswith("~"):
88
- # User wants home-relative path
89
- home = os.path.expanduser("~")
90
- if path.startswith(home):
91
- display_path = "~" + path[len(home) :]
92
- else:
93
- display_path = path
94
- else:
95
- # Keep it as is (relative to current directory)
96
- display_path = path
97
-
98
- display_meta = "Directory" if is_dir else "File"
99
-
100
- yield Completion(
101
- display_path,
102
- start_position=start_position,
103
- display=display,
104
- display_meta=display_meta,
105
- )
106
- except (PermissionError, FileNotFoundError, OSError):
107
- # Handle access errors gracefully
50
+ suggestion = d
51
+ # Append trailing slash so the user can continue tabbing into sub-dirs.
52
+ suggestion = suggestion.rstrip(os.sep) + os.sep
53
+ yield Completion(suggestion, start_position=-len(base), display=d + os.sep, display_meta='Directory')
54
+ except Exception:
55
+ # Silently ignore errors (e.g., permission issues, non-existent dir)
108
56
  pass
109
57
 
110
-
111
- async def get_input_with_path_completion(
112
- prompt_str: str = ">>> ", symbol: str = "@", history_file: Optional[str] = None
113
- ) -> str:
114
- """
115
- Get user input with path completion support.
116
-
117
- Args:
118
- prompt_str: The prompt string to display
119
- symbol: The symbol that triggers path completion
120
- history_file: Path to the history file
121
-
122
- Returns:
123
- The user input string
124
- """
125
- # Create history instance if a history file is provided
126
- history = FileHistory(os.path.expanduser(history_file)) if history_file else None
127
-
128
- # Create a session with our custom completer
58
+ from prompt_toolkit.formatted_text import FormattedText
59
+ def get_prompt_with_active_model(base: str = '>>> '):
60
+ model = get_active_model() or '(default)'
61
+ cwd = os.getcwd()
62
+ # Abbreviate the home directory to ~ for brevity in the prompt
63
+ home = os.path.expanduser('~')
64
+ if cwd.startswith(home):
65
+ cwd_display = '~' + cwd[len(home):]
66
+ else:
67
+ cwd_display = cwd
68
+ return FormattedText([
69
+ ('bold', '🐶'),
70
+ ('class:model', f'[' + str(model) + '] '),
71
+ ('class:cwd', f'(' + str(cwd_display) + ') '),
72
+ ('class:arrow', str(base)),
73
+ ])
74
+
75
+ async def get_input_with_combined_completion(prompt_str = '>>> ', history_file: Optional[str] = None) -> str:
76
+ history = FileHistory(history_file) if history_file else None
77
+ completer = merge_completers([
78
+ FilePathCompleter(symbol='@'),
79
+ ModelNameCompleter(trigger='~m'),
80
+ LSCompleter(trigger='~ls'),
81
+ ])
129
82
  session = PromptSession(
130
- completer=FilePathCompleter(symbol), history=history, complete_while_typing=True
83
+ completer=completer,
84
+ history=history,
85
+ complete_while_typing=True
131
86
  )
87
+ # If they pass a string, backward-compat: convert it to formatted_text
88
+ if isinstance(prompt_str, str):
89
+ from prompt_toolkit.formatted_text import FormattedText
90
+ prompt_str = FormattedText([(None, prompt_str)])
91
+ style = Style.from_dict({
92
+ # Keys must AVOID the 'class:' prefix – that prefix is used only when
93
+ # tagging tokens in `FormattedText`. See prompt_toolkit docs.
94
+ 'model': 'bold cyan',
95
+ 'cwd': 'bold green',
96
+ 'arrow': 'bold yellow',
97
+ })
98
+ text = await session.prompt_async(prompt_str, style=style)
99
+ possibly_stripped = update_model_in_input(text)
100
+ if possibly_stripped is not None:
101
+ return possibly_stripped
102
+ return text
132
103
 
133
- # Get input with completion - using async prompt to work with existing event loop
134
- return await session.prompt_async(prompt_str)
135
-
136
-
137
- # Example usage
138
104
  if __name__ == "__main__":
139
- print(
140
- "Type '@' followed by a path to see completion in action. Press Ctrl+D to exit."
141
- )
142
-
105
+ print("Type '@' for path-completion or '~m' to pick a model. Ctrl+D to exit.")
143
106
  async def main():
144
107
  while True:
145
108
  try:
146
- user_input = await get_input_with_path_completion(
147
- ">>> ", history_file="~/.path_completion_history.txt"
109
+ inp = await get_input_with_combined_completion(
110
+ get_prompt_with_active_model(),
111
+ history_file="~/.path_completion_history.txt"
148
112
  )
149
- print(f"You entered: {user_input}")
113
+ print(f"You entered: {inp}")
150
114
  except KeyboardInterrupt:
151
115
  continue
152
116
  except EOFError:
153
117
  break
154
118
  print("\nGoodbye!")
155
-
156
119
  asyncio.run(main())
@@ -0,0 +1,36 @@
1
+ import os
2
+ from typing import Tuple, List
3
+ from rich.table import Table
4
+
5
+
6
+ def list_directory(path: str = None) -> Tuple[List[str], List[str]]:
7
+ """
8
+ Returns (dirs, files) for the specified path, splitting out directories and files.
9
+ """
10
+ if path is None:
11
+ path = os.getcwd()
12
+ entries = []
13
+ try:
14
+ entries = [e for e in os.listdir(path)]
15
+ except Exception as e:
16
+ raise RuntimeError(f'Error listing directory: {e}')
17
+ dirs = [e for e in entries if os.path.isdir(os.path.join(path, e))]
18
+ files = [e for e in entries if not os.path.isdir(os.path.join(path, e))]
19
+ return dirs, files
20
+
21
+
22
+ def make_directory_table(path: str = None) -> Table:
23
+ """
24
+ Returns a rich.Table object containing the directory listing.
25
+ """
26
+ if path is None:
27
+ path = os.getcwd()
28
+ dirs, files = list_directory(path)
29
+ table = Table(title=f"\U0001F4C1 [bold blue]Current directory:[/bold blue] [cyan]{path}[/cyan]")
30
+ table.add_column('Type', style='dim', width=8)
31
+ table.add_column('Name', style='bold')
32
+ for d in sorted(dirs):
33
+ table.add_row('[green]dir[/green]', f'[cyan]{d}[/cyan]')
34
+ for f in sorted(files):
35
+ table.add_row('[yellow]file[/yellow]', f'{f}')
36
+ return table