velune-cli 0.9.0__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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
velune/cli/pull_ui.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Interactive model browser TUI for /pull — select a model to download."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.application import Application
|
|
8
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
9
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
10
|
+
from prompt_toolkit.layout import Layout
|
|
11
|
+
from prompt_toolkit.layout.containers import Window
|
|
12
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
13
|
+
|
|
14
|
+
from velune.providers.ollama_manager import RECOMMENDED_MODELS
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
_SKILL_STYLES: dict[str, str] = {
|
|
20
|
+
"coding": "fg:ansigreen",
|
|
21
|
+
"reasoning": "fg:ansimagenta",
|
|
22
|
+
"embedding": "fg:ansicyan",
|
|
23
|
+
"general": "fg:ansibrightblack",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def run_pull_ui(
|
|
28
|
+
local_models: list[str],
|
|
29
|
+
hardware_ram_gb: float,
|
|
30
|
+
console: Console,
|
|
31
|
+
) -> str | None:
|
|
32
|
+
"""Show the interactive model browser and return the chosen model_id, or None."""
|
|
33
|
+
|
|
34
|
+
selected_idx = [0]
|
|
35
|
+
result: list[str | None] = [None]
|
|
36
|
+
|
|
37
|
+
def _fits(model: dict) -> bool:
|
|
38
|
+
try:
|
|
39
|
+
needed = float(model["ram_needed"].replace(" GB", "").strip())
|
|
40
|
+
return hardware_ram_gb >= needed
|
|
41
|
+
except Exception:
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
def render_list() -> FormattedText:
|
|
45
|
+
lines: list[tuple[str, str]] = []
|
|
46
|
+
lines.append(("bold", " Available models to pull\n"))
|
|
47
|
+
lines.append(("fg:ansibrightblack", " ↑↓ navigate · Enter pull · Esc cancel\n\n"))
|
|
48
|
+
lines.append(("fg:ansibrightblack", f" Your RAM: {hardware_ram_gb:.0f} GB\n\n"))
|
|
49
|
+
|
|
50
|
+
for i, model in enumerate(RECOMMENDED_MODELS):
|
|
51
|
+
is_active = i == selected_idx[0]
|
|
52
|
+
prefix = "❯ " if is_active else " "
|
|
53
|
+
row_style = "bold fg:cyan" if is_active else ""
|
|
54
|
+
model_id = model["model_id"]
|
|
55
|
+
is_local = any(
|
|
56
|
+
m == model_id or m.split(":")[0] == model_id.split(":")[0] and m == model_id
|
|
57
|
+
for m in local_models
|
|
58
|
+
)
|
|
59
|
+
fits = _fits(model)
|
|
60
|
+
skill = model.get("skill", "general")
|
|
61
|
+
skill_style = _SKILL_STYLES.get(skill, "fg:ansibrightblack")
|
|
62
|
+
|
|
63
|
+
if is_local:
|
|
64
|
+
status = " ✓ installed"
|
|
65
|
+
status_style = "fg:ansigreen"
|
|
66
|
+
elif not fits:
|
|
67
|
+
status = " needs more RAM"
|
|
68
|
+
status_style = "fg:ansibrightblack"
|
|
69
|
+
else:
|
|
70
|
+
status = ""
|
|
71
|
+
status_style = ""
|
|
72
|
+
|
|
73
|
+
lines.append(
|
|
74
|
+
(
|
|
75
|
+
row_style,
|
|
76
|
+
f" {prefix}{model_id:<34} {model['size_gb']:4.1f} GB ",
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
lines.append((skill_style, f"[{skill}]"))
|
|
80
|
+
if status:
|
|
81
|
+
lines.append((status_style, status))
|
|
82
|
+
lines.append(("", "\n"))
|
|
83
|
+
|
|
84
|
+
if is_active:
|
|
85
|
+
lines.append(("fg:ansibrightblack", f" {model['description']}\n"))
|
|
86
|
+
lines.append(
|
|
87
|
+
("fg:ansibrightblack", f" RAM needed: {model['ram_needed']}\n")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return FormattedText(lines)
|
|
91
|
+
|
|
92
|
+
kb = KeyBindings()
|
|
93
|
+
|
|
94
|
+
@kb.add("up")
|
|
95
|
+
def _up(event) -> None:
|
|
96
|
+
selected_idx[0] = (selected_idx[0] - 1) % len(RECOMMENDED_MODELS)
|
|
97
|
+
|
|
98
|
+
@kb.add("down")
|
|
99
|
+
def _down(event) -> None:
|
|
100
|
+
selected_idx[0] = (selected_idx[0] + 1) % len(RECOMMENDED_MODELS)
|
|
101
|
+
|
|
102
|
+
@kb.add("enter")
|
|
103
|
+
def _select(event) -> None:
|
|
104
|
+
result[0] = RECOMMENDED_MODELS[selected_idx[0]]["model_id"]
|
|
105
|
+
event.app.exit()
|
|
106
|
+
|
|
107
|
+
@kb.add("escape")
|
|
108
|
+
@kb.add("c-c")
|
|
109
|
+
def _cancel(event) -> None:
|
|
110
|
+
event.app.exit()
|
|
111
|
+
|
|
112
|
+
app = Application(
|
|
113
|
+
layout=Layout(
|
|
114
|
+
Window(
|
|
115
|
+
content=FormattedTextControl(render_list, focusable=True),
|
|
116
|
+
)
|
|
117
|
+
),
|
|
118
|
+
key_bindings=kb,
|
|
119
|
+
full_screen=False,
|
|
120
|
+
mouse_support=False,
|
|
121
|
+
)
|
|
122
|
+
await app.run_async()
|
|
123
|
+
return result[0]
|
velune/cli/registry.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CLI command discovery and registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from importlib.metadata import EntryPoint, entry_points
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from velune.cli.commands import (
|
|
13
|
+
ask_command,
|
|
14
|
+
chat_command,
|
|
15
|
+
config_cmd,
|
|
16
|
+
daemon_cmd,
|
|
17
|
+
doctor_cmd,
|
|
18
|
+
init_command,
|
|
19
|
+
mcp_cmd,
|
|
20
|
+
mcp_serve,
|
|
21
|
+
memory_cmd,
|
|
22
|
+
models_cmd,
|
|
23
|
+
run_command,
|
|
24
|
+
setup_command,
|
|
25
|
+
workspace_cmd,
|
|
26
|
+
)
|
|
27
|
+
from velune.kernel.registry import ServiceContainer
|
|
28
|
+
|
|
29
|
+
BUILTIN_COMMAND_MODULES: Sequence[str] = (
|
|
30
|
+
"velune.cli.commands.ask",
|
|
31
|
+
"velune.cli.commands.chat",
|
|
32
|
+
"velune.cli.commands.run",
|
|
33
|
+
"velune.cli.commands.models",
|
|
34
|
+
"velune.cli.commands.workspace",
|
|
35
|
+
"velune.cli.commands.memory",
|
|
36
|
+
"velune.cli.commands.config",
|
|
37
|
+
"velune.cli.commands.daemon",
|
|
38
|
+
"velune.cli.commands.doctor",
|
|
39
|
+
"velune.cli.commands.mcp",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def discover_builtin_modules() -> list[ModuleType]:
|
|
44
|
+
"""Import and return built-in command modules."""
|
|
45
|
+
|
|
46
|
+
modules: list[ModuleType] = []
|
|
47
|
+
for module_name in BUILTIN_COMMAND_MODULES:
|
|
48
|
+
modules.append(importlib.import_module(module_name))
|
|
49
|
+
return modules
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def discover_plugin_entry_points(group: str = "velune.commands") -> list[EntryPoint]:
|
|
53
|
+
"""Return third-party command extension entry points."""
|
|
54
|
+
|
|
55
|
+
return list(entry_points(group=group))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register_commands(app: typer.Typer, container: ServiceContainer) -> None:
|
|
59
|
+
"""Attach all built-in and plugin command groups to the app."""
|
|
60
|
+
|
|
61
|
+
app.command(name="ask")(ask_command)
|
|
62
|
+
app.command(name="chat")(chat_command)
|
|
63
|
+
app.command(name="init")(init_command)
|
|
64
|
+
app.command(name="run")(run_command)
|
|
65
|
+
app.command(name="setup")(setup_command)
|
|
66
|
+
app.command(name="mcp-serve")(mcp_serve)
|
|
67
|
+
app.add_typer(models_cmd, name="models")
|
|
68
|
+
app.add_typer(workspace_cmd, name="workspace")
|
|
69
|
+
app.add_typer(memory_cmd, name="memory")
|
|
70
|
+
app.add_typer(config_cmd, name="config")
|
|
71
|
+
app.add_typer(daemon_cmd, name="daemon")
|
|
72
|
+
app.add_typer(doctor_cmd, name="doctor")
|
|
73
|
+
app.add_typer(mcp_cmd, name="mcp")
|
|
74
|
+
|
|
75
|
+
for entry_point in discover_plugin_entry_points():
|
|
76
|
+
loaded = entry_point.load()
|
|
77
|
+
if hasattr(loaded, "register"):
|
|
78
|
+
loaded.register(app=app, container=container)
|
|
79
|
+
elif callable(loaded):
|
|
80
|
+
loaded(app=app, container=container)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Rich error panel renderer for structured VeluneError display."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from velune.core.errors.catalog import VeluneError
|
|
13
|
+
|
|
14
|
+
_BUG_REPORT_URL = "https://github.com/velune-ai/velune/issues"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_error(error: VeluneError) -> Panel:
|
|
18
|
+
"""Build a Rich Panel for a VeluneError with cause and fix sections."""
|
|
19
|
+
body = Text()
|
|
20
|
+
|
|
21
|
+
cause = error.get_cause()
|
|
22
|
+
if cause:
|
|
23
|
+
body.append("Cause\n", style="bold white")
|
|
24
|
+
body.append(f" {cause}\n", style="dim white")
|
|
25
|
+
|
|
26
|
+
if error.fix:
|
|
27
|
+
body.append("\nFix\n", style="bold white")
|
|
28
|
+
for step in error.fix:
|
|
29
|
+
body.append(f" • {step}\n", style="dim white")
|
|
30
|
+
|
|
31
|
+
if error.docs_url:
|
|
32
|
+
body.append(f"\n docs → {error.docs_url}", style="blue underline")
|
|
33
|
+
|
|
34
|
+
detail = error.get_detail()
|
|
35
|
+
if detail and detail != error.title:
|
|
36
|
+
body.append("\n\n Detail: ", style="dim")
|
|
37
|
+
body.append(detail, style="dim white")
|
|
38
|
+
|
|
39
|
+
body.append("\n\n Use --verbose for full stack trace.", style="dim")
|
|
40
|
+
|
|
41
|
+
return Panel(
|
|
42
|
+
body,
|
|
43
|
+
title=f"[bold red]Error:[/bold red] {error.title}",
|
|
44
|
+
border_style="red",
|
|
45
|
+
padding=(1, 2),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def render_unexpected_error(exc: Exception) -> Panel:
|
|
50
|
+
"""Build a Rich Panel for an unrecognised exception."""
|
|
51
|
+
body = Text()
|
|
52
|
+
body.append(
|
|
53
|
+
"An unexpected error occurred.\n\n",
|
|
54
|
+
style="dim white",
|
|
55
|
+
)
|
|
56
|
+
body.append(f" {type(exc).__name__}: ", style="dim white")
|
|
57
|
+
body.append(f"{exc}\n", style="white")
|
|
58
|
+
|
|
59
|
+
body.append("\nWhat to do\n", style="bold white")
|
|
60
|
+
body.append(" • Use --verbose to see the full stack trace\n", style="dim white")
|
|
61
|
+
body.append(f" • Report the issue at {_BUG_REPORT_URL}\n", style="dim white")
|
|
62
|
+
body.append(" • Include the --verbose output in your report\n", style="dim white")
|
|
63
|
+
|
|
64
|
+
return Panel(
|
|
65
|
+
body,
|
|
66
|
+
title="[bold red]Unexpected Error[/bold red]",
|
|
67
|
+
border_style="red",
|
|
68
|
+
padding=(1, 2),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def print_error(error: VeluneError, console: Console | None = None) -> None:
|
|
73
|
+
"""Convenience wrapper: render and print a VeluneError to the given console."""
|
|
74
|
+
(console or Console()).print(render_error(error))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def print_unexpected_error(exc: Exception, console: Console | None = None) -> None:
|
|
78
|
+
"""Convenience wrapper: render and print an unexpected exception."""
|
|
79
|
+
(console or Console()).print(render_unexpected_error(exc))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
4
|
+
from rich.markdown import CodeBlock, Markdown
|
|
5
|
+
from rich.syntax import Syntax
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CustomCodeBlock(CodeBlock):
|
|
9
|
+
"""A code block with syntax highlighting and line numbers for blocks >5 lines."""
|
|
10
|
+
|
|
11
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
12
|
+
code = str(self.text).rstrip()
|
|
13
|
+
line_count = len(code.splitlines())
|
|
14
|
+
line_numbers = line_count > 5
|
|
15
|
+
syntax = Syntax(
|
|
16
|
+
code,
|
|
17
|
+
self.lexer_name,
|
|
18
|
+
theme="monokai",
|
|
19
|
+
word_wrap=True,
|
|
20
|
+
line_numbers=line_numbers,
|
|
21
|
+
padding=1,
|
|
22
|
+
)
|
|
23
|
+
yield syntax
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CustomMarkdown(Markdown):
|
|
27
|
+
"""Custom Markdown renderer that overrides CodeBlock element handling."""
|
|
28
|
+
|
|
29
|
+
elements = Markdown.elements.copy()
|
|
30
|
+
elements["fence"] = CustomCodeBlock
|
|
31
|
+
elements["code_block"] = CustomCodeBlock
|
|
32
|
+
|
|
33
|
+
def __init__(self, markup: str, code_theme: str = "monokai", **kwargs) -> None:
|
|
34
|
+
super().__init__(markup, code_theme=code_theme, **kwargs)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MarkdownStreamBuffer:
|
|
38
|
+
"""Buffer that accumulates chunks of Markdown, handles split fences, and returns clean renderables."""
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
self._buffer = ""
|
|
42
|
+
|
|
43
|
+
def append(self, text: str) -> None:
|
|
44
|
+
self._buffer += text
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def raw_content(self) -> str:
|
|
48
|
+
return self._buffer
|
|
49
|
+
|
|
50
|
+
def get_renderable(self) -> CustomMarkdown:
|
|
51
|
+
content = self._buffer
|
|
52
|
+
|
|
53
|
+
# Clean up split code fences at the end of the buffer to prevent flicker/incorrect rendering.
|
|
54
|
+
# If it ends with incomplete backticks on a new line (e.g. \n` or \n``)
|
|
55
|
+
# or just starts with incomplete backticks if buffer is very short
|
|
56
|
+
if content.endswith("\n`"):
|
|
57
|
+
content = content[:-2]
|
|
58
|
+
elif content.endswith("\n``"):
|
|
59
|
+
content = content[:-3]
|
|
60
|
+
elif content == "`" or content == "``":
|
|
61
|
+
content = ""
|
|
62
|
+
|
|
63
|
+
return CustomMarkdown(content)
|