bone-agent 1.4.0 → 2.0.0
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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
package/src/ui/setup_wizard.py
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
"""Interactive first-run setup wizard.
|
|
2
|
-
|
|
3
|
-
Guides new users through provider selection, API key entry,
|
|
4
|
-
Obsidian vault configuration, and optional settings using
|
|
5
|
-
prompt_toolkit prompts and rich console output.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
-
|
|
11
|
-
from prompt_toolkit import prompt as pt_prompt
|
|
12
|
-
from prompt_toolkit.formatted_text import FormattedText
|
|
13
|
-
from rich.console import Console
|
|
14
|
-
from rich.panel import Panel
|
|
15
|
-
from rich.text import Text
|
|
16
|
-
|
|
17
|
-
from llm import config as llm_config
|
|
18
|
-
from core.config_manager import ConfigManager
|
|
19
|
-
|
|
20
|
-
# Provider metadata for wizard display
|
|
21
|
-
WIZARD_PROVIDERS = [
|
|
22
|
-
("bone", "bone-agent (free tier — no key needed)", "api"),
|
|
23
|
-
("openai", "OpenAI", "api"),
|
|
24
|
-
("anthropic", "Anthropic (Claude)", "api"),
|
|
25
|
-
("gemini", "Google Gemini", "api"),
|
|
26
|
-
("openrouter", "OpenRouter (multi-provider)", "api"),
|
|
27
|
-
("glm", "GLM (Zhipu AI)", "api"),
|
|
28
|
-
("kimi", "Kimi (Moonshot AI)", "api"),
|
|
29
|
-
("minimax", "MiniMax", "api"),
|
|
30
|
-
("local", "Local model (llama.cpp)", "local"),
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
# API providers that need a key (skip bone free tier and local)
|
|
34
|
-
_API_PROVIDERS = [p for p in WIZARD_PROVIDERS if p[2] == "api" and p[0] != "bone"]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _resolve_config_path() -> Path:
|
|
38
|
-
"""Resolve config.yaml path (delegates to llm_config)."""
|
|
39
|
-
return llm_config.resolve_config_path()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def is_first_run() -> bool:
|
|
43
|
-
"""Return True if config.yaml does not exist."""
|
|
44
|
-
return not _resolve_config_path().exists()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _prompt(console: Console, message: str, default: str = "", password: bool = False) -> str:
|
|
48
|
-
"""Prompt the user using prompt_toolkit with rich-styled message.
|
|
49
|
-
|
|
50
|
-
Falls back to simple input() if prompt_toolkit fails.
|
|
51
|
-
"""
|
|
52
|
-
try:
|
|
53
|
-
suffix = f" [{default}]" if default else ""
|
|
54
|
-
if password:
|
|
55
|
-
return pt_prompt(
|
|
56
|
-
FormattedText([("class:prompt", f"{message}{suffix}: ")]),
|
|
57
|
-
is_password=True,
|
|
58
|
-
).strip() or default
|
|
59
|
-
return pt_prompt(
|
|
60
|
-
FormattedText([("class:prompt", f"{message}{suffix}: ")]),
|
|
61
|
-
).strip() or default
|
|
62
|
-
except (EOFError, KeyboardInterrupt):
|
|
63
|
-
raise
|
|
64
|
-
except Exception:
|
|
65
|
-
# Fallback for environments where prompt_toolkit misbehaves
|
|
66
|
-
suffix = f" [{default}]" if default else ""
|
|
67
|
-
if password:
|
|
68
|
-
import getpass
|
|
69
|
-
return getpass.getpass(f"{message}{suffix}: ").strip() or default
|
|
70
|
-
return input(f"{message}{suffix}: ").strip() or default
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _select_provider(console: Console) -> str:
|
|
74
|
-
"""Interactive provider selection. Returns provider ID."""
|
|
75
|
-
console.print()
|
|
76
|
-
console.print(Panel(
|
|
77
|
-
Text.from_markup("[bold #5F9EA0]Select a provider[/bold #5F9EA0]\n\n"
|
|
78
|
-
"Enter the number or provider name."),
|
|
79
|
-
title="Step 1 of 3",
|
|
80
|
-
border_style="grey23",
|
|
81
|
-
padding=(0, 2),
|
|
82
|
-
title_align="left",
|
|
83
|
-
))
|
|
84
|
-
|
|
85
|
-
for i, (pid, label, ptype) in enumerate(WIZARD_PROVIDERS, 1):
|
|
86
|
-
marker = "[dim]free[/dim]" if pid == "bone" else ""
|
|
87
|
-
console.print(f" [bold]{i:>2}[/bold]. {label} {marker}")
|
|
88
|
-
|
|
89
|
-
console.print()
|
|
90
|
-
|
|
91
|
-
while True:
|
|
92
|
-
choice = _prompt(console, "Provider", default="1")
|
|
93
|
-
# Try numeric lookup
|
|
94
|
-
try:
|
|
95
|
-
idx = int(choice)
|
|
96
|
-
if 1 <= idx <= len(WIZARD_PROVIDERS):
|
|
97
|
-
selected = WIZARD_PROVIDERS[idx - 1]
|
|
98
|
-
console.print(f" [green]Selected:[/green] {selected[1]}")
|
|
99
|
-
return selected[0]
|
|
100
|
-
except ValueError:
|
|
101
|
-
pass
|
|
102
|
-
# Try name lookup
|
|
103
|
-
choice_lower = choice.lower().strip()
|
|
104
|
-
for pid, label, ptype in WIZARD_PROVIDERS:
|
|
105
|
-
if choice_lower in (pid, pid.split("_")[0]):
|
|
106
|
-
console.print(f" [green]Selected:[/green] {label}")
|
|
107
|
-
return pid
|
|
108
|
-
|
|
109
|
-
console.print(f" [red]Invalid choice: '{choice}'. Enter a number or provider name.[/red]")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _prompt_api_key(console: Console, provider_id: str) -> str:
|
|
113
|
-
"""Prompt for API key if the provider requires one."""
|
|
114
|
-
# bone free tier and local don't need a key
|
|
115
|
-
if provider_id == "bone":
|
|
116
|
-
console.print("\n [dim]bone-agent free tier — no API key required.[/dim]")
|
|
117
|
-
return ""
|
|
118
|
-
if provider_id == "local":
|
|
119
|
-
console.print("\n [dim]Local model — no API key required.[/dim]")
|
|
120
|
-
return ""
|
|
121
|
-
|
|
122
|
-
provider_label = next((label for pid, label, _ in WIZARD_PROVIDERS if pid == provider_id), provider_id)
|
|
123
|
-
console.print()
|
|
124
|
-
console.print(Panel(
|
|
125
|
-
Text.from_markup(f"Enter your [bold]{provider_label}[/bold] API key."),
|
|
126
|
-
title="Step 2 of 3",
|
|
127
|
-
border_style="grey23",
|
|
128
|
-
padding=(0, 2),
|
|
129
|
-
title_align="left",
|
|
130
|
-
))
|
|
131
|
-
|
|
132
|
-
key = _prompt(console, "API key", password=True)
|
|
133
|
-
if not key:
|
|
134
|
-
console.print(" [yellow]No key entered — you can set it later with [bold]/key[/bold].[/yellow]")
|
|
135
|
-
return key
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def _prompt_obsidian(console: Console) -> tuple[bool, str]:
|
|
139
|
-
"""Prompt for Obsidian vault settings. Returns (enabled, vault_path)."""
|
|
140
|
-
console.print()
|
|
141
|
-
console.print(Panel(
|
|
142
|
-
Text.from_markup("Enable Obsidian vault integration?\n"
|
|
143
|
-
"This lets bone-agent create project notes in your vault."),
|
|
144
|
-
title="Step 3 of 3",
|
|
145
|
-
border_style="grey23",
|
|
146
|
-
padding=(0, 2),
|
|
147
|
-
title_align="left",
|
|
148
|
-
))
|
|
149
|
-
|
|
150
|
-
enable = _prompt(console, "Enable Obsidian? (y/n)", default="n").lower().startswith("y")
|
|
151
|
-
|
|
152
|
-
if not enable:
|
|
153
|
-
console.print(" [dim]Obsidian integration disabled. Enable later with [bold]/obsidian[/bold].[/dim]")
|
|
154
|
-
return (False, "")
|
|
155
|
-
|
|
156
|
-
while True:
|
|
157
|
-
vault_path = _prompt(console, "Vault path", default="~/Vault")
|
|
158
|
-
expanded = Path(vault_path).expanduser().resolve()
|
|
159
|
-
|
|
160
|
-
if expanded.is_dir():
|
|
161
|
-
# Check for .obsidian folder as validation
|
|
162
|
-
obsidian_marker = expanded / ".obsidian"
|
|
163
|
-
if obsidian_marker.is_dir():
|
|
164
|
-
console.print(f" [green]Valid Obsidian vault detected.[/green]")
|
|
165
|
-
else:
|
|
166
|
-
console.print(f" [yellow]Directory exists but no .obsidian/ folder found.[/yellow]")
|
|
167
|
-
console.print(f" [yellow]Make sure this is your Obsidian vault root.[/yellow]")
|
|
168
|
-
else:
|
|
169
|
-
console.print(f" [yellow]Directory does not exist yet: {expanded}[/yellow]")
|
|
170
|
-
console.print(f" [yellow]It will be created when needed.[/yellow]")
|
|
171
|
-
|
|
172
|
-
confirm = _prompt(console, "Confirm this path? (y/n)", default="y").lower().startswith("y")
|
|
173
|
-
if confirm:
|
|
174
|
-
return (True, str(expanded))
|
|
175
|
-
|
|
176
|
-
retry = _prompt(console, "Try another path? (y/n)", default="y").lower().startswith("y")
|
|
177
|
-
if not retry:
|
|
178
|
-
return (False, "")
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def write_config(provider_id: str, api_key: str = "",
|
|
182
|
-
obsidian_enabled: bool = False, obsidian_path: str = "") -> Path:
|
|
183
|
-
"""Generate and write config.yaml from wizard responses.
|
|
184
|
-
|
|
185
|
-
Uses generate_config_template() for the base, then overlays
|
|
186
|
-
user selections.
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
Path to the written config file.
|
|
190
|
-
"""
|
|
191
|
-
config_data = llm_config.generate_config_template()
|
|
192
|
-
|
|
193
|
-
# Set the chosen provider
|
|
194
|
-
config_data["LAST_PROVIDER"] = provider_id
|
|
195
|
-
|
|
196
|
-
# Set API key if provided
|
|
197
|
-
if api_key:
|
|
198
|
-
# Map provider ID to its API key config field
|
|
199
|
-
key_map = {
|
|
200
|
-
"openrouter": "OPENROUTER_API_KEY",
|
|
201
|
-
"glm": "GLM_API_KEY",
|
|
202
|
-
"openai": "OPENAI_API_KEY",
|
|
203
|
-
"gemini": "GEMINI_API_KEY",
|
|
204
|
-
"minimax": "MINIMAX_API_KEY",
|
|
205
|
-
"anthropic": "ANTHROPIC_API_KEY",
|
|
206
|
-
"kimi": "KIMI_API_KEY",
|
|
207
|
-
"bone": "BONE_PROXY_API_KEY",
|
|
208
|
-
}
|
|
209
|
-
config_key = key_map.get(provider_id)
|
|
210
|
-
if config_key:
|
|
211
|
-
config_data[config_key] = api_key
|
|
212
|
-
|
|
213
|
-
# Set Obsidian settings
|
|
214
|
-
config_data["OBSIDIAN_SETTINGS"] = {
|
|
215
|
-
"enabled": obsidian_enabled,
|
|
216
|
-
"vault_path": obsidian_path,
|
|
217
|
-
"exclude_folders": ".obsidian,.trash,node_modules,.git,__pycache__",
|
|
218
|
-
"project_base": "Dev",
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
config_path = _resolve_config_path()
|
|
222
|
-
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
-
|
|
224
|
-
import yaml
|
|
225
|
-
with open(config_path, "w", encoding="utf-8-sig") as f:
|
|
226
|
-
yaml.dump(config_data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
227
|
-
|
|
228
|
-
return config_path
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def run_wizard(console: Optional[Console] = None) -> bool:
|
|
232
|
-
"""Run the interactive setup wizard.
|
|
233
|
-
|
|
234
|
-
Args:
|
|
235
|
-
console: Rich console instance. Uses a new one if None.
|
|
236
|
-
|
|
237
|
-
Returns:
|
|
238
|
-
True if wizard completed successfully, False if user aborted.
|
|
239
|
-
"""
|
|
240
|
-
if console is None:
|
|
241
|
-
console = Console()
|
|
242
|
-
|
|
243
|
-
console.print()
|
|
244
|
-
console.print(Panel(
|
|
245
|
-
Text.from_markup(
|
|
246
|
-
"[bold #5F9EA0]Welcome to bone-agent![/bold #5F9EA0]\n\n"
|
|
247
|
-
"Let's get you set up. This will take about a minute.\n"
|
|
248
|
-
"You can always change settings later with [bold]/config[/bold]."
|
|
249
|
-
),
|
|
250
|
-
border_style="grey23",
|
|
251
|
-
padding=(0, 2),
|
|
252
|
-
title_align="left",
|
|
253
|
-
))
|
|
254
|
-
|
|
255
|
-
try:
|
|
256
|
-
# Step 1: Provider
|
|
257
|
-
provider_id = _select_provider(console)
|
|
258
|
-
|
|
259
|
-
# Step 2: API key
|
|
260
|
-
api_key = _prompt_api_key(console, provider_id)
|
|
261
|
-
|
|
262
|
-
# Step 3: Obsidian
|
|
263
|
-
obsidian_enabled, obsidian_path = _prompt_obsidian(console)
|
|
264
|
-
|
|
265
|
-
# Write config
|
|
266
|
-
config_path = write_config(
|
|
267
|
-
provider_id=provider_id,
|
|
268
|
-
api_key=api_key,
|
|
269
|
-
obsidian_enabled=obsidian_enabled,
|
|
270
|
-
obsidian_path=obsidian_path,
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
console.print()
|
|
274
|
-
console.print(Panel(
|
|
275
|
-
Text.from_markup(
|
|
276
|
-
f"[bold green]Setup complete![/bold green]\n\n"
|
|
277
|
-
f"Config written to: [dim]{config_path}[/dim]\n"
|
|
278
|
-
f"Provider: [bold]{provider_id}[/bold]\n"
|
|
279
|
-
f"Obsidian: {'enabled' if obsidian_enabled else 'disabled'}\n\n"
|
|
280
|
-
f"[dim]Tip: Use [bold]/provider[/bold] to switch providers,[/dim]\n"
|
|
281
|
-
f"[dim] [bold]/key[/bold] to update API keys,[/dim]\n"
|
|
282
|
-
f"[dim] [bold]/obsidian[/bold] to reconfigure vault.[/dim]"
|
|
283
|
-
),
|
|
284
|
-
border_style="grey23",
|
|
285
|
-
padding=(0, 2),
|
|
286
|
-
title_align="left",
|
|
287
|
-
))
|
|
288
|
-
console.print()
|
|
289
|
-
|
|
290
|
-
return True
|
|
291
|
-
|
|
292
|
-
except (KeyboardInterrupt, EOFError):
|
|
293
|
-
console.print("\n[yellow]Setup cancelled.[/yellow]")
|
|
294
|
-
return False
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
"""Live panel for streaming sub-agent tool output."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import threading
|
|
5
|
-
import time
|
|
6
|
-
|
|
7
|
-
from rich.panel import Panel
|
|
8
|
-
from rich.text import Text
|
|
9
|
-
from rich.live import Live
|
|
10
|
-
|
|
11
|
-
from core.tool_feedback import build_panel_tool_message
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class SubAgentPanel:
|
|
17
|
-
"""Live panel for streaming sub-agent tool output.
|
|
18
|
-
|
|
19
|
-
Displays a Rich panel with animated spinner, tool call log, and
|
|
20
|
-
completion/error status for sub-agent invocations.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
24
|
-
|
|
25
|
-
def __init__(self, query, console):
|
|
26
|
-
"""Initialize the sub-agent panel.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
query: The task query for the sub-agent
|
|
30
|
-
console: Rich console for display
|
|
31
|
-
"""
|
|
32
|
-
self.console = console
|
|
33
|
-
self.query = query
|
|
34
|
-
self.tool_calls = [] # List of formatted Rich markup strings
|
|
35
|
-
self.total_tool_calls = 0
|
|
36
|
-
self.token_info = None # Live token info string, e.g. "32k / 45k"
|
|
37
|
-
self._live = None
|
|
38
|
-
self._spinner_index = 0
|
|
39
|
-
self._show_spinner = True
|
|
40
|
-
self._spinner_thread = None
|
|
41
|
-
self._stop_spinner = threading.Event()
|
|
42
|
-
self._saved_termios = None
|
|
43
|
-
|
|
44
|
-
# ------------------------------------------------------------------
|
|
45
|
-
# Panel rendering
|
|
46
|
-
# ------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
def _get_title(self):
|
|
49
|
-
"""Get panel title with optional spinner and tool call counter."""
|
|
50
|
-
token_suffix = f" | {self.token_info}" if self.token_info else ""
|
|
51
|
-
if self._show_spinner:
|
|
52
|
-
spinner = self._SPINNER_FRAMES[self._spinner_index % len(self._SPINNER_FRAMES)]
|
|
53
|
-
return f"[#5F9EA0]{spinner} Sub-Agent ({self.total_tool_calls}){token_suffix}[/#5F9EA0]"
|
|
54
|
-
return f"[#5F9EA0]Sub-Agent ({self.total_tool_calls}){token_suffix}[/#5F9EA0]"
|
|
55
|
-
|
|
56
|
-
def _render_panel(self, title=None, border_style="#5F9EA0"):
|
|
57
|
-
"""Render the current panel state.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
title: Optional title override. If None, uses _get_title().
|
|
61
|
-
border_style: Border style (default: "#5F9EA0")
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
Rich Panel object with current content and title
|
|
65
|
-
"""
|
|
66
|
-
lines = [f"[bold #5F9EA0]Query:[/bold #5F9EA0] {self.query}", ""]
|
|
67
|
-
|
|
68
|
-
if self.tool_calls:
|
|
69
|
-
content = "\n".join(self.tool_calls)
|
|
70
|
-
lines.append(content)
|
|
71
|
-
else:
|
|
72
|
-
lines.append("[dim]No tools called yet[/dim]")
|
|
73
|
-
|
|
74
|
-
content = "\n".join(lines)
|
|
75
|
-
return Panel(
|
|
76
|
-
Text.from_markup(content, justify="left"),
|
|
77
|
-
title=title if title is not None else self._get_title(),
|
|
78
|
-
title_align="left",
|
|
79
|
-
border_style=border_style,
|
|
80
|
-
padding=(0, 1),
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
# ------------------------------------------------------------------
|
|
84
|
-
# Spinner animation
|
|
85
|
-
# ------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
def _spin(self):
|
|
88
|
-
"""Background thread: continuously increment spinner and update display."""
|
|
89
|
-
while not self._stop_spinner.is_set():
|
|
90
|
-
self._spinner_index += 1
|
|
91
|
-
if self._live:
|
|
92
|
-
self._live.update(self._render_panel())
|
|
93
|
-
time.sleep(0.1) # 10 updates per second = smooth animation
|
|
94
|
-
|
|
95
|
-
# ------------------------------------------------------------------
|
|
96
|
-
# Terminal raw mode (suppress keystroke echoes during spinner)
|
|
97
|
-
# ------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
@staticmethod
|
|
100
|
-
def _set_raw_mode():
|
|
101
|
-
"""Switch stdin to raw mode to prevent keystroke echoes during spinner."""
|
|
102
|
-
import os
|
|
103
|
-
import sys
|
|
104
|
-
if os.name == 'nt':
|
|
105
|
-
return
|
|
106
|
-
try:
|
|
107
|
-
import termios
|
|
108
|
-
fd = sys.stdin.fileno()
|
|
109
|
-
old = termios.tcgetattr(fd)
|
|
110
|
-
new = old.copy()
|
|
111
|
-
new[3] &= ~(termios.ECHO | termios.ICANON | termios.IEXTEN)
|
|
112
|
-
new[0] &= ~(termios.ICRNL)
|
|
113
|
-
termios.tcsetattr(fd, termios.TCSANOW, new)
|
|
114
|
-
return old
|
|
115
|
-
except Exception:
|
|
116
|
-
return None
|
|
117
|
-
|
|
118
|
-
@staticmethod
|
|
119
|
-
def _restore_terminal_mode(saved):
|
|
120
|
-
"""Restore terminal mode from saved termios attributes."""
|
|
121
|
-
import os
|
|
122
|
-
import sys
|
|
123
|
-
if saved is None:
|
|
124
|
-
return
|
|
125
|
-
try:
|
|
126
|
-
import os as _os
|
|
127
|
-
if _os.name == 'nt':
|
|
128
|
-
return
|
|
129
|
-
except Exception:
|
|
130
|
-
pass
|
|
131
|
-
try:
|
|
132
|
-
import termios
|
|
133
|
-
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, saved)
|
|
134
|
-
except Exception:
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
# ------------------------------------------------------------------
|
|
138
|
-
# Context manager (Live display lifecycle)
|
|
139
|
-
# ------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
def __enter__(self):
|
|
142
|
-
"""Start Live display context.
|
|
143
|
-
|
|
144
|
-
Returns:
|
|
145
|
-
self for use in with statement
|
|
146
|
-
"""
|
|
147
|
-
self._saved_termios = self._set_raw_mode()
|
|
148
|
-
panel = self._render_panel()
|
|
149
|
-
self._live = Live(panel, console=self.console, refresh_per_second=10)
|
|
150
|
-
self._live.__enter__()
|
|
151
|
-
|
|
152
|
-
# Start background spinner thread
|
|
153
|
-
self._spinner_thread = threading.Thread(target=self._spin, daemon=True)
|
|
154
|
-
self._spinner_thread.start()
|
|
155
|
-
|
|
156
|
-
return self
|
|
157
|
-
|
|
158
|
-
def __exit__(self, *args):
|
|
159
|
-
"""End Live display context."""
|
|
160
|
-
self._stop_spinner.set()
|
|
161
|
-
if self._spinner_thread:
|
|
162
|
-
self._spinner_thread.join(timeout=0.5)
|
|
163
|
-
if self._live:
|
|
164
|
-
self._live.__exit__(*args)
|
|
165
|
-
self._restore_terminal_mode(self._saved_termios)
|
|
166
|
-
self._saved_termios = None
|
|
167
|
-
|
|
168
|
-
# ------------------------------------------------------------------
|
|
169
|
-
# Public API
|
|
170
|
-
# ------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
def add_tool_call(self, tool_name, tool_result=None, command=None):
|
|
173
|
-
"""Add a tool call message to the panel and refresh display.
|
|
174
|
-
|
|
175
|
-
Delegates formatting to core.tool_feedback.build_panel_tool_message
|
|
176
|
-
to avoid duplicating display logic.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
tool_name: Name of the tool (e.g., "read_file", "rg")
|
|
180
|
-
tool_result: Optional tool result string (for detailed formatting)
|
|
181
|
-
command: Optional command string for context
|
|
182
|
-
"""
|
|
183
|
-
self.total_tool_calls += 1
|
|
184
|
-
message = build_panel_tool_message(tool_name, tool_result, command)
|
|
185
|
-
self.tool_calls.append(message)
|
|
186
|
-
# Keep only last 5 tool calls
|
|
187
|
-
if len(self.tool_calls) > 5:
|
|
188
|
-
self.tool_calls.pop(0)
|
|
189
|
-
self._live.update(self._render_panel())
|
|
190
|
-
|
|
191
|
-
def append(self, text):
|
|
192
|
-
"""Append text to panel and refresh display (kept for compatibility).
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
text: Text to append (may contain Rich markup)
|
|
196
|
-
"""
|
|
197
|
-
# Just update panel to refresh title counter
|
|
198
|
-
self._live.update(self._render_panel())
|
|
199
|
-
|
|
200
|
-
def set_complete(self, usage=None):
|
|
201
|
-
"""Mark panel as complete with optional token info.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
usage: Optional dict with 'prompt', 'completion', 'total' token counts
|
|
205
|
-
"""
|
|
206
|
-
self._show_spinner = False # Stop spinner
|
|
207
|
-
|
|
208
|
-
# Build title with token usage: conversation length first, total billed second
|
|
209
|
-
if usage and usage.get('total_tokens'):
|
|
210
|
-
ctx_tokens = usage.get('context_tokens', 0)
|
|
211
|
-
total_tokens = usage.get('total_tokens', 0)
|
|
212
|
-
title = f"[green]✓ Sub-Agent Complete ({self.total_tool_calls}) - {ctx_tokens:,} curr, {total_tokens:,} total[/green]"
|
|
213
|
-
else:
|
|
214
|
-
title = f"[green]✓ Sub-Agent Complete ({self.total_tool_calls})[/green]"
|
|
215
|
-
|
|
216
|
-
self._live.update(self._render_panel(
|
|
217
|
-
title=title,
|
|
218
|
-
border_style="green"
|
|
219
|
-
))
|
|
220
|
-
|
|
221
|
-
def set_error(self, message):
|
|
222
|
-
"""Show error in panel with red styling.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
message: Error message to display
|
|
226
|
-
"""
|
|
227
|
-
self._show_spinner = False # Stop spinner
|
|
228
|
-
self._live.update(Panel(
|
|
229
|
-
message,
|
|
230
|
-
title="[red]✗ Sub-Agent Error[/red]",
|
|
231
|
-
title_align="left",
|
|
232
|
-
border_style="red",
|
|
233
|
-
padding=(0, 1),
|
|
234
|
-
))
|