axion-code 1.0.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.
- axion/__init__.py +3 -0
- axion/api/__init__.py +0 -0
- axion/api/anthropic.py +460 -0
- axion/api/client.py +259 -0
- axion/api/error.py +161 -0
- axion/api/ollama.py +597 -0
- axion/api/openai_compat.py +805 -0
- axion/api/openai_responses.py +627 -0
- axion/api/prompt_cache.py +31 -0
- axion/api/sse.py +98 -0
- axion/api/types.py +451 -0
- axion/cli/__init__.py +0 -0
- axion/cli/init_cmd.py +50 -0
- axion/cli/input.py +290 -0
- axion/cli/main.py +2953 -0
- axion/cli/render.py +489 -0
- axion/cli/tui.py +766 -0
- axion/commands/__init__.py +0 -0
- axion/commands/handlers/__init__.py +0 -0
- axion/commands/handlers/agents.py +51 -0
- axion/commands/handlers/builtin_commands.py +367 -0
- axion/commands/handlers/mcp.py +59 -0
- axion/commands/handlers/models.py +75 -0
- axion/commands/handlers/plugins.py +55 -0
- axion/commands/handlers/skills.py +61 -0
- axion/commands/parsing.py +317 -0
- axion/commands/registry.py +166 -0
- axion/compat_harness/__init__.py +0 -0
- axion/compat_harness/extractor.py +145 -0
- axion/plugins/__init__.py +0 -0
- axion/plugins/hooks.py +22 -0
- axion/plugins/manager.py +391 -0
- axion/plugins/manifest.py +270 -0
- axion/runtime/__init__.py +0 -0
- axion/runtime/bash.py +388 -0
- axion/runtime/bootstrap.py +39 -0
- axion/runtime/claude_subscription.py +300 -0
- axion/runtime/compact.py +233 -0
- axion/runtime/config.py +397 -0
- axion/runtime/conversation.py +1073 -0
- axion/runtime/file_ops.py +613 -0
- axion/runtime/git.py +213 -0
- axion/runtime/hooks.py +235 -0
- axion/runtime/image.py +212 -0
- axion/runtime/lanes.py +282 -0
- axion/runtime/lsp.py +425 -0
- axion/runtime/mcp/__init__.py +0 -0
- axion/runtime/mcp/client.py +76 -0
- axion/runtime/mcp/lifecycle.py +96 -0
- axion/runtime/mcp/stdio.py +318 -0
- axion/runtime/mcp/tool_bridge.py +79 -0
- axion/runtime/memory.py +196 -0
- axion/runtime/oauth.py +329 -0
- axion/runtime/openai_subscription.py +346 -0
- axion/runtime/permissions.py +247 -0
- axion/runtime/plan_mode.py +96 -0
- axion/runtime/policy_engine.py +259 -0
- axion/runtime/prompt.py +586 -0
- axion/runtime/recovery.py +261 -0
- axion/runtime/remote.py +28 -0
- axion/runtime/sandbox.py +68 -0
- axion/runtime/scheduler.py +231 -0
- axion/runtime/session.py +365 -0
- axion/runtime/sharing.py +159 -0
- axion/runtime/skills.py +124 -0
- axion/runtime/tasks.py +258 -0
- axion/runtime/usage.py +241 -0
- axion/runtime/workers.py +186 -0
- axion/telemetry/__init__.py +0 -0
- axion/telemetry/events.py +67 -0
- axion/telemetry/profile.py +49 -0
- axion/telemetry/sink.py +60 -0
- axion/telemetry/tracer.py +95 -0
- axion/tools/__init__.py +0 -0
- axion/tools/lane_completion.py +33 -0
- axion/tools/registry.py +853 -0
- axion/tools/tool_search.py +226 -0
- axion_code-1.0.0.dist-info/METADATA +709 -0
- axion_code-1.0.0.dist-info/RECORD +82 -0
- axion_code-1.0.0.dist-info/WHEEL +4 -0
- axion_code-1.0.0.dist-info/entry_points.txt +2 -0
- axion_code-1.0.0.dist-info/licenses/LICENSE +21 -0
axion/cli/render.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""Rich-based terminal rendering: markdown, syntax highlighting, spinners.
|
|
2
|
+
|
|
3
|
+
Maps to: rust/crates/rusty-claude-cli/src/render.rs
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.theme import Theme
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Constants (matching Rust render.rs)
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
READ_DISPLAY_MAX_LINES = 80
|
|
24
|
+
READ_DISPLAY_MAX_CHARS = 6_000
|
|
25
|
+
TOOL_OUTPUT_DISPLAY_MAX_LINES = 60
|
|
26
|
+
TOOL_OUTPUT_DISPLAY_MAX_CHARS = 4_000
|
|
27
|
+
|
|
28
|
+
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
29
|
+
SPINNER_INTERVAL_MS = 80
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Color themes
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
class ColorTheme(enum.Enum):
|
|
37
|
+
DEFAULT = "default"
|
|
38
|
+
DARK = "dark"
|
|
39
|
+
LIGHT = "light"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Navy/Cyan theme inspired by modern terminal UIs
|
|
43
|
+
CLAW_THEME = Theme({
|
|
44
|
+
"axion.prompt": "#00d4aa bold", # Bright teal/cyan for prompt
|
|
45
|
+
"axion.assistant": "#e0e0e0", # Light gray for response text
|
|
46
|
+
"axion.tool_name": "#00d4aa bold", # Teal for tool names
|
|
47
|
+
"axion.tool_border": "#0a192f", # Dark navy for borders
|
|
48
|
+
"axion.tool_input": "#8892b0", # Muted blue-gray for inputs
|
|
49
|
+
"axion.tool_output": "#64ffda", # Mint green for outputs
|
|
50
|
+
"axion.tool_success": "#64ffda bold", # Bright mint for success
|
|
51
|
+
"axion.tool_error": "#ff6b6b bold", # Coral red for errors
|
|
52
|
+
"axion.error": "#ff6b6b bold", # Coral red
|
|
53
|
+
"axion.warning": "#ffd93d", # Warm yellow
|
|
54
|
+
"axion.info": "#8892b0", # Muted blue-gray
|
|
55
|
+
"axion.cost": "#00d4aa dim", # Dim teal for cost
|
|
56
|
+
"axion.status": "#8892b0", # Muted blue-gray
|
|
57
|
+
"axion.heading": "#ccd6f6 bold", # Light blue-white for headings
|
|
58
|
+
"axion.code": "#64ffda", # Mint green for code
|
|
59
|
+
"axion.link": "#00d4aa underline", # Teal links
|
|
60
|
+
"axion.thinking": "#8892b0 italic", # Muted italic for thinking
|
|
61
|
+
"axion.success_text": "#64ffda", # Mint green
|
|
62
|
+
"axion.dim": "#8892b0", # Muted blue-gray for dim text
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Spinner (matching Rust 10-frame braille spinner)
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
class Spinner:
|
|
71
|
+
"""Animated spinner using Rich (works cross-platform including Windows).
|
|
72
|
+
|
|
73
|
+
Maps to: rust/crates/rusty-claude-cli/src/render.rs::Spinner
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, console: Console | None = None) -> None:
|
|
77
|
+
self._console = console or Console(stderr=True)
|
|
78
|
+
self._status: Any | None = None
|
|
79
|
+
self._running = False
|
|
80
|
+
|
|
81
|
+
def start(self, label: str = "Thinking...") -> None:
|
|
82
|
+
"""Start the spinner animation using Rich Status."""
|
|
83
|
+
self._running = True
|
|
84
|
+
self._status = self._console.status(f"[cyan]{label}[/cyan]", spinner="dots")
|
|
85
|
+
self._status.start()
|
|
86
|
+
|
|
87
|
+
def stop(self, final_label: str | None = None, success: bool = True) -> None:
|
|
88
|
+
"""Stop the spinner and optionally show final status."""
|
|
89
|
+
self._running = False
|
|
90
|
+
if self._status:
|
|
91
|
+
self._status.stop()
|
|
92
|
+
self._status = None
|
|
93
|
+
if final_label:
|
|
94
|
+
icon = "[green]✔[/green]" if success else "[red]✘[/red]"
|
|
95
|
+
self._console.print(f"{icon} {final_label}")
|
|
96
|
+
|
|
97
|
+
def finish(self, label: str = "Done") -> None:
|
|
98
|
+
"""Stop with success indicator."""
|
|
99
|
+
self.stop(label, success=True)
|
|
100
|
+
|
|
101
|
+
def fail(self, label: str = "Failed") -> None:
|
|
102
|
+
"""Stop with failure indicator."""
|
|
103
|
+
self.stop(label, success=False)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Markdown stream state (for incremental rendering)
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
class MarkdownStreamState:
|
|
111
|
+
"""Buffers incomplete markdown for safe streaming render.
|
|
112
|
+
|
|
113
|
+
Maps to: rust/crates/rusty-claude-cli/src/render.rs::MarkdownStreamState
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self) -> None:
|
|
117
|
+
self._pending = ""
|
|
118
|
+
self._in_code_fence = False
|
|
119
|
+
|
|
120
|
+
def push(self, renderer: TerminalRenderer, delta: str) -> str | None:
|
|
121
|
+
"""Accumulate delta text and render when safe.
|
|
122
|
+
|
|
123
|
+
Returns rendered text if a safe boundary was found, None otherwise.
|
|
124
|
+
"""
|
|
125
|
+
self._pending += delta
|
|
126
|
+
|
|
127
|
+
boundary = self._find_safe_boundary()
|
|
128
|
+
if boundary is None:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
ready = self._pending[:boundary]
|
|
132
|
+
self._pending = self._pending[boundary:]
|
|
133
|
+
|
|
134
|
+
if ready.strip():
|
|
135
|
+
return ready
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def flush(self, renderer: TerminalRenderer) -> str | None:
|
|
139
|
+
"""Render any remaining pending text."""
|
|
140
|
+
if not self._pending.strip():
|
|
141
|
+
self._pending = ""
|
|
142
|
+
return None
|
|
143
|
+
result = self._pending
|
|
144
|
+
self._pending = ""
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
def _find_safe_boundary(self) -> int | None:
|
|
148
|
+
"""Find a safe point to split the pending text for rendering.
|
|
149
|
+
|
|
150
|
+
Only splits outside code fences, at blank lines or code block ends.
|
|
151
|
+
"""
|
|
152
|
+
in_fence = self._in_code_fence
|
|
153
|
+
last_safe = None
|
|
154
|
+
|
|
155
|
+
lines = self._pending.split("\n")
|
|
156
|
+
pos = 0
|
|
157
|
+
|
|
158
|
+
for i, line in enumerate(lines):
|
|
159
|
+
stripped = line.strip()
|
|
160
|
+
# Check for code fence toggling
|
|
161
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
162
|
+
in_fence = not in_fence
|
|
163
|
+
if not in_fence:
|
|
164
|
+
# End of code block — safe boundary after this line
|
|
165
|
+
end_pos = pos + len(line) + 1 # +1 for newline
|
|
166
|
+
if end_pos <= len(self._pending):
|
|
167
|
+
last_safe = end_pos
|
|
168
|
+
|
|
169
|
+
if not in_fence and stripped == "" and i > 0:
|
|
170
|
+
# Blank line outside fence — safe boundary
|
|
171
|
+
end_pos = pos + len(line) + 1
|
|
172
|
+
if end_pos <= len(self._pending):
|
|
173
|
+
last_safe = end_pos
|
|
174
|
+
|
|
175
|
+
pos += len(line) + 1
|
|
176
|
+
|
|
177
|
+
# Update fence state for rendered portion
|
|
178
|
+
if last_safe is not None:
|
|
179
|
+
rendered = self._pending[:last_safe]
|
|
180
|
+
for line in rendered.split("\n"):
|
|
181
|
+
stripped = line.strip()
|
|
182
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
183
|
+
self._in_code_fence = not self._in_code_fence
|
|
184
|
+
|
|
185
|
+
return last_safe
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Tool display formatting
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def format_tool_call_start(tool_name: str, tool_input: str) -> str:
|
|
193
|
+
"""Format a tool invocation with box-drawing characters.
|
|
194
|
+
|
|
195
|
+
Maps to: rust/crates/rusty-claude-cli/src/main.rs::format_tool_call_start
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
params = json.loads(tool_input) if tool_input else {}
|
|
199
|
+
except json.JSONDecodeError:
|
|
200
|
+
params = {}
|
|
201
|
+
|
|
202
|
+
# Tool-specific formatting
|
|
203
|
+
detail = ""
|
|
204
|
+
match tool_name:
|
|
205
|
+
case "Bash":
|
|
206
|
+
cmd = params.get("command", "")
|
|
207
|
+
desc = params.get("description", "")
|
|
208
|
+
detail = f" $ {cmd}" if cmd else ""
|
|
209
|
+
if desc:
|
|
210
|
+
detail = f" {desc}\n{detail}"
|
|
211
|
+
case "Read":
|
|
212
|
+
path = params.get("file_path", "")
|
|
213
|
+
offset = params.get("offset", "")
|
|
214
|
+
limit = params.get("limit", "")
|
|
215
|
+
detail = f" 📄 {path}"
|
|
216
|
+
if offset or limit:
|
|
217
|
+
detail += f" (lines {offset or 1}-{(offset or 0) + (limit or '?')})"
|
|
218
|
+
case "Write":
|
|
219
|
+
path = params.get("file_path", "")
|
|
220
|
+
content = params.get("content", "")
|
|
221
|
+
lines = content.count("\n") + 1 if content else 0
|
|
222
|
+
detail = f" ✏️ {path} ({lines} lines)"
|
|
223
|
+
case "Edit":
|
|
224
|
+
path = params.get("file_path", "")
|
|
225
|
+
old = params.get("old_string", "")[:60]
|
|
226
|
+
new = params.get("new_string", "")[:60]
|
|
227
|
+
detail = f" 📝 {path}\n - {old!r}\n + {new!r}"
|
|
228
|
+
case "Glob":
|
|
229
|
+
pattern = params.get("pattern", "")
|
|
230
|
+
path = params.get("path", ".")
|
|
231
|
+
detail = f" 🔍 {pattern} in {path}"
|
|
232
|
+
case "Grep":
|
|
233
|
+
pattern = params.get("pattern", "")
|
|
234
|
+
path = params.get("path", ".")
|
|
235
|
+
detail = f" 🔎 /{pattern}/ in {path}"
|
|
236
|
+
case "Agent":
|
|
237
|
+
desc = params.get("description", "")
|
|
238
|
+
detail = f" 🤖 {desc}" if desc else ""
|
|
239
|
+
case _:
|
|
240
|
+
# Generic tool display
|
|
241
|
+
if params:
|
|
242
|
+
summary = ", ".join(f"{k}={v!r}" for k, v in list(params.items())[:3])
|
|
243
|
+
if len(summary) > 100:
|
|
244
|
+
summary = summary[:100] + "..."
|
|
245
|
+
detail = f" {summary}"
|
|
246
|
+
|
|
247
|
+
header = f"╭─ {tool_name} "
|
|
248
|
+
border_len = max(60 - len(header), 4)
|
|
249
|
+
header += "─" * border_len + "╮"
|
|
250
|
+
|
|
251
|
+
lines = [header]
|
|
252
|
+
if detail:
|
|
253
|
+
for line in detail.split("\n"):
|
|
254
|
+
lines.append(f"│ {line}")
|
|
255
|
+
|
|
256
|
+
return "\n".join(lines)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def format_tool_result(
|
|
260
|
+
tool_name: str, output: str, is_error: bool = False
|
|
261
|
+
) -> str:
|
|
262
|
+
"""Format tool execution result.
|
|
263
|
+
|
|
264
|
+
Maps to: rust/crates/rusty-claude-cli/src/main.rs::format_tool_result
|
|
265
|
+
"""
|
|
266
|
+
icon = "✗" if is_error else "✓"
|
|
267
|
+
status = "error" if is_error else "success"
|
|
268
|
+
|
|
269
|
+
# Truncate output
|
|
270
|
+
display = output
|
|
271
|
+
truncated = False
|
|
272
|
+
output_lines = display.split("\n")
|
|
273
|
+
|
|
274
|
+
if len(output_lines) > TOOL_OUTPUT_DISPLAY_MAX_LINES:
|
|
275
|
+
output_lines = output_lines[:TOOL_OUTPUT_DISPLAY_MAX_LINES]
|
|
276
|
+
truncated = True
|
|
277
|
+
display = "\n".join(output_lines)
|
|
278
|
+
|
|
279
|
+
if len(display) > TOOL_OUTPUT_DISPLAY_MAX_CHARS:
|
|
280
|
+
display = display[:TOOL_OUTPUT_DISPLAY_MAX_CHARS]
|
|
281
|
+
truncated = True
|
|
282
|
+
|
|
283
|
+
# Tool-specific result formatting
|
|
284
|
+
match tool_name:
|
|
285
|
+
case "Read":
|
|
286
|
+
line_count = display.count("\n") + 1
|
|
287
|
+
if line_count > READ_DISPLAY_MAX_LINES:
|
|
288
|
+
display_lines = display.split("\n")[:READ_DISPLAY_MAX_LINES]
|
|
289
|
+
display = "\n".join(display_lines)
|
|
290
|
+
truncated = True
|
|
291
|
+
case "Glob":
|
|
292
|
+
# Show file count
|
|
293
|
+
pass
|
|
294
|
+
case "Grep":
|
|
295
|
+
# Show match count
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
result = f"│ {icon} {tool_name} ({status})"
|
|
299
|
+
if display.strip():
|
|
300
|
+
# Indent output
|
|
301
|
+
indented = "\n".join(f"│ {line}" for line in display.split("\n"))
|
|
302
|
+
result += f"\n{indented}"
|
|
303
|
+
|
|
304
|
+
if truncated:
|
|
305
|
+
result += "\n│ ... (output truncated)"
|
|
306
|
+
|
|
307
|
+
result += f"\n╰{'─' * 58}╯"
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
# Terminal renderer
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
class TerminalRenderer:
|
|
316
|
+
"""Renders conversation output to the terminal using Rich.
|
|
317
|
+
|
|
318
|
+
Maps to: rust/crates/rusty-claude-cli/src/render.rs::TerminalRenderer
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
def __init__(self, console: Console | None = None) -> None:
|
|
322
|
+
self.console = console or Console(theme=CLAW_THEME)
|
|
323
|
+
|
|
324
|
+
def render_markdown(self, text: str) -> None:
|
|
325
|
+
"""Render markdown text to the terminal."""
|
|
326
|
+
md = Markdown(text, code_theme="monokai")
|
|
327
|
+
self.console.print(md)
|
|
328
|
+
|
|
329
|
+
def render_code(self, code: str, language: str = "python") -> None:
|
|
330
|
+
"""Render syntax-highlighted code."""
|
|
331
|
+
syntax = Syntax(code, language, theme="monokai", line_numbers=True)
|
|
332
|
+
self.console.print(syntax)
|
|
333
|
+
|
|
334
|
+
def render_text(self, text: str, style: str = "") -> None:
|
|
335
|
+
"""Render plain text with optional style."""
|
|
336
|
+
self.console.print(text, style=style, highlight=False)
|
|
337
|
+
|
|
338
|
+
def render_streaming_text(self, text: str) -> None:
|
|
339
|
+
"""Render streaming text (no newline)."""
|
|
340
|
+
self.console.print(text, end="", highlight=False)
|
|
341
|
+
|
|
342
|
+
def render_tool_call(self, tool_name: str, tool_input: str) -> None:
|
|
343
|
+
"""Render a tool invocation with box-drawing border."""
|
|
344
|
+
formatted = format_tool_call_start(tool_name, tool_input)
|
|
345
|
+
self.console.print(f"[axion.tool_border]{formatted}[/axion.tool_border]")
|
|
346
|
+
|
|
347
|
+
def render_tool_result(self, tool_name: str, output: str, is_error: bool = False) -> None:
|
|
348
|
+
"""Render tool execution result."""
|
|
349
|
+
formatted = format_tool_result(tool_name, output, is_error)
|
|
350
|
+
style = "axion.tool_error" if is_error else "axion.tool_border"
|
|
351
|
+
self.console.print(f"[{style}]{formatted}[/{style}]")
|
|
352
|
+
|
|
353
|
+
def render_tool_use_simple(self, tool_name: str, tool_input: str) -> None:
|
|
354
|
+
"""Render a simple tool use indicator (no box)."""
|
|
355
|
+
self.console.print(f"[axion.tool_name]⚡ {tool_name}[/axion.tool_name]")
|
|
356
|
+
if tool_input:
|
|
357
|
+
display = tool_input[:500] + "..." if len(tool_input) > 500 else tool_input
|
|
358
|
+
self.console.print(f" [axion.tool_input]{display}[/axion.tool_input]")
|
|
359
|
+
|
|
360
|
+
def render_tool_result_simple(self, output: str, is_error: bool = False) -> None:
|
|
361
|
+
"""Render a simple tool result (no box)."""
|
|
362
|
+
if is_error:
|
|
363
|
+
self.console.print(f"[axion.tool_error]✗ {output}[/axion.tool_error]")
|
|
364
|
+
else:
|
|
365
|
+
display = output[:1000] + "\n... (truncated)" if len(output) > 1000 else output
|
|
366
|
+
self.console.print(f"[axion.tool_output]{display}[/axion.tool_output]")
|
|
367
|
+
|
|
368
|
+
def render_thinking(self, text: str) -> None:
|
|
369
|
+
"""Render collapsed thinking output."""
|
|
370
|
+
preview = text[:100] + "..." if len(text) > 100 else text
|
|
371
|
+
self.console.print(f"[axion.thinking]💭 Thinking: {preview}[/axion.thinking]")
|
|
372
|
+
|
|
373
|
+
def render_error(self, message: str) -> None:
|
|
374
|
+
"""Render an error message."""
|
|
375
|
+
self.console.print(f"[axion.error]Error: {message}[/axion.error]")
|
|
376
|
+
|
|
377
|
+
def render_context_window_error(
|
|
378
|
+
self,
|
|
379
|
+
model: str,
|
|
380
|
+
estimated_tokens: int,
|
|
381
|
+
context_window: int,
|
|
382
|
+
session_id: str | None = None,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Render a context window exceeded error with details."""
|
|
385
|
+
pct = (estimated_tokens / context_window * 100) if context_window > 0 else 0
|
|
386
|
+
lines = [
|
|
387
|
+
"[axion.error]Context window blocked[/axion.error]",
|
|
388
|
+
f" Model: {model}",
|
|
389
|
+
f" Estimated tokens: {estimated_tokens:,}",
|
|
390
|
+
f" Context window: {context_window:,}",
|
|
391
|
+
f" Usage: {pct:.0f}%",
|
|
392
|
+
]
|
|
393
|
+
if session_id:
|
|
394
|
+
lines.append(f" Session: {session_id}")
|
|
395
|
+
lines.append(" Try /compact to reduce history or /clear to start fresh.")
|
|
396
|
+
self.console.print("\n".join(lines))
|
|
397
|
+
|
|
398
|
+
def render_warning(self, message: str) -> None:
|
|
399
|
+
"""Render a warning message."""
|
|
400
|
+
self.console.print(f"[axion.warning]Warning: {message}[/axion.warning]")
|
|
401
|
+
|
|
402
|
+
def render_info(self, message: str) -> None:
|
|
403
|
+
"""Render an info message."""
|
|
404
|
+
self.console.print(f"[axion.info]{message}[/axion.info]")
|
|
405
|
+
|
|
406
|
+
def render_cost(self, cost_line: str) -> None:
|
|
407
|
+
"""Render cost information."""
|
|
408
|
+
self.console.print(f"[axion.cost]{cost_line}[/axion.cost]")
|
|
409
|
+
|
|
410
|
+
def render_separator(self) -> None:
|
|
411
|
+
"""Render a horizontal separator."""
|
|
412
|
+
self.console.rule(style="dim")
|
|
413
|
+
|
|
414
|
+
def render_welcome(self, version: str, model: str) -> None:
|
|
415
|
+
"""Render the welcome banner."""
|
|
416
|
+
self.console.print(f"\n[bold]🐍 Axion Code[/bold] v{version}")
|
|
417
|
+
self.console.print(f"[dim]Model: {model}[/dim]")
|
|
418
|
+
self.console.print("[dim]Type /help for commands, Ctrl+C to interrupt[/dim]\n")
|
|
419
|
+
|
|
420
|
+
def render_status_report(
|
|
421
|
+
self,
|
|
422
|
+
model: str,
|
|
423
|
+
permission_mode: str,
|
|
424
|
+
message_count: int,
|
|
425
|
+
turn_count: int,
|
|
426
|
+
session_id: str,
|
|
427
|
+
cwd: str,
|
|
428
|
+
git_branch: str | None = None,
|
|
429
|
+
estimated_tokens: int = 0,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Render a full status report."""
|
|
432
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
433
|
+
table.add_column(style="bold")
|
|
434
|
+
table.add_column()
|
|
435
|
+
table.add_row("Model", model)
|
|
436
|
+
table.add_row("Permissions", permission_mode)
|
|
437
|
+
table.add_row("Messages", str(message_count))
|
|
438
|
+
table.add_row("Turns", str(turn_count))
|
|
439
|
+
table.add_row("Est. Tokens", f"{estimated_tokens:,}")
|
|
440
|
+
table.add_row("Session", session_id)
|
|
441
|
+
table.add_row("Working Dir", cwd)
|
|
442
|
+
if git_branch:
|
|
443
|
+
table.add_row("Git Branch", git_branch)
|
|
444
|
+
self.console.print(Panel(table, title="Status", border_style="dim"))
|
|
445
|
+
|
|
446
|
+
def render_session_list(self, sessions: list[dict[str, Any]]) -> None:
|
|
447
|
+
"""Render a list of sessions."""
|
|
448
|
+
if not sessions:
|
|
449
|
+
self.console.print("[dim]No sessions found.[/dim]")
|
|
450
|
+
return
|
|
451
|
+
table = Table(title="Sessions", show_lines=False)
|
|
452
|
+
table.add_column("ID", style="cyan")
|
|
453
|
+
table.add_column("Messages", justify="right")
|
|
454
|
+
table.add_column("Modified")
|
|
455
|
+
table.add_column("Branch")
|
|
456
|
+
for s in sessions:
|
|
457
|
+
table.add_row(
|
|
458
|
+
s.get("id", "?"),
|
|
459
|
+
str(s.get("message_count", 0)),
|
|
460
|
+
s.get("modified", "?"),
|
|
461
|
+
s.get("branch", ""),
|
|
462
|
+
)
|
|
463
|
+
self.console.print(table)
|
|
464
|
+
|
|
465
|
+
def render_json_output(self, data: dict[str, Any]) -> None:
|
|
466
|
+
"""Render structured JSON output."""
|
|
467
|
+
self.console.print_json(json.dumps(data, indent=2, default=str))
|
|
468
|
+
|
|
469
|
+
def render_permission_prompt(
|
|
470
|
+
self, tool_name: str, current_mode: str, required_mode: str, reason: str = ""
|
|
471
|
+
) -> None:
|
|
472
|
+
"""Render a permission approval request."""
|
|
473
|
+
self.console.print()
|
|
474
|
+
self.console.print("[bold yellow]Permission approval required[/bold yellow]")
|
|
475
|
+
self.console.print(f" Tool: [bold]{tool_name}[/bold]")
|
|
476
|
+
self.console.print(f" Current mode: {current_mode}")
|
|
477
|
+
self.console.print(f" Required mode: {required_mode}")
|
|
478
|
+
if reason:
|
|
479
|
+
self.console.print(f" Reason: {reason}")
|
|
480
|
+
|
|
481
|
+
def render_auto_compaction_notice(self, removed_count: int) -> None:
|
|
482
|
+
"""Render auto-compaction notice."""
|
|
483
|
+
self.console.print(
|
|
484
|
+
f"[dim]Auto-compacted: removed {removed_count} messages to stay within context window.[/dim]"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def render_export_success(self, path: str) -> None:
|
|
488
|
+
"""Render export success message."""
|
|
489
|
+
self.console.print(f"[green]Transcript exported to: {path}[/green]")
|