tunacode-cli 0.0.1__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.

Potentially problematic release.


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

Files changed (65) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/__init__.py +4 -0
  3. tunacode/cli/commands.py +632 -0
  4. tunacode/cli/main.py +47 -0
  5. tunacode/cli/repl.py +251 -0
  6. tunacode/configuration/__init__.py +1 -0
  7. tunacode/configuration/defaults.py +26 -0
  8. tunacode/configuration/models.py +69 -0
  9. tunacode/configuration/settings.py +32 -0
  10. tunacode/constants.py +129 -0
  11. tunacode/context.py +83 -0
  12. tunacode/core/__init__.py +0 -0
  13. tunacode/core/agents/__init__.py +0 -0
  14. tunacode/core/agents/main.py +119 -0
  15. tunacode/core/setup/__init__.py +17 -0
  16. tunacode/core/setup/agent_setup.py +41 -0
  17. tunacode/core/setup/base.py +37 -0
  18. tunacode/core/setup/config_setup.py +179 -0
  19. tunacode/core/setup/coordinator.py +45 -0
  20. tunacode/core/setup/environment_setup.py +62 -0
  21. tunacode/core/setup/git_safety_setup.py +188 -0
  22. tunacode/core/setup/undo_setup.py +32 -0
  23. tunacode/core/state.py +43 -0
  24. tunacode/core/tool_handler.py +57 -0
  25. tunacode/exceptions.py +105 -0
  26. tunacode/prompts/system.txt +71 -0
  27. tunacode/py.typed +0 -0
  28. tunacode/services/__init__.py +1 -0
  29. tunacode/services/mcp.py +86 -0
  30. tunacode/services/undo_service.py +244 -0
  31. tunacode/setup.py +50 -0
  32. tunacode/tools/__init__.py +0 -0
  33. tunacode/tools/base.py +244 -0
  34. tunacode/tools/read_file.py +89 -0
  35. tunacode/tools/run_command.py +107 -0
  36. tunacode/tools/update_file.py +117 -0
  37. tunacode/tools/write_file.py +82 -0
  38. tunacode/types.py +259 -0
  39. tunacode/ui/__init__.py +1 -0
  40. tunacode/ui/completers.py +129 -0
  41. tunacode/ui/console.py +74 -0
  42. tunacode/ui/constants.py +16 -0
  43. tunacode/ui/decorators.py +59 -0
  44. tunacode/ui/input.py +95 -0
  45. tunacode/ui/keybindings.py +27 -0
  46. tunacode/ui/lexers.py +46 -0
  47. tunacode/ui/output.py +109 -0
  48. tunacode/ui/panels.py +156 -0
  49. tunacode/ui/prompt_manager.py +117 -0
  50. tunacode/ui/tool_ui.py +187 -0
  51. tunacode/ui/validators.py +23 -0
  52. tunacode/utils/__init__.py +0 -0
  53. tunacode/utils/bm25.py +55 -0
  54. tunacode/utils/diff_utils.py +69 -0
  55. tunacode/utils/file_utils.py +41 -0
  56. tunacode/utils/ripgrep.py +17 -0
  57. tunacode/utils/system.py +336 -0
  58. tunacode/utils/text_utils.py +87 -0
  59. tunacode/utils/user_configuration.py +54 -0
  60. tunacode_cli-0.0.1.dist-info/METADATA +242 -0
  61. tunacode_cli-0.0.1.dist-info/RECORD +65 -0
  62. tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
  63. tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
  64. tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
  65. tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
