hanzo 0.3.24__py3-none-any.whl → 0.3.26__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 hanzo might be problematic. Click here for more details.
- hanzo/__init__.py +2 -2
- hanzo/cli.py +13 -5
- hanzo/commands/auth.py +206 -266
- hanzo/commands/auth_broken.py +377 -0
- hanzo/commands/chat.py +3 -0
- hanzo/interactive/enhanced_repl.py +688 -0
- hanzo/interactive/model_selector.py +166 -0
- hanzo/interactive/repl.py +2 -2
- hanzo/tools/__init__.py +5 -0
- hanzo/tools/detector.py +291 -0
- hanzo/ui/__init__.py +13 -0
- hanzo/ui/inline_startup.py +136 -0
- hanzo/ui/startup.py +350 -0
- {hanzo-0.3.24.dist-info → hanzo-0.3.26.dist-info}/METADATA +1 -1
- {hanzo-0.3.24.dist-info → hanzo-0.3.26.dist-info}/RECORD +17 -9
- {hanzo-0.3.24.dist-info → hanzo-0.3.26.dist-info}/WHEEL +0 -0
- {hanzo-0.3.24.dist-info → hanzo-0.3.26.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Quick model selector with arrow key navigation."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
from prompt_toolkit import Application
|
|
5
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
6
|
+
from prompt_toolkit.layout.containers import HSplit, Window
|
|
7
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
8
|
+
from prompt_toolkit.layout.layout import Layout
|
|
9
|
+
from prompt_toolkit.widgets import Label
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class QuickModelSelector:
|
|
14
|
+
"""Quick model selector with arrow navigation."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, models: List[Tuple[str, str]], tools: List[Tuple[str, str]], current: str):
|
|
17
|
+
self.models = models
|
|
18
|
+
self.tools = tools
|
|
19
|
+
self.current = current
|
|
20
|
+
self.all_items = tools + models # Tools first, then models
|
|
21
|
+
self.selected_index = 0
|
|
22
|
+
|
|
23
|
+
# Find current selection
|
|
24
|
+
for i, (item_id, _) in enumerate(self.all_items):
|
|
25
|
+
if item_id == current:
|
|
26
|
+
self.selected_index = i
|
|
27
|
+
break
|
|
28
|
+
|
|
29
|
+
def get_display_lines(self) -> List[str]:
|
|
30
|
+
"""Get display lines for the selector."""
|
|
31
|
+
lines = []
|
|
32
|
+
|
|
33
|
+
if self.tools:
|
|
34
|
+
lines.append("AI Coding Assistants:")
|
|
35
|
+
for i, (tool_id, tool_name) in enumerate(self.tools):
|
|
36
|
+
marker = "→ " if i == self.selected_index else " "
|
|
37
|
+
lines.append(f"{marker}{tool_name}")
|
|
38
|
+
|
|
39
|
+
if self.models:
|
|
40
|
+
if self.tools:
|
|
41
|
+
lines.append("") # Empty line
|
|
42
|
+
lines.append("Language Models:")
|
|
43
|
+
|
|
44
|
+
tool_count = len(self.tools)
|
|
45
|
+
for i, (model_id, model_name) in enumerate(self.models):
|
|
46
|
+
actual_idx = tool_count + i
|
|
47
|
+
marker = "→ " if actual_idx == self.selected_index else " "
|
|
48
|
+
lines.append(f"{marker}{model_name}")
|
|
49
|
+
|
|
50
|
+
return lines
|
|
51
|
+
|
|
52
|
+
def move_up(self):
|
|
53
|
+
"""Move selection up."""
|
|
54
|
+
if self.selected_index > 0:
|
|
55
|
+
self.selected_index -= 1
|
|
56
|
+
|
|
57
|
+
def move_down(self):
|
|
58
|
+
"""Move selection down."""
|
|
59
|
+
if self.selected_index < len(self.all_items) - 1:
|
|
60
|
+
self.selected_index += 1
|
|
61
|
+
|
|
62
|
+
def get_selected(self) -> Tuple[str, str]:
|
|
63
|
+
"""Get the selected item."""
|
|
64
|
+
if 0 <= self.selected_index < len(self.all_items):
|
|
65
|
+
return self.all_items[self.selected_index]
|
|
66
|
+
return None, None
|
|
67
|
+
|
|
68
|
+
async def run(self) -> Optional[str]:
|
|
69
|
+
"""Run the selector and return selected model/tool ID."""
|
|
70
|
+
kb = KeyBindings()
|
|
71
|
+
|
|
72
|
+
@kb.add('up')
|
|
73
|
+
def _(event):
|
|
74
|
+
self.move_up()
|
|
75
|
+
event.app.invalidate()
|
|
76
|
+
|
|
77
|
+
@kb.add('down')
|
|
78
|
+
def _(event):
|
|
79
|
+
self.move_down()
|
|
80
|
+
event.app.invalidate()
|
|
81
|
+
|
|
82
|
+
@kb.add('enter')
|
|
83
|
+
def _(event):
|
|
84
|
+
event.app.exit(result=self.get_selected()[0])
|
|
85
|
+
|
|
86
|
+
@kb.add('c-c')
|
|
87
|
+
@kb.add('escape')
|
|
88
|
+
def _(event):
|
|
89
|
+
event.app.exit(result=None)
|
|
90
|
+
|
|
91
|
+
def get_text():
|
|
92
|
+
lines = self.get_display_lines()
|
|
93
|
+
lines.append("")
|
|
94
|
+
lines.append("↑/↓: Navigate Enter: Select Esc: Cancel")
|
|
95
|
+
return "\n".join(lines)
|
|
96
|
+
|
|
97
|
+
layout = Layout(
|
|
98
|
+
Window(
|
|
99
|
+
FormattedTextControl(get_text),
|
|
100
|
+
wrap_lines=False
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
app = Application(
|
|
105
|
+
layout=layout,
|
|
106
|
+
key_bindings=kb,
|
|
107
|
+
full_screen=False,
|
|
108
|
+
mouse_support=True
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return await app.run_async()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class BackgroundTaskManager:
|
|
115
|
+
"""Manage background tasks."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, console: Optional[Console] = None):
|
|
118
|
+
self.console = console or Console()
|
|
119
|
+
self.tasks = {} # task_id -> process
|
|
120
|
+
self.next_id = 1
|
|
121
|
+
|
|
122
|
+
def add_task(self, name: str, process):
|
|
123
|
+
"""Add a background task."""
|
|
124
|
+
task_id = f"task_{self.next_id}"
|
|
125
|
+
self.next_id += 1
|
|
126
|
+
self.tasks[task_id] = {
|
|
127
|
+
"name": name,
|
|
128
|
+
"process": process,
|
|
129
|
+
"started": True
|
|
130
|
+
}
|
|
131
|
+
return task_id
|
|
132
|
+
|
|
133
|
+
def list_tasks(self):
|
|
134
|
+
"""List all background tasks."""
|
|
135
|
+
if not self.tasks:
|
|
136
|
+
self.console.print("[dim]No background tasks running[/dim]")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self.console.print("[bold]Background Tasks:[/bold]")
|
|
140
|
+
for task_id, task in self.tasks.items():
|
|
141
|
+
status = "🟢 Running" if task["process"].poll() is None else "🔴 Stopped"
|
|
142
|
+
self.console.print(f" {task_id}: {task['name']} - {status}")
|
|
143
|
+
|
|
144
|
+
def kill_task(self, task_id: str):
|
|
145
|
+
"""Kill a background task."""
|
|
146
|
+
if task_id in self.tasks:
|
|
147
|
+
task = self.tasks[task_id]
|
|
148
|
+
if task["process"].poll() is None:
|
|
149
|
+
task["process"].terminate()
|
|
150
|
+
self.console.print(f"[yellow]Terminated {task_id}: {task['name']}[/yellow]")
|
|
151
|
+
else:
|
|
152
|
+
self.console.print(f"[dim]Task {task_id} already stopped[/dim]")
|
|
153
|
+
del self.tasks[task_id]
|
|
154
|
+
else:
|
|
155
|
+
self.console.print(f"[red]Task {task_id} not found[/red]")
|
|
156
|
+
|
|
157
|
+
def kill_all(self):
|
|
158
|
+
"""Kill all background tasks."""
|
|
159
|
+
if not self.tasks:
|
|
160
|
+
self.console.print("[dim]No tasks to kill[/dim]")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
for task_id in list(self.tasks.keys()):
|
|
164
|
+
self.kill_task(task_id)
|
|
165
|
+
|
|
166
|
+
self.console.print("[green]All tasks terminated[/green]")
|
hanzo/interactive/repl.py
CHANGED
hanzo/tools/__init__.py
ADDED
hanzo/tools/detector.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Detect available AI coding tools and assistants."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional, Tuple
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich import box
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class AITool:
|
|
17
|
+
"""Represents an AI coding tool."""
|
|
18
|
+
name: str
|
|
19
|
+
command: str
|
|
20
|
+
display_name: str
|
|
21
|
+
provider: str
|
|
22
|
+
priority: int # Lower is higher priority
|
|
23
|
+
check_command: Optional[str] = None
|
|
24
|
+
env_var: Optional[str] = None
|
|
25
|
+
api_endpoint: Optional[str] = None
|
|
26
|
+
detected: bool = False
|
|
27
|
+
version: Optional[str] = None
|
|
28
|
+
path: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ToolDetector:
|
|
32
|
+
"""Detect and manage available AI coding tools."""
|
|
33
|
+
|
|
34
|
+
# Define available tools with priority order
|
|
35
|
+
TOOLS = [
|
|
36
|
+
AITool(
|
|
37
|
+
name="claude-code",
|
|
38
|
+
command="claude",
|
|
39
|
+
display_name="Claude Code",
|
|
40
|
+
provider="anthropic",
|
|
41
|
+
priority=1,
|
|
42
|
+
check_command="claude --version",
|
|
43
|
+
env_var="ANTHROPIC_API_KEY"
|
|
44
|
+
),
|
|
45
|
+
AITool(
|
|
46
|
+
name="hanzo-dev",
|
|
47
|
+
command="hanzo dev",
|
|
48
|
+
display_name="Hanzo Dev (Native)",
|
|
49
|
+
provider="hanzo",
|
|
50
|
+
priority=2,
|
|
51
|
+
check_command="hanzo --version",
|
|
52
|
+
env_var="HANZO_API_KEY"
|
|
53
|
+
),
|
|
54
|
+
AITool(
|
|
55
|
+
name="openai-codex",
|
|
56
|
+
command="openai",
|
|
57
|
+
display_name="OpenAI Codex",
|
|
58
|
+
provider="openai",
|
|
59
|
+
priority=3,
|
|
60
|
+
check_command="openai --version",
|
|
61
|
+
env_var="OPENAI_API_KEY"
|
|
62
|
+
),
|
|
63
|
+
AITool(
|
|
64
|
+
name="gemini-cli",
|
|
65
|
+
command="gemini",
|
|
66
|
+
display_name="Gemini CLI",
|
|
67
|
+
provider="google",
|
|
68
|
+
priority=4,
|
|
69
|
+
check_command="gemini --version",
|
|
70
|
+
env_var="GEMINI_API_KEY"
|
|
71
|
+
),
|
|
72
|
+
AITool(
|
|
73
|
+
name="grok-cli",
|
|
74
|
+
command="grok",
|
|
75
|
+
display_name="Grok CLI",
|
|
76
|
+
provider="xai",
|
|
77
|
+
priority=5,
|
|
78
|
+
check_command="grok --version",
|
|
79
|
+
env_var="GROK_API_KEY"
|
|
80
|
+
),
|
|
81
|
+
AITool(
|
|
82
|
+
name="openhands",
|
|
83
|
+
command="openhands",
|
|
84
|
+
display_name="OpenHands CLI",
|
|
85
|
+
provider="openhands",
|
|
86
|
+
priority=6,
|
|
87
|
+
check_command="openhands --version",
|
|
88
|
+
env_var=None
|
|
89
|
+
),
|
|
90
|
+
AITool(
|
|
91
|
+
name="cursor",
|
|
92
|
+
command="cursor",
|
|
93
|
+
display_name="Cursor AI",
|
|
94
|
+
provider="cursor",
|
|
95
|
+
priority=7,
|
|
96
|
+
check_command="cursor --version",
|
|
97
|
+
env_var=None
|
|
98
|
+
),
|
|
99
|
+
AITool(
|
|
100
|
+
name="codeium",
|
|
101
|
+
command="codeium",
|
|
102
|
+
display_name="Codeium",
|
|
103
|
+
provider="codeium",
|
|
104
|
+
priority=8,
|
|
105
|
+
check_command="codeium --version",
|
|
106
|
+
env_var="CODEIUM_API_KEY"
|
|
107
|
+
),
|
|
108
|
+
AITool(
|
|
109
|
+
name="aider",
|
|
110
|
+
command="aider",
|
|
111
|
+
display_name="Aider",
|
|
112
|
+
provider="aider",
|
|
113
|
+
priority=9,
|
|
114
|
+
check_command="aider --version",
|
|
115
|
+
env_var=None
|
|
116
|
+
),
|
|
117
|
+
AITool(
|
|
118
|
+
name="continue",
|
|
119
|
+
command="continue",
|
|
120
|
+
display_name="Continue Dev",
|
|
121
|
+
provider="continue",
|
|
122
|
+
priority=10,
|
|
123
|
+
check_command="continue --version",
|
|
124
|
+
env_var=None
|
|
125
|
+
)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def __init__(self, console: Optional[Console] = None):
|
|
129
|
+
self.console = console or Console()
|
|
130
|
+
self.detected_tools: List[AITool] = []
|
|
131
|
+
|
|
132
|
+
def detect_all(self) -> List[AITool]:
|
|
133
|
+
"""Detect all available AI tools."""
|
|
134
|
+
self.detected_tools = []
|
|
135
|
+
|
|
136
|
+
for tool in self.TOOLS:
|
|
137
|
+
if self.detect_tool(tool):
|
|
138
|
+
self.detected_tools.append(tool)
|
|
139
|
+
|
|
140
|
+
# Sort by priority
|
|
141
|
+
self.detected_tools.sort(key=lambda t: t.priority)
|
|
142
|
+
return self.detected_tools
|
|
143
|
+
|
|
144
|
+
def detect_tool(self, tool: AITool) -> bool:
|
|
145
|
+
"""Detect if a specific tool is available."""
|
|
146
|
+
# Check if command exists
|
|
147
|
+
tool.path = shutil.which(tool.command.split()[0])
|
|
148
|
+
if tool.path:
|
|
149
|
+
tool.detected = True
|
|
150
|
+
|
|
151
|
+
# Try to get version
|
|
152
|
+
if tool.check_command:
|
|
153
|
+
try:
|
|
154
|
+
result = subprocess.run(
|
|
155
|
+
tool.check_command.split(),
|
|
156
|
+
capture_output=True,
|
|
157
|
+
text=True,
|
|
158
|
+
timeout=2
|
|
159
|
+
)
|
|
160
|
+
if result.returncode == 0:
|
|
161
|
+
tool.version = result.stdout.strip().split()[-1]
|
|
162
|
+
except:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
# Check environment variable as fallback
|
|
168
|
+
if tool.env_var and os.getenv(tool.env_var):
|
|
169
|
+
tool.detected = True
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def get_default_tool(self) -> Optional[AITool]:
|
|
175
|
+
"""Get the default tool based on priority and availability."""
|
|
176
|
+
if not self.detected_tools:
|
|
177
|
+
self.detect_all()
|
|
178
|
+
|
|
179
|
+
if self.detected_tools:
|
|
180
|
+
return self.detected_tools[0]
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def get_tool_by_name(self, name: str) -> Optional[AITool]:
|
|
184
|
+
"""Get a specific tool by name."""
|
|
185
|
+
for tool in self.TOOLS:
|
|
186
|
+
if tool.name == name or tool.display_name.lower() == name.lower():
|
|
187
|
+
if self.detect_tool(tool):
|
|
188
|
+
return tool
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def show_available_tools(self):
|
|
192
|
+
"""Display available tools in a table."""
|
|
193
|
+
self.detect_all()
|
|
194
|
+
|
|
195
|
+
table = Table(title="Available AI Coding Tools", box=box.ROUNDED)
|
|
196
|
+
table.add_column("#", style="dim")
|
|
197
|
+
table.add_column("Tool", style="cyan")
|
|
198
|
+
table.add_column("Provider", style="yellow")
|
|
199
|
+
table.add_column("Status", style="green")
|
|
200
|
+
table.add_column("Version", style="blue")
|
|
201
|
+
table.add_column("Priority", style="magenta")
|
|
202
|
+
|
|
203
|
+
for i, tool in enumerate(self.TOOLS, 1):
|
|
204
|
+
status = "✅ Available" if tool.detected else "❌ Not Found"
|
|
205
|
+
version = tool.version or "Unknown" if tool.detected else "-"
|
|
206
|
+
|
|
207
|
+
# Highlight the default tool
|
|
208
|
+
if tool.detected and tool == self.detected_tools[0] if self.detected_tools else False:
|
|
209
|
+
table.add_row(
|
|
210
|
+
str(i),
|
|
211
|
+
f"[bold green]→ {tool.display_name}[/bold green]",
|
|
212
|
+
tool.provider,
|
|
213
|
+
status,
|
|
214
|
+
version,
|
|
215
|
+
str(tool.priority)
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
table.add_row(
|
|
219
|
+
str(i),
|
|
220
|
+
tool.display_name,
|
|
221
|
+
tool.provider,
|
|
222
|
+
status,
|
|
223
|
+
version,
|
|
224
|
+
str(tool.priority)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
self.console.print(table)
|
|
228
|
+
|
|
229
|
+
if self.detected_tools:
|
|
230
|
+
default = self.detected_tools[0]
|
|
231
|
+
self.console.print(f"\n[green]Default tool: {default.display_name}[/green]")
|
|
232
|
+
else:
|
|
233
|
+
self.console.print("\n[yellow]No AI coding tools detected.[/yellow]")
|
|
234
|
+
self.console.print("[dim]Install Claude Code, OpenAI CLI, or other tools to enable AI features.[/dim]")
|
|
235
|
+
|
|
236
|
+
def get_tool_command(self, tool: AITool, prompt: str) -> List[str]:
|
|
237
|
+
"""Get the command to execute for a tool with a prompt."""
|
|
238
|
+
if tool.name == "claude-code":
|
|
239
|
+
return ["claude", prompt]
|
|
240
|
+
elif tool.name == "hanzo-dev":
|
|
241
|
+
return ["hanzo", "dev", "--prompt", prompt]
|
|
242
|
+
elif tool.name == "openai-codex":
|
|
243
|
+
return ["openai", "api", "completions.create", "-m", "code-davinci-002", "-p", prompt]
|
|
244
|
+
elif tool.name == "gemini-cli":
|
|
245
|
+
return ["gemini", "generate", "--prompt", prompt]
|
|
246
|
+
elif tool.name == "grok-cli":
|
|
247
|
+
return ["grok", "complete", prompt]
|
|
248
|
+
elif tool.name == "openhands":
|
|
249
|
+
return ["openhands", "run", prompt]
|
|
250
|
+
elif tool.name == "cursor":
|
|
251
|
+
return ["cursor", "--prompt", prompt]
|
|
252
|
+
elif tool.name == "aider":
|
|
253
|
+
return ["aider", "--message", prompt]
|
|
254
|
+
else:
|
|
255
|
+
return [tool.command, prompt]
|
|
256
|
+
|
|
257
|
+
def execute_with_tool(self, tool: AITool, prompt: str) -> Tuple[bool, str]:
|
|
258
|
+
"""Execute a prompt with a specific tool."""
|
|
259
|
+
try:
|
|
260
|
+
command = self.get_tool_command(tool, prompt)
|
|
261
|
+
result = subprocess.run(
|
|
262
|
+
command,
|
|
263
|
+
capture_output=True,
|
|
264
|
+
text=True,
|
|
265
|
+
timeout=30
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if result.returncode == 0:
|
|
269
|
+
return True, result.stdout
|
|
270
|
+
else:
|
|
271
|
+
return False, result.stderr or "Command failed"
|
|
272
|
+
except subprocess.TimeoutExpired:
|
|
273
|
+
return False, "Command timed out"
|
|
274
|
+
except Exception as e:
|
|
275
|
+
return False, str(e)
|
|
276
|
+
|
|
277
|
+
def execute_with_fallback(self, prompt: str) -> Tuple[bool, str, AITool]:
|
|
278
|
+
"""Execute with fallback through available tools."""
|
|
279
|
+
if not self.detected_tools:
|
|
280
|
+
self.detect_all()
|
|
281
|
+
|
|
282
|
+
for tool in self.detected_tools:
|
|
283
|
+
self.console.print(f"[dim]Trying {tool.display_name}...[/dim]")
|
|
284
|
+
success, output = self.execute_with_tool(tool, prompt)
|
|
285
|
+
|
|
286
|
+
if success:
|
|
287
|
+
return True, output, tool
|
|
288
|
+
else:
|
|
289
|
+
self.console.print(f"[yellow]{tool.display_name} failed: {output}[/yellow]")
|
|
290
|
+
|
|
291
|
+
return False, "No available tools could handle the request", None
|
hanzo/ui/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inline startup notifications for Hanzo commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from rich import box
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InlineStartup:
|
|
19
|
+
"""Lightweight inline startup notifications."""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.config_dir = Path.home() / ".hanzo"
|
|
23
|
+
self.last_shown_file = self.config_dir / ".last_inline_shown"
|
|
24
|
+
self.show_interval = timedelta(hours=24) # Show once per day
|
|
25
|
+
|
|
26
|
+
def should_show(self) -> bool:
|
|
27
|
+
"""Check if we should show inline startup."""
|
|
28
|
+
# Check environment variable
|
|
29
|
+
if os.environ.get("HANZO_NO_STARTUP") == "1":
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Check last shown time
|
|
33
|
+
if self.last_shown_file.exists():
|
|
34
|
+
try:
|
|
35
|
+
last_shown = datetime.fromisoformat(
|
|
36
|
+
self.last_shown_file.read_text().strip()
|
|
37
|
+
)
|
|
38
|
+
if datetime.now() - last_shown < self.show_interval:
|
|
39
|
+
return False
|
|
40
|
+
except:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
def mark_shown(self):
|
|
46
|
+
"""Mark inline startup as shown."""
|
|
47
|
+
self.config_dir.mkdir(exist_ok=True)
|
|
48
|
+
self.last_shown_file.write_text(datetime.now().isoformat())
|
|
49
|
+
|
|
50
|
+
def show_mini(self, command: str = None):
|
|
51
|
+
"""Show mini inline startup."""
|
|
52
|
+
if not self.should_show():
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# Build message
|
|
56
|
+
message = Text()
|
|
57
|
+
message.append("✨ ", style="yellow")
|
|
58
|
+
message.append("Hanzo AI ", style="bold cyan")
|
|
59
|
+
message.append("v0.3.23", style="green")
|
|
60
|
+
|
|
61
|
+
# Add what's new teaser
|
|
62
|
+
message.append(" • ", style="dim")
|
|
63
|
+
message.append("What's new: ", style="dim")
|
|
64
|
+
message.append("Router management, improved docs", style="yellow dim")
|
|
65
|
+
|
|
66
|
+
# Show panel
|
|
67
|
+
console.print(
|
|
68
|
+
Panel(
|
|
69
|
+
message,
|
|
70
|
+
box=box.MINIMAL,
|
|
71
|
+
border_style="cyan",
|
|
72
|
+
padding=(0, 1)
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.mark_shown()
|
|
77
|
+
|
|
78
|
+
def show_command_hint(self, command: str):
|
|
79
|
+
"""Show command-specific hints."""
|
|
80
|
+
hints = {
|
|
81
|
+
"chat": "💡 Tip: Use --model to change AI model, --router for local proxy",
|
|
82
|
+
"node": "💡 Tip: Run 'hanzo node start' to enable local AI inference",
|
|
83
|
+
"router": "💡 Tip: Router provides unified access to 100+ LLM providers",
|
|
84
|
+
"repl": "💡 Tip: REPL combines Python with AI assistance",
|
|
85
|
+
"agent": "💡 Tip: Agents can work in parallel with 'hanzo agent swarm'"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
hint = hints.get(command)
|
|
89
|
+
if hint and os.environ.get("HANZO_SHOW_HINTS") != "0":
|
|
90
|
+
console.print(f"[dim]{hint}[/dim]")
|
|
91
|
+
|
|
92
|
+
def show_status_bar(self):
|
|
93
|
+
"""Show a compact status bar."""
|
|
94
|
+
items = []
|
|
95
|
+
|
|
96
|
+
# Check router
|
|
97
|
+
try:
|
|
98
|
+
import httpx
|
|
99
|
+
response = httpx.get("http://localhost:4000/health", timeout=0.5)
|
|
100
|
+
if response.status_code == 200:
|
|
101
|
+
items.append("[green]Router ✓[/green]")
|
|
102
|
+
except:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
# Check node
|
|
106
|
+
try:
|
|
107
|
+
import httpx
|
|
108
|
+
response = httpx.get("http://localhost:8000/health", timeout=0.5)
|
|
109
|
+
if response.status_code == 200:
|
|
110
|
+
items.append("[green]Node ✓[/green]")
|
|
111
|
+
except:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# Check API key
|
|
115
|
+
if os.environ.get("HANZO_API_KEY"):
|
|
116
|
+
items.append("[green]API ✓[/green]")
|
|
117
|
+
else:
|
|
118
|
+
items.append("[yellow]API ⚠[/yellow]")
|
|
119
|
+
|
|
120
|
+
if items:
|
|
121
|
+
status = " • ".join(items)
|
|
122
|
+
console.print(f"[dim]Status: {status}[/dim]")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def show_inline_startup(command: str = None):
|
|
126
|
+
"""Show inline startup notification."""
|
|
127
|
+
startup = InlineStartup()
|
|
128
|
+
startup.show_mini(command)
|
|
129
|
+
if command:
|
|
130
|
+
startup.show_command_hint(command)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def show_status():
|
|
134
|
+
"""Show compact status bar."""
|
|
135
|
+
startup = InlineStartup()
|
|
136
|
+
startup.show_status_bar()
|