code-puppy 0.0.302__py3-none-any.whl → 0.0.335__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/agents/base_agent.py +343 -35
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +898 -0
- code_puppy/command_line/add_model_menu.py +23 -1
- code_puppy/command_line/autosave_menu.py +271 -35
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +8 -2
- code_puppy/command_line/config_commands.py +82 -10
- code_puppy/command_line/core_commands.py +70 -7
- code_puppy/command_line/diff_menu.py +5 -0
- code_puppy/command_line/mcp/custom_server_form.py +4 -0
- code_puppy/command_line/mcp/edit_command.py +3 -1
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/install_command.py +8 -3
- code_puppy/command_line/mcp/install_menu.py +5 -1
- code_puppy/command_line/mcp/logs_command.py +173 -64
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +10 -4
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +3 -1
- code_puppy/command_line/mcp/status_command.py +2 -1
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +3 -1
- code_puppy/command_line/mcp/wizard_utils.py +10 -4
- code_puppy/command_line/model_settings_menu.py +58 -7
- code_puppy/command_line/motd.py +13 -7
- code_puppy/command_line/onboarding_slides.py +180 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/prompt_toolkit_completion.py +16 -2
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +106 -17
- code_puppy/http_utils.py +155 -196
- code_puppy/keymap.py +8 -0
- code_puppy/main.py +5 -828
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +61 -32
- code_puppy/mcp_/config_wizard.py +5 -1
- code_puppy/mcp_/managed_server.py +23 -3
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/messaging/__init__.py +20 -4
- code_puppy/messaging/bus.py +64 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/messages.py +16 -0
- code_puppy/messaging/renderers.py +21 -9
- code_puppy/messaging/rich_renderer.py +113 -67
- code_puppy/messaging/spinner/console_spinner.py +34 -0
- code_puppy/model_factory.py +271 -45
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +21 -7
- code_puppy/plugins/__init__.py +12 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +612 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +595 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/config.py +5 -1
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +5 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +30 -0
- code_puppy/plugins/claude_code_oauth/utils.py +1 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
- code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/terminal_utils.py +291 -0
- code_puppy/tools/agent_tools.py +34 -9
- code_puppy/tools/command_runner.py +344 -27
- code_puppy/tools/file_operations.py +33 -45
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +21 -7
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/RECORD +87 -64
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
code_puppy/command_line/motd.py
CHANGED
|
@@ -8,13 +8,19 @@ import os
|
|
|
8
8
|
from code_puppy.config import CONFIG_DIR
|
|
9
9
|
from code_puppy.messaging import emit_info
|
|
10
10
|
|
|
11
|
-
MOTD_VERSION = "
|
|
12
|
-
MOTD_MESSAGE = """
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
11
|
+
MOTD_VERSION = "2026-01-01"
|
|
12
|
+
MOTD_MESSAGE = """
|
|
13
|
+
# 🐶 Happy New Year! January 1st, 2026 🎉
|
|
14
|
+
Reminder that Code Puppy supports three different OAuth subscriptions:
|
|
15
|
+
|
|
16
|
+
### Claude Code - `/claude-code-auth`
|
|
17
|
+
- Opus / Haiku / Sonnet
|
|
18
|
+
|
|
19
|
+
### ChatGPT Pro/Plus - `/chatgpt-auth`
|
|
20
|
+
- gpt-5.2 and gpt-5.2 codex
|
|
21
|
+
|
|
22
|
+
### Google Antigravity - `/antigravity-auth`
|
|
23
|
+
- Gemini 3 Pro, Flash, and Anthropic models including Opus and Sonnet.
|
|
18
24
|
"""
|
|
19
25
|
MOTD_TRACK_FILE = os.path.join(CONFIG_DIR, "motd.txt")
|
|
20
26
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Slide content for the onboarding wizard.
|
|
2
|
+
|
|
3
|
+
🐶 Lean, mean, ADHD-friendly slides. 5 slides max!
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# Slide Data Constants
|
|
10
|
+
# ============================================================================
|
|
11
|
+
|
|
12
|
+
# Model subscription options
|
|
13
|
+
MODEL_OPTIONS: List[Tuple[str, str, str]] = [
|
|
14
|
+
("chatgpt", "ChatGPT Plus/Pro/Max", "OAuth login - no API key needed"),
|
|
15
|
+
("claude", "Claude Code Pro/Max", "OAuth login - no API key needed"),
|
|
16
|
+
("api_keys", "API Keys", "OpenAI, Anthropic, Google, etc."),
|
|
17
|
+
("openrouter", "OpenRouter", "Single key for 100+ models"),
|
|
18
|
+
("skip", "Skip for now", "Configure later with /set or /add_model"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# Navigation Footer (shown on ALL slides)
|
|
24
|
+
# ============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_nav_footer() -> str:
|
|
28
|
+
"""Navigation hints shown at bottom of every slide."""
|
|
29
|
+
return (
|
|
30
|
+
"\n[dim]─────────────────────────────────────[/dim]\n"
|
|
31
|
+
"[green]→/l[/green] Next "
|
|
32
|
+
"[green]←/h[/green] Back "
|
|
33
|
+
"[green]↑↓/jk[/green] Options "
|
|
34
|
+
"[green]Enter[/green] Select "
|
|
35
|
+
"[yellow]ESC[/yellow] Skip"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ============================================================================
|
|
40
|
+
# Gradient Banner
|
|
41
|
+
# ============================================================================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_gradient_banner() -> str:
|
|
45
|
+
"""Generate the gradient CODE PUPPY banner."""
|
|
46
|
+
try:
|
|
47
|
+
import pyfiglet
|
|
48
|
+
|
|
49
|
+
lines = pyfiglet.figlet_format("CODE PUPPY", font="ansi_shadow").split("\n")
|
|
50
|
+
colors = ["bright_blue", "bright_cyan", "bright_green"]
|
|
51
|
+
result = []
|
|
52
|
+
for i, line in enumerate(lines):
|
|
53
|
+
if line.strip():
|
|
54
|
+
color = colors[min(i // 2, len(colors) - 1)]
|
|
55
|
+
result.append(f"[{color}]{line}[/{color}]")
|
|
56
|
+
return "\n".join(result)
|
|
57
|
+
except ImportError:
|
|
58
|
+
return "[bold bright_cyan]═══ CODE PUPPY 🐶 ═══[/bold bright_cyan]"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# Slide Content (5 slides total)
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def slide_welcome() -> str:
|
|
67
|
+
"""Slide 1: Welcome - quick intro."""
|
|
68
|
+
content = get_gradient_banner()
|
|
69
|
+
content += "\n\n"
|
|
70
|
+
content += "[bold white]Welcome! 🐶[/bold white]\n\n"
|
|
71
|
+
content += "[cyan]Quick setup:[/cyan]\n"
|
|
72
|
+
content += " 1. Pick your model provider\n"
|
|
73
|
+
content += " 2. Optional: MCP servers\n"
|
|
74
|
+
content += " 3. Learn when to use which agent\n"
|
|
75
|
+
content += " 4. Start coding!\n\n"
|
|
76
|
+
content += "[dim]Takes ~1 minute. Let's go![/dim]"
|
|
77
|
+
content += get_nav_footer()
|
|
78
|
+
return content
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def slide_models(selected_option: int, options: List[Tuple[str, str]]) -> str:
|
|
82
|
+
"""Slide 2: Model selection."""
|
|
83
|
+
content = "[bold cyan]📦 Pick Your Models[/bold cyan]\n\n"
|
|
84
|
+
content += "[white]How do you want to access LLMs?[/white]\n\n"
|
|
85
|
+
|
|
86
|
+
for i, (_, label) in enumerate(options):
|
|
87
|
+
if i == selected_option:
|
|
88
|
+
content += f"[bold green]▶ {label}[/bold green]\n"
|
|
89
|
+
else:
|
|
90
|
+
content += f"[dim] {label}[/dim]\n"
|
|
91
|
+
|
|
92
|
+
content += "\n"
|
|
93
|
+
|
|
94
|
+
# Context based on selection
|
|
95
|
+
opt = options[selected_option][0] if options else None
|
|
96
|
+
if opt == "chatgpt":
|
|
97
|
+
content += "[yellow]💡 ChatGPT OAuth[/yellow]\n"
|
|
98
|
+
content += " Uses your existing subscription\n"
|
|
99
|
+
content += " GPT-5.2, GPT-5.2-codex\n"
|
|
100
|
+
elif opt == "claude":
|
|
101
|
+
content += "[yellow]💡 Claude OAuth[/yellow]\n"
|
|
102
|
+
content += " Uses your existing subscription\n"
|
|
103
|
+
content += " Opus/Sonnet/Haiku 4.5\n"
|
|
104
|
+
elif opt == "api_keys":
|
|
105
|
+
content += "[yellow]💡 API Keys[/yellow]\n"
|
|
106
|
+
content += " [cyan]/set OPENAI_API_KEY=sk-...[/cyan]\n"
|
|
107
|
+
content += " [cyan]/add_model[/cyan] to browse 1500+ models\n"
|
|
108
|
+
elif opt == "openrouter":
|
|
109
|
+
content += "[yellow]💡 OpenRouter[/yellow]\n"
|
|
110
|
+
content += " One API key, all providers\n"
|
|
111
|
+
content += " [cyan]/set OPENROUTER_API_KEY=...[/cyan]\n"
|
|
112
|
+
else:
|
|
113
|
+
content += "[dim]No worries! Use /set or /add_model later[/dim]\n"
|
|
114
|
+
|
|
115
|
+
content += get_nav_footer()
|
|
116
|
+
return content
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def slide_mcp() -> str:
|
|
120
|
+
"""Slide 3: MCP servers (optional power-ups)."""
|
|
121
|
+
content = "[bold cyan]🔌 MCP Servers (Optional)[/bold cyan]\n\n"
|
|
122
|
+
content += "[white]Supercharge with external tools![/white]\n\n"
|
|
123
|
+
content += "[green]Commands:[/green]\n"
|
|
124
|
+
content += " [cyan]/mcp install[/cyan] Browse catalog\n"
|
|
125
|
+
content += " [cyan]/mcp add[/cyan] Add custom server\n"
|
|
126
|
+
content += " [cyan]/mcp list[/cyan] See your servers\n\n"
|
|
127
|
+
content += "[yellow]🌟 Popular picks:[/yellow]\n"
|
|
128
|
+
content += " • GitHub integration\n"
|
|
129
|
+
content += " • Postgres/databases\n"
|
|
130
|
+
content += " • Slack, Linear, etc.\n\n"
|
|
131
|
+
content += "[dim]Skip this if you just want to code![/dim]"
|
|
132
|
+
content += get_nav_footer()
|
|
133
|
+
return content
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def slide_use_cases() -> str:
|
|
137
|
+
"""Slide 4: When to use which agent - THE IMPORTANT ONE."""
|
|
138
|
+
content = "[bold cyan]🎯 When to Use What[/bold cyan]\n\n"
|
|
139
|
+
|
|
140
|
+
content += "[bold yellow]🐶 Code Puppy (default)[/bold yellow]\n"
|
|
141
|
+
content += " [green]USE FOR:[/green] Direct coding tasks\n"
|
|
142
|
+
content += " • Fix this bug\n"
|
|
143
|
+
content += " • Add a feature to this file\n"
|
|
144
|
+
content += " • Refactor this function\n"
|
|
145
|
+
content += " • Write tests for X\n\n"
|
|
146
|
+
|
|
147
|
+
content += "[bold yellow]📋 Planning Agent[/bold yellow]\n"
|
|
148
|
+
content += " [green]USE FOR:[/green] Complex multi-step projects\n"
|
|
149
|
+
content += " • Build me a REST API with auth\n"
|
|
150
|
+
content += " • Create a CLI tool from scratch\n"
|
|
151
|
+
content += " • Refactor entire codebase\n"
|
|
152
|
+
content += " • Multi-file architectural changes\n\n"
|
|
153
|
+
|
|
154
|
+
content += "[cyan]Switch: /agent planning-agent[/cyan]\n"
|
|
155
|
+
content += "[dim]Planning breaks big tasks into steps,[/dim]\n"
|
|
156
|
+
content += "[dim]then delegates to specialists.[/dim]"
|
|
157
|
+
content += get_nav_footer()
|
|
158
|
+
return content
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def slide_done(trigger_oauth: str | None) -> str:
|
|
162
|
+
"""Slide 5: You're ready!"""
|
|
163
|
+
content = "[bold green]🎉 Ready to Roll![/bold green]\n\n"
|
|
164
|
+
content += "[bold cyan]Essential commands:[/bold cyan]\n"
|
|
165
|
+
content += " [cyan]/model[/cyan] Switch models\n"
|
|
166
|
+
content += " [cyan]/agent[/cyan] Switch agents\n"
|
|
167
|
+
content += " [cyan]/help[/cyan] All commands\n\n"
|
|
168
|
+
|
|
169
|
+
content += "[bold yellow]Pro tips:[/bold yellow]\n"
|
|
170
|
+
content += " • Be specific in prompts\n"
|
|
171
|
+
content += " • Use Planning Agent for big tasks\n"
|
|
172
|
+
content += " • @ for file path completion\n\n"
|
|
173
|
+
|
|
174
|
+
if trigger_oauth:
|
|
175
|
+
content += f"[bold cyan]→ {trigger_oauth.title()} OAuth next![/bold cyan]\n\n"
|
|
176
|
+
|
|
177
|
+
content += "[dim]Re-run anytime: [/dim][cyan]/tutorial[/cyan]\n"
|
|
178
|
+
content += "\n[bold yellow]Press Enter to start coding! 🐶[/bold yellow]"
|
|
179
|
+
content += get_nav_footer()
|
|
180
|
+
return content
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Interactive TUI onboarding wizard for first-time Code Puppy users.
|
|
2
|
+
|
|
3
|
+
🐶 Quick 5-slide tutorial. ADHD-friendly!
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from code_puppy.command_line.onboarding_wizard import (
|
|
7
|
+
run_onboarding_wizard,
|
|
8
|
+
reset_onboarding,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
result = await run_onboarding_wizard()
|
|
12
|
+
# result: "chatgpt", "claude", "completed", "skipped", or None
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from typing import List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
from prompt_toolkit import Application
|
|
22
|
+
from prompt_toolkit.formatted_text import ANSI
|
|
23
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
24
|
+
from prompt_toolkit.layout import Layout, Window
|
|
25
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
26
|
+
from prompt_toolkit.widgets import Frame
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
|
|
29
|
+
from code_puppy.config import CONFIG_DIR
|
|
30
|
+
|
|
31
|
+
from .onboarding_slides import (
|
|
32
|
+
MODEL_OPTIONS,
|
|
33
|
+
slide_done,
|
|
34
|
+
slide_mcp,
|
|
35
|
+
slide_models,
|
|
36
|
+
slide_use_cases,
|
|
37
|
+
slide_welcome,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ============================================================================
|
|
41
|
+
# State Tracking
|
|
42
|
+
# ============================================================================
|
|
43
|
+
|
|
44
|
+
ONBOARDING_COMPLETE_FILE = os.path.join(CONFIG_DIR, "onboarding_complete")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def has_completed_onboarding() -> bool:
|
|
48
|
+
"""Check if the user has already completed onboarding."""
|
|
49
|
+
return os.path.exists(ONBOARDING_COMPLETE_FILE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def mark_onboarding_complete() -> None:
|
|
53
|
+
"""Mark onboarding as complete."""
|
|
54
|
+
os.makedirs(os.path.dirname(ONBOARDING_COMPLETE_FILE), exist_ok=True)
|
|
55
|
+
with open(ONBOARDING_COMPLETE_FILE, "w") as f:
|
|
56
|
+
f.write("completed\n")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def should_show_onboarding() -> bool:
|
|
60
|
+
"""Determine if the onboarding wizard should be shown.
|
|
61
|
+
|
|
62
|
+
Returns False if:
|
|
63
|
+
- User has already completed onboarding
|
|
64
|
+
- CODE_PUPPY_SKIP_TUTORIAL env var is set to '1' or 'true'
|
|
65
|
+
"""
|
|
66
|
+
# Allow skipping tutorial via environment variable (useful for testing)
|
|
67
|
+
skip_env = os.environ.get("CODE_PUPPY_SKIP_TUTORIAL", "").lower()
|
|
68
|
+
if skip_env in ("1", "true", "yes"):
|
|
69
|
+
return False
|
|
70
|
+
return not has_completed_onboarding()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def reset_onboarding() -> None:
|
|
74
|
+
"""Reset onboarding state (for re-running with /tutorial)."""
|
|
75
|
+
if os.path.exists(ONBOARDING_COMPLETE_FILE):
|
|
76
|
+
os.remove(ONBOARDING_COMPLETE_FILE)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
# Onboarding Wizard Class
|
|
81
|
+
# ============================================================================
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OnboardingWizard:
|
|
85
|
+
"""5-slide interactive tutorial.
|
|
86
|
+
|
|
87
|
+
Slides:
|
|
88
|
+
0: Welcome
|
|
89
|
+
1: Model selection
|
|
90
|
+
2: MCP servers
|
|
91
|
+
3: Use cases (Planning vs Coding)
|
|
92
|
+
4: Done!
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
TOTAL_SLIDES = 5
|
|
96
|
+
|
|
97
|
+
def __init__(self):
|
|
98
|
+
"""Initialize wizard state."""
|
|
99
|
+
self.current_slide = 0
|
|
100
|
+
self.selected_option = 0
|
|
101
|
+
self.trigger_oauth: Optional[str] = None
|
|
102
|
+
self.model_choice: Optional[str] = None
|
|
103
|
+
self.result: Optional[str] = None
|
|
104
|
+
self._should_exit = False
|
|
105
|
+
|
|
106
|
+
def get_progress_indicator(self) -> str:
|
|
107
|
+
"""Progress dots: ● ○ ○ ○ ○"""
|
|
108
|
+
return " ".join(
|
|
109
|
+
"●" if i == self.current_slide else "○" for i in range(self.TOTAL_SLIDES)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def get_slide_content(self) -> str:
|
|
113
|
+
"""Get content for current slide."""
|
|
114
|
+
if self.current_slide == 0:
|
|
115
|
+
return slide_welcome()
|
|
116
|
+
elif self.current_slide == 1:
|
|
117
|
+
options = self.get_options_for_slide()
|
|
118
|
+
return slide_models(self.selected_option, options)
|
|
119
|
+
elif self.current_slide == 2:
|
|
120
|
+
return slide_mcp()
|
|
121
|
+
elif self.current_slide == 3:
|
|
122
|
+
return slide_use_cases()
|
|
123
|
+
else: # slide 4
|
|
124
|
+
return slide_done(self.trigger_oauth)
|
|
125
|
+
|
|
126
|
+
def get_options_for_slide(self) -> List[Tuple[str, str]]:
|
|
127
|
+
"""Get selectable options for current slide."""
|
|
128
|
+
if self.current_slide == 1: # Model selection
|
|
129
|
+
return [(opt[0], opt[1]) for opt in MODEL_OPTIONS]
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
def handle_option_select(self) -> None:
|
|
133
|
+
"""Handle option selection."""
|
|
134
|
+
if self.current_slide == 1: # Model selection
|
|
135
|
+
options = self.get_options_for_slide()
|
|
136
|
+
if 0 <= self.selected_option < len(options):
|
|
137
|
+
choice_id = options[self.selected_option][0]
|
|
138
|
+
self.model_choice = choice_id
|
|
139
|
+
if choice_id == "chatgpt":
|
|
140
|
+
self.trigger_oauth = "chatgpt"
|
|
141
|
+
elif choice_id == "claude":
|
|
142
|
+
self.trigger_oauth = "claude"
|
|
143
|
+
|
|
144
|
+
def next_slide(self) -> bool:
|
|
145
|
+
"""Move to next slide."""
|
|
146
|
+
if self.current_slide < self.TOTAL_SLIDES - 1:
|
|
147
|
+
self.current_slide += 1
|
|
148
|
+
self.selected_option = 0
|
|
149
|
+
return True
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def prev_slide(self) -> bool:
|
|
153
|
+
"""Move to previous slide."""
|
|
154
|
+
if self.current_slide > 0:
|
|
155
|
+
self.current_slide -= 1
|
|
156
|
+
self.selected_option = 0
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def next_option(self) -> None:
|
|
161
|
+
"""Move to next option."""
|
|
162
|
+
options = self.get_options_for_slide()
|
|
163
|
+
if options:
|
|
164
|
+
self.selected_option = (self.selected_option + 1) % len(options)
|
|
165
|
+
|
|
166
|
+
def prev_option(self) -> None:
|
|
167
|
+
"""Move to previous option."""
|
|
168
|
+
options = self.get_options_for_slide()
|
|
169
|
+
if options:
|
|
170
|
+
self.selected_option = (self.selected_option - 1) % len(options)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ============================================================================
|
|
174
|
+
# TUI Rendering
|
|
175
|
+
# ============================================================================
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _get_slide_panel_content(wizard: OnboardingWizard) -> ANSI:
|
|
179
|
+
"""Generate slide content for display."""
|
|
180
|
+
buffer = io.StringIO()
|
|
181
|
+
console = Console(
|
|
182
|
+
file=buffer,
|
|
183
|
+
force_terminal=True,
|
|
184
|
+
width=80,
|
|
185
|
+
legacy_windows=False,
|
|
186
|
+
color_system="truecolor",
|
|
187
|
+
no_color=False,
|
|
188
|
+
force_interactive=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Progress indicator
|
|
192
|
+
progress = wizard.get_progress_indicator()
|
|
193
|
+
console.print(f"[dim]{progress}[/dim]")
|
|
194
|
+
console.print(
|
|
195
|
+
f"[dim]Slide {wizard.current_slide + 1} of {wizard.TOTAL_SLIDES}[/dim]\n"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Slide content (includes nav footer)
|
|
199
|
+
console.print(wizard.get_slide_content())
|
|
200
|
+
|
|
201
|
+
return ANSI(buffer.getvalue())
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ============================================================================
|
|
205
|
+
# Main Entry Point
|
|
206
|
+
# ============================================================================
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def run_onboarding_wizard() -> Optional[str]:
|
|
210
|
+
"""Run the interactive tutorial.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
- "chatgpt" if user wants ChatGPT OAuth
|
|
214
|
+
- "claude" if user wants Claude OAuth
|
|
215
|
+
- "completed" if finished normally
|
|
216
|
+
- "skipped" if user pressed ESC
|
|
217
|
+
- None on error
|
|
218
|
+
"""
|
|
219
|
+
from code_puppy.tools.command_runner import set_awaiting_user_input
|
|
220
|
+
|
|
221
|
+
wizard = OnboardingWizard()
|
|
222
|
+
set_awaiting_user_input(True)
|
|
223
|
+
|
|
224
|
+
# Enter alternate screen buffer
|
|
225
|
+
sys.stdout.write("\033[?1049h") # Enter alternate buffer
|
|
226
|
+
sys.stdout.write("\033[2J\033[H") # Clear and home
|
|
227
|
+
sys.stdout.flush()
|
|
228
|
+
time.sleep(0.1)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
kb = KeyBindings()
|
|
232
|
+
|
|
233
|
+
@kb.add("right")
|
|
234
|
+
@kb.add("l")
|
|
235
|
+
def next_slide(event):
|
|
236
|
+
if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
|
|
237
|
+
wizard.result = "completed"
|
|
238
|
+
wizard._should_exit = True
|
|
239
|
+
event.app.exit()
|
|
240
|
+
else:
|
|
241
|
+
wizard.next_slide()
|
|
242
|
+
event.app.invalidate()
|
|
243
|
+
|
|
244
|
+
@kb.add("left")
|
|
245
|
+
@kb.add("h")
|
|
246
|
+
def prev_slide(event):
|
|
247
|
+
wizard.prev_slide()
|
|
248
|
+
event.app.invalidate()
|
|
249
|
+
|
|
250
|
+
@kb.add("down")
|
|
251
|
+
@kb.add("j")
|
|
252
|
+
def next_option(event):
|
|
253
|
+
wizard.next_option()
|
|
254
|
+
event.app.invalidate()
|
|
255
|
+
|
|
256
|
+
@kb.add("up")
|
|
257
|
+
@kb.add("k")
|
|
258
|
+
def prev_option(event):
|
|
259
|
+
wizard.prev_option()
|
|
260
|
+
event.app.invalidate()
|
|
261
|
+
|
|
262
|
+
@kb.add("enter")
|
|
263
|
+
def select_or_next(event):
|
|
264
|
+
options = wizard.get_options_for_slide()
|
|
265
|
+
if options:
|
|
266
|
+
wizard.handle_option_select()
|
|
267
|
+
|
|
268
|
+
if wizard.current_slide == wizard.TOTAL_SLIDES - 1:
|
|
269
|
+
wizard.result = "completed"
|
|
270
|
+
wizard._should_exit = True
|
|
271
|
+
event.app.exit()
|
|
272
|
+
else:
|
|
273
|
+
wizard.next_slide()
|
|
274
|
+
event.app.invalidate()
|
|
275
|
+
|
|
276
|
+
@kb.add("escape")
|
|
277
|
+
def skip_wizard(event):
|
|
278
|
+
wizard.result = "skipped"
|
|
279
|
+
wizard._should_exit = True
|
|
280
|
+
event.app.exit()
|
|
281
|
+
|
|
282
|
+
@kb.add("c-c")
|
|
283
|
+
def cancel_wizard(event):
|
|
284
|
+
wizard.result = "skipped"
|
|
285
|
+
wizard._should_exit = True
|
|
286
|
+
event.app.exit()
|
|
287
|
+
|
|
288
|
+
slide_panel = Window(
|
|
289
|
+
content=FormattedTextControl(lambda: _get_slide_panel_content(wizard))
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
root_container = Frame(slide_panel, title="🐶 Code Puppy Tutorial")
|
|
293
|
+
layout = Layout(root_container)
|
|
294
|
+
|
|
295
|
+
app = Application(
|
|
296
|
+
layout=layout,
|
|
297
|
+
key_bindings=kb,
|
|
298
|
+
full_screen=False,
|
|
299
|
+
mouse_support=False,
|
|
300
|
+
color_depth="DEPTH_24_BIT",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
sys.stdout.write("\033[2J\033[H")
|
|
304
|
+
sys.stdout.flush()
|
|
305
|
+
|
|
306
|
+
await app.run_async()
|
|
307
|
+
|
|
308
|
+
except KeyboardInterrupt:
|
|
309
|
+
wizard.result = "skipped"
|
|
310
|
+
except Exception:
|
|
311
|
+
wizard.result = None
|
|
312
|
+
finally:
|
|
313
|
+
set_awaiting_user_input(False)
|
|
314
|
+
sys.stdout.write("\033[?1049l")
|
|
315
|
+
sys.stdout.flush()
|
|
316
|
+
|
|
317
|
+
# Clear exit message
|
|
318
|
+
from code_puppy.messaging import emit_info
|
|
319
|
+
|
|
320
|
+
if wizard.result == "skipped":
|
|
321
|
+
emit_info("✓ Tutorial skipped")
|
|
322
|
+
elif wizard.result == "completed":
|
|
323
|
+
emit_info("✓ Tutorial completed! Welcome to Code Puppy! 🐶")
|
|
324
|
+
else:
|
|
325
|
+
emit_info("✓ Exited tutorial")
|
|
326
|
+
|
|
327
|
+
if wizard.result in ("completed", "skipped"):
|
|
328
|
+
mark_onboarding_complete()
|
|
329
|
+
|
|
330
|
+
if wizard.trigger_oauth:
|
|
331
|
+
return wizard.trigger_oauth
|
|
332
|
+
|
|
333
|
+
return wizard.result
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
async def run_onboarding_if_needed() -> Optional[str]:
|
|
337
|
+
"""Run tutorial if user hasn't seen it yet."""
|
|
338
|
+
if should_show_onboarding():
|
|
339
|
+
return await run_onboarding_wizard()
|
|
340
|
+
return None
|
|
@@ -582,12 +582,26 @@ async def get_input_with_combined_completion(
|
|
|
582
582
|
# Ctrl+X keybinding - exit with KeyboardInterrupt for shell command cancellation
|
|
583
583
|
@bindings.add(Keys.ControlX)
|
|
584
584
|
def _(event):
|
|
585
|
-
|
|
585
|
+
try:
|
|
586
|
+
event.app.exit(exception=KeyboardInterrupt)
|
|
587
|
+
except Exception:
|
|
588
|
+
# Ignore "Return value already set" errors when exit was already called
|
|
589
|
+
# This happens when user presses multiple exit keys in quick succession
|
|
590
|
+
pass
|
|
586
591
|
|
|
587
592
|
# Escape keybinding - exit with KeyboardInterrupt
|
|
588
593
|
@bindings.add(Keys.Escape)
|
|
589
594
|
def _(event):
|
|
590
|
-
|
|
595
|
+
try:
|
|
596
|
+
event.app.exit(exception=KeyboardInterrupt)
|
|
597
|
+
except Exception:
|
|
598
|
+
# Ignore "Return value already set" errors when exit was already called
|
|
599
|
+
pass
|
|
600
|
+
|
|
601
|
+
# NOTE: We intentionally do NOT override Ctrl+C here.
|
|
602
|
+
# prompt_toolkit's default Ctrl+C handler properly resets the terminal state on Windows.
|
|
603
|
+
# Overriding it with event.app.exit(exception=KeyboardInterrupt) can leave the terminal
|
|
604
|
+
# in a bad state where characters cannot be typed. Let prompt_toolkit handle Ctrl+C natively.
|
|
591
605
|
|
|
592
606
|
# Toggle multiline with Alt+M
|
|
593
607
|
@bindings.add(Keys.Escape, "m")
|
|
@@ -246,6 +246,8 @@ def handle_dump_context_command(command: str) -> bool:
|
|
|
246
246
|
)
|
|
247
247
|
def handle_load_context_command(command: str) -> bool:
|
|
248
248
|
"""Load message history from a file."""
|
|
249
|
+
from rich.text import Text
|
|
250
|
+
|
|
249
251
|
from code_puppy.agents.agent_manager import get_current_agent
|
|
250
252
|
from code_puppy.config import rotate_autosave_id
|
|
251
253
|
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
@@ -278,12 +280,17 @@ def handle_load_context_command(command: str) -> bool:
|
|
|
278
280
|
# Rotate autosave id to avoid overwriting any existing autosave
|
|
279
281
|
try:
|
|
280
282
|
new_id = rotate_autosave_id()
|
|
281
|
-
autosave_info =
|
|
283
|
+
autosave_info = Text.from_markup(
|
|
284
|
+
f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
|
|
285
|
+
)
|
|
282
286
|
except Exception:
|
|
283
|
-
autosave_info = ""
|
|
287
|
+
autosave_info = Text("")
|
|
284
288
|
|
|
285
|
-
|
|
289
|
+
# Build the success message with proper Text concatenation
|
|
290
|
+
success_msg = Text(
|
|
286
291
|
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
287
|
-
f"📁 From: {session_path}
|
|
292
|
+
f"📁 From: {session_path}"
|
|
288
293
|
)
|
|
294
|
+
success_msg.append_text(autosave_info)
|
|
295
|
+
emit_success(success_msg)
|
|
289
296
|
return True
|