tunacode/ui/console.py ADDED
@@ -0,0 +1,74 @@
1
+ """Main console coordination module for Sidekick UI.
2
+
3
+ This module re-exports functions from specialized UI modules to maintain
4
+ backward compatibility while organizing code into focused modules.
5
+ """
6
+
7
+ from rich.console import Console as RichConsole
8
+ from rich.markdown import Markdown
9
+
10
+ # Import and re-export all functions from specialized modules
11
+ from .input import formatted_text, input, multiline_input
12
+ from .keybindings import create_key_bindings
13
+ from .output import (banner, clear, info, line, muted, print, spinner, success, sync_print,
14
+ update_available, usage, version, warning)
15
+ from .panels import (agent, dump_messages, error, help, models, panel, sync_panel,
16
+ sync_tool_confirm, tool_confirm)
17
+ from .prompt_manager import PromptConfig, PromptManager
18
+ from .validators import ModelValidator
19
+
20
+ # Create console object for backward compatibility
21
+ console = RichConsole()
22
+
23
+ # Create key bindings object for backward compatibility
24
+ kb = create_key_bindings()
25
+
26
+
27
+ # Re-export markdown utility for backward compatibility
28
+ def markdown(text: str) -> Markdown:
29
+ """Create a Markdown object."""
30
+ return Markdown(text)
31
+
32
+
33
+ # All functions are now available through imports above
34
+ __all__ = [
35
+ # From input module
36
+ "formatted_text",
37
+ "input",
38
+ "multiline_input",
39
+ # From keybindings module
40
+ "create_key_bindings",
41
+ "kb",
42
+ # From output module
43
+ "banner",
44
+ "clear",
45
+ "console",
46
+ "info",
47
+ "line",
48
+ "muted",
49
+ "print",
50
+ "spinner",
51
+ "success",
52
+ "sync_print",
53
+ "update_available",
54
+ "usage",
55
+ "version",
56
+ "warning",
57
+ # From panels module
58
+ "agent",
59
+ "dump_messages",
60
+ "error",
61
+ "help",
62
+ "models",
63
+ "panel",
64
+ "sync_panel",
65
+ "sync_tool_confirm",
66
+ "tool_confirm",
67
+ # From prompt_manager module
68
+ "PromptConfig",
69
+ "PromptManager",
70
+ # From validators module
71
+ "ModelValidator",
72
+ # Local utilities
73
+ "markdown",
74
+ ]
@@ -0,0 +1,16 @@
1
+ """UI-specific constants for Sidekick."""
2
+
3
+ # UI Layout Constants
4
+ DEFAULT_PANEL_PADDING = {"top": 1, "right": 0, "bottom": 1, "left": 0}
5
+
6
+ # Spinner Configuration
7
+ SPINNER_TYPE = "dots12" # Modern spinner style
8
+ SPINNER_STYLE = "#00d7ff" # Modern cyan color
9
+
10
+ # Input Configuration
11
+ DEFAULT_PROMPT = "❯ "
12
+ MULTILINE_PROMPT = " ❯ "
13
+ MAX_HISTORY_SIZE = 1000
14
+
15
+ # Display Limits
16
+ MAX_DISPLAY_LENGTH = 50 # For truncating in UI
@@ -0,0 +1,59 @@
1
+ """
2
+ Module: sidekick.ui.decorators
3
+
4
+ Provides decorators for UI functions including sync/async wrapper patterns.
5
+ """
6
+
7
+ import asyncio
8
+ from functools import wraps
9
+ from typing import Any, Callable, TypeVar
10
+
11
+ F = TypeVar("F", bound=Callable[..., Any])
12
+
13
+
14
+ def create_sync_wrapper(async_func: F) -> F:
15
+ """Create a synchronous wrapper for an async function.
16
+
17
+ This decorator does NOT modify the original async function.
18
+ Instead, it attaches a sync version as a 'sync' attribute.
19
+
20
+ Args:
21
+ async_func: The async function to wrap
22
+
23
+ Returns:
24
+ The original async function with sync version attached
25
+ """
26
+
27
+ @wraps(async_func)
28
+ def sync_wrapper(*args, **kwargs):
29
+ try:
30
+ loop = asyncio.get_event_loop()
31
+ if loop.is_running():
32
+ # If we're already in an async context, we can't use run_until_complete
33
+ # This might happen when called from within an async function
34
+ raise RuntimeError(
35
+ f"Cannot call sync_{async_func.__name__} from within an async context. "
36
+ f"Use await {async_func.__name__}() instead."
37
+ )
38
+ except RuntimeError:
39
+ # No event loop exists, create one
40
+ loop = asyncio.new_event_loop()
41
+ asyncio.set_event_loop(loop)
42
+
43
+ return loop.run_until_complete(async_func(*args, **kwargs))
44
+
45
+ # Set a naming convention
46
+ sync_wrapper.__name__ = f"sync_{async_func.__name__}"
47
+ sync_wrapper.__qualname__ = f"sync_{async_func.__qualname__}"
48
+
49
+ # Update docstring to indicate this is a sync version
50
+ if async_func.__doc__:
51
+ sync_wrapper.__doc__ = (
52
+ f"Synchronous version of {async_func.__name__}.\n\n{async_func.__doc__}"
53
+ )
54
+
55
+ # Attach the sync version as an attribute
56
+ async_func.sync = sync_wrapper
57
+
58
+ # Return the original async function
59
+ return async_func
tunacode/ui/input.py ADDED
@@ -0,0 +1,95 @@
1
+ """User input handling functions for Sidekick UI."""
2
+
3
+ from typing import Optional
4
+
5
+ from prompt_toolkit.formatted_text import HTML
6
+ from prompt_toolkit.key_binding import KeyBindings
7
+ from prompt_toolkit.styles import Style
8
+ from prompt_toolkit.validation import Validator
9
+
10
+ from tunacode.constants import UI_COLORS, UI_PROMPT_PREFIX
11
+ from tunacode.core.state import StateManager
12
+
13
+ from .completers import create_completer
14
+ from .keybindings import create_key_bindings
15
+ from .lexers import FileReferenceLexer
16
+ from .prompt_manager import PromptConfig, PromptManager
17
+
18
+
19
+ def formatted_text(text: str) -> HTML:
20
+ """Create formatted HTML text."""
21
+ return HTML(text)
22
+
23
+
24
+ async def input(
25
+ session_key: str,
26
+ pretext: str = UI_PROMPT_PREFIX,
27
+ is_password: bool = False,
28
+ validator: Optional[Validator] = None,
29
+ multiline: bool = False,
30
+ key_bindings: Optional[KeyBindings] = None,
31
+ placeholder: Optional[HTML] = None,
32
+ completer=None,
33
+ lexer=None,
34
+ timeoutlen: float = 0.05,
35
+ state_manager: Optional[StateManager] = None,
36
+ ) -> str:
37
+ """
38
+ Prompt for user input using simplified prompt management.
39
+
40
+ Args:
41
+ session_key: The session key for the prompt
42
+ pretext: The text to display before the input prompt
43
+ is_password: Whether to mask the input
44
+ validator: Optional input validator
45
+ multiline: Whether to allow multiline input
46
+ key_bindings: Optional custom key bindings
47
+ placeholder: Optional placeholder text
48
+ completer: Optional completer for tab completion
49
+ lexer: Optional lexer for syntax highlighting
50
+ timeoutlen: Timeout length for input
51
+ state_manager: The state manager for session storage
52
+
53
+ Returns:
54
+ User input string
55
+ """
56
+ # Create prompt configuration
57
+ config = PromptConfig(
58
+ multiline=multiline,
59
+ is_password=is_password,
60
+ validator=validator,
61
+ key_bindings=key_bindings,
62
+ placeholder=placeholder,
63
+ completer=completer,
64
+ lexer=lexer,
65
+ timeoutlen=timeoutlen,
66
+ )
67
+
68
+ # Create prompt manager
69
+ manager = PromptManager(state_manager)
70
+
71
+ # Get user input
72
+ return await manager.get_input(session_key, pretext, config)
73
+
74
+
75
+ async def multiline_input(state_manager: Optional[StateManager] = None, command_registry=None) -> str:
76
+ """Get multiline input from the user with @file completion and highlighting."""
77
+ kb = create_key_bindings()
78
+ placeholder = formatted_text(
79
+ (
80
+ "<darkgrey>"
81
+ "<bold>Enter</bold> to submit, "
82
+ "<bold>Esc + Enter</bold> for new line, "
83
+ "<bold>/help</bold> for commands"
84
+ "</darkgrey>"
85
+ )
86
+ )
87
+ return await input(
88
+ "multiline",
89
+ key_bindings=kb,
90
+ multiline=True,
91
+ placeholder=placeholder,
92
+ completer=create_completer(command_registry),
93
+ lexer=FileReferenceLexer(),
94
+ state_manager=state_manager
95
+ )
@@ -0,0 +1,27 @@
1
+ """Key binding handlers for Sidekick UI."""
2
+
3
+ from prompt_toolkit.key_binding import KeyBindings
4
+
5
+
6
+ def create_key_bindings() -> KeyBindings:
7
+ """Create and configure key bindings for the UI."""
8
+ kb = KeyBindings()
9
+
10
+
11
+
12
+ @kb.add("enter")
13
+ def _submit(event):
14
+ """Submit the current buffer."""
15
+ event.current_buffer.validate_and_handle()
16
+
17
+ @kb.add("c-o") # ctrl+o
18
+ def _newline(event):
19
+ """Insert a newline character."""
20
+ event.current_buffer.insert_text("\n")
21
+
22
+ @kb.add("escape", "enter")
23
+ def _escape_enter(event):
24
+ """Insert a newline when escape then enter is pressed."""
25
+ event.current_buffer.insert_text("\n")
26
+
27
+ return kb
tunacode/ui/lexers.py ADDED
@@ -0,0 +1,46 @@
1
+ """Custom lexers for syntax highlighting in the CLI."""
2
+
3
+ import re
4
+
5
+ from prompt_toolkit.formatted_text import FormattedText
6
+ from prompt_toolkit.lexers import Lexer
7
+
8
+
9
+ class FileReferenceLexer(Lexer):
10
+ """Lexer that highlights @file references in light blue."""
11
+
12
+ # Pattern to match @file references
13
+ FILE_REF_PATTERN = re.compile(r'@([\w./_-]+)')
14
+
15
+ def lex_document(self, document):
16
+ """Return a formatted text list for the given document."""
17
+ lines = document.text.split('\n')
18
+
19
+ def get_line_tokens(line_number):
20
+ """Get tokens for a specific line."""
21
+ if line_number >= len(lines):
22
+ return []
23
+
24
+ line = lines[line_number]
25
+ tokens = []
26
+ last_end = 0
27
+
28
+ # Find all @file references in the line
29
+ for match in self.FILE_REF_PATTERN.finditer(line):
30
+ start, end = match.span()
31
+
32
+ # Add text before the match
33
+ if start > last_end:
34
+ tokens.append(('', line[last_end:start]))
35
+
36
+ # Add the @file reference with styling
37
+ tokens.append(('class:file-reference', match.group(0)))
38
+ last_end = end
39
+
40
+ # Add remaining text
41
+ if last_end < len(line):
42
+ tokens.append(('', line[last_end:]))
43
+
44
+ return tokens
45
+
46
+ return get_line_tokens
tunacode/ui/output.py ADDED
@@ -0,0 +1,109 @@
1
+ """Output and display functions for Sidekick UI."""
2
+
3
+ from prompt_toolkit.application import run_in_terminal
4
+ from rich.console import Console
5
+ from rich.padding import Padding
6
+
7
+ from tunacode.configuration.settings import ApplicationSettings
8
+ from tunacode.constants import (MSG_UPDATE_AVAILABLE, MSG_UPDATE_INSTRUCTION, MSG_VERSION_DISPLAY,
9
+ UI_COLORS, UI_THINKING_MESSAGE)
10
+ from tunacode.core.state import StateManager
11
+ from tunacode.utils.file_utils import DotDict
12
+
13
+ from .constants import SPINNER_TYPE
14
+ from .decorators import create_sync_wrapper
15
+
16
+ console = Console()
17
+ colors = DotDict(UI_COLORS)
18
+
19
+ BANNER = """[bold #00d7ff]┌─────────────────────────────────────────────────────────────────┐[/bold #00d7ff]
20
+ [bold #00d7ff]│[/bold #00d7ff] [bold white]T U N A C O D E[/bold white] [dim #64748b]• Agentic AI Development Environment[/dim #64748b] [bold #00d7ff]│[/bold #00d7ff]
21
+ [bold #00d7ff]└─────────────────────────────────────────────────────────────────┘[/bold #00d7ff]"""
22
+
23
+
24
+ @create_sync_wrapper
25
+ async def print(message, **kwargs) -> None:
26
+ """Print a message to the console."""
27
+ await run_in_terminal(lambda: console.print(message, **kwargs))
28
+
29
+
30
+ async def line() -> None:
31
+ """Print a line to the console."""
32
+ await run_in_terminal(lambda: console.line())
33
+
34
+
35
+ async def info(text: str) -> None:
36
+ """Print an informational message."""
37
+ await print(f"[{colors.primary}]●[/{colors.primary}] {text}", style=colors.muted)
38
+
39
+
40
+ async def success(message: str) -> None:
41
+ """Print a success message."""
42
+ await print(f"[{colors.success}]✓[/{colors.success}] {message}")
43
+
44
+
45
+ async def warning(text: str) -> None:
46
+ """Print a warning message."""
47
+ await print(f"[{colors.warning}]⚠[/{colors.warning}] {text}")
48
+
49
+
50
+ async def muted(text: str, spaces: int = 0) -> None:
51
+ """Print a muted message."""
52
+ await print(f"{' ' * spaces}[{colors.muted}]•[/{colors.muted}] [dim]{text}[/dim]")
53
+
54
+
55
+ async def usage(usage: str) -> None:
56
+ """Print usage information."""
57
+ await print(Padding(usage, (0, 0, 1, 2)), style=colors.muted)
58
+
59
+
60
+ async def version() -> None:
61
+ """Print version information."""
62
+ app_settings = ApplicationSettings()
63
+ await info(MSG_VERSION_DISPLAY.format(version=app_settings.version))
64
+
65
+
66
+ async def banner() -> None:
67
+ """Display the application banner."""
68
+ console.clear()
69
+ banner_padding = Padding(BANNER, (2, 0, 1, 0))
70
+ await print(banner_padding)
71
+
72
+
73
+ async def clear() -> None:
74
+ """Clear the console and display the banner."""
75
+ console.clear()
76
+ await banner()
77
+
78
+
79
+ async def update_available(latest_version: str) -> None:
80
+ """Display update available notification."""
81
+ await warning(MSG_UPDATE_AVAILABLE.format(latest_version=latest_version))
82
+ await muted(MSG_UPDATE_INSTRUCTION)
83
+
84
+
85
+ async def spinner(show: bool = True, spinner_obj=None, state_manager: StateManager = None):
86
+ """Manage a spinner display."""
87
+ icon = SPINNER_TYPE
88
+ message = UI_THINKING_MESSAGE
89
+
90
+ # Get spinner from state manager if available
91
+ if spinner_obj is None and state_manager:
92
+ spinner_obj = state_manager.session.spinner
93
+
94
+ if not spinner_obj:
95
+ spinner_obj = await run_in_terminal(lambda: console.status(message, spinner=icon))
96
+ # Store it back in state manager if available
97
+ if state_manager:
98
+ state_manager.session.spinner = spinner_obj
99
+
100
+ if show:
101
+ spinner_obj.start()
102
+ else:
103
+ spinner_obj.stop()
104
+
105
+ return spinner_obj
106
+
107
+
108
+ # Auto-generated sync version
109
+ sync_print = print.sync # type: ignore
tunacode/ui/panels.py ADDED
@@ -0,0 +1,156 @@
1
+ """Panel display functions for Sidekick UI."""
2
+
3
+ from typing import Any, Optional, Union
4
+
5
+ from rich.markdown import Markdown
6
+ from rich.padding import Padding
7
+ from rich.panel import Panel
8
+ from rich.pretty import Pretty
9
+ from rich.table import Table
10
+
11
+ from tunacode.configuration.models import ModelRegistry
12
+ from tunacode.constants import (APP_NAME, CMD_CLEAR, CMD_COMPACT, CMD_DUMP, CMD_EXIT, CMD_HELP,
13
+ CMD_MODEL, CMD_UNDO, CMD_YOLO, DESC_CLEAR, DESC_COMPACT, DESC_DUMP,
14
+ DESC_EXIT, DESC_HELP, DESC_MODEL, DESC_MODEL_DEFAULT,
15
+ DESC_MODEL_SWITCH, DESC_UNDO, DESC_YOLO, PANEL_AVAILABLE_COMMANDS,
16
+ PANEL_ERROR, PANEL_MESSAGE_HISTORY, PANEL_MODELS, UI_COLORS)
17
+ from tunacode.core.state import StateManager
18
+ from tunacode.utils.file_utils import DotDict
19
+
20
+ from .constants import DEFAULT_PANEL_PADDING
21
+ from .decorators import create_sync_wrapper
22
+ from .output import print
23
+
24
+ colors = DotDict(UI_COLORS)
25
+
26
+
27
+ @create_sync_wrapper
28
+ async def panel(
29
+ title: str,
30
+ text: Union[str, Markdown, Pretty],
31
+ top: int = DEFAULT_PANEL_PADDING["top"],
32
+ right: int = DEFAULT_PANEL_PADDING["right"],
33
+ bottom: int = DEFAULT_PANEL_PADDING["bottom"],
34
+ left: int = DEFAULT_PANEL_PADDING["left"],
35
+ border_style: Optional[str] = None,
36
+ **kwargs: Any,
37
+ ) -> None:
38
+ """Display a rich panel with modern styling."""
39
+ border_style = border_style or kwargs.get("style") or colors.border
40
+ panel_obj = Panel(
41
+ Padding(text, (0, 1, 0, 1)),
42
+ title=f"[bold]{title}[/bold]",
43
+ title_align="left",
44
+ border_style=border_style,
45
+ padding=(0, 1)
46
+ )
47
+ await print(Padding(panel_obj, (top, right, bottom, left)), **kwargs)
48
+
49
+
50
+ async def agent(text: str, bottom: int = 1) -> None:
51
+ """Display an agent panel with modern styling."""
52
+ title = f"[bold {colors.primary}]●[/bold {colors.primary}] {APP_NAME}"
53
+ await panel(title, Markdown(text), bottom=bottom, border_style=colors.primary)
54
+
55
+
56
+ async def error(text: str) -> None:
57
+ """Display an error panel."""
58
+ await panel(PANEL_ERROR, text, style=colors.error)
59
+
60
+
61
+ async def dump_messages(messages_list=None, state_manager: StateManager = None) -> None:
62
+ """Display message history panel."""
63
+ if messages_list is None and state_manager:
64
+ # Get messages from state manager
65
+ messages = Pretty(state_manager.session.messages)
66
+ elif messages_list is not None:
67
+ messages = Pretty(messages_list)
68
+ else:
69
+ # No messages available
70
+ messages = Pretty([])
71
+ await panel(PANEL_MESSAGE_HISTORY, messages, style=colors.muted)
72
+
73
+
74
+ async def models(state_manager: StateManager = None) -> None:
75
+ """Display available models panel."""
76
+ model_registry = ModelRegistry()
77
+ model_ids = list(model_registry.list_models().keys())
78
+ model_list = "\n".join([f"{index} - {model}" for index, model in enumerate(model_ids)])
79
+ current_model = state_manager.session.current_model if state_manager else "unknown"
80
+ text = f"Current model: {current_model}\n\n{model_list}"
81
+ await panel(PANEL_MODELS, text, border_style=colors.muted)
82
+
83
+
84
+ async def help(command_registry=None) -> None:
85
+ """Display the available commands organized by category."""
86
+ table = Table(show_header=False, box=None, padding=(0, 3, 0, 0))
87
+ table.add_column("Command", style=f"bold {colors.primary}", justify="right", min_width=16)
88
+ table.add_column("Description", style=colors.muted)
89
+
90
+ if command_registry:
91
+ # Use the new command registry to display commands by category
92
+ from ..cli.commands import CommandCategory
93
+
94
+ category_order = [
95
+ CommandCategory.SYSTEM,
96
+ CommandCategory.NAVIGATION,
97
+ CommandCategory.DEVELOPMENT,
98
+ CommandCategory.MODEL,
99
+ CommandCategory.DEBUG,
100
+ ]
101
+
102
+ for category in category_order:
103
+ commands = command_registry.get_commands_by_category(category)
104
+ if commands:
105
+ # Add category header
106
+ table.add_row("", "")
107
+ table.add_row(f"[bold]{category.value.title()}[/bold]", "")
108
+
109
+ # Add commands in this category
110
+ for command in commands:
111
+ # Show primary command name
112
+ cmd_display = f"/{command.name}"
113
+ table.add_row(cmd_display, command.description)
114
+
115
+ # Special handling for model command variations
116
+ if command.name == "model":
117
+ table.add_row(f"{cmd_display} <n>", DESC_MODEL_SWITCH)
118
+ table.add_row(f"{cmd_display} <n> default", DESC_MODEL_DEFAULT)
119
+
120
+ # Add built-in commands
121
+ table.add_row("", "")
122
+ table.add_row("[bold]Built-in[/bold]", "")
123
+ table.add_row(CMD_EXIT, DESC_EXIT)
124
+ else:
125
+ # Fallback to static command list
126
+ commands = [
127
+ (CMD_HELP, DESC_HELP),
128
+ (CMD_CLEAR, DESC_CLEAR),
129
+ (CMD_DUMP, DESC_DUMP),
130
+ (CMD_YOLO, DESC_YOLO),
131
+ (CMD_UNDO, DESC_UNDO),
132
+ (CMD_COMPACT, DESC_COMPACT),
133
+ (CMD_MODEL, DESC_MODEL),
134
+ (f"{CMD_MODEL} <n>", DESC_MODEL_SWITCH),
135
+ (f"{CMD_MODEL} <n> default", DESC_MODEL_DEFAULT),
136
+ (CMD_EXIT, DESC_EXIT),
137
+ ]
138
+
139
+ for cmd, desc in commands:
140
+ table.add_row(cmd, desc)
141
+
142
+ await panel(PANEL_AVAILABLE_COMMANDS, table, border_style=colors.muted)
143
+
144
+
145
+ @create_sync_wrapper
146
+ async def tool_confirm(
147
+ title: str, content: Union[str, Markdown], filepath: Optional[str] = None
148
+ ) -> None:
149
+ """Display a tool confirmation panel."""
150
+ bottom_padding = 0 if filepath else 1
151
+ await panel(title, content, bottom=bottom_padding, border_style=colors.warning)
152
+
153
+
154
+ # Auto-generated sync versions
155
+ sync_panel = panel.sync # type: ignore
156
+ sync_tool_confirm = tool_confirm.sync # type: ignore