superqode 0.1.5__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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Log Formatter for SuperQode.
|
|
3
|
+
|
|
4
|
+
Converts LogEntry objects into Rich renderables with consistent styling
|
|
5
|
+
across all provider modes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from rich.console import RenderableType, Group
|
|
14
|
+
from rich.markdown import Markdown
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
from rich.box import ROUNDED
|
|
19
|
+
|
|
20
|
+
from superqode.logging.unified_log import LogConfig, LogEntry, LogVerbosity
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Theme:
|
|
24
|
+
"""Unified theme colors."""
|
|
25
|
+
|
|
26
|
+
# Primary
|
|
27
|
+
purple = "#a855f7"
|
|
28
|
+
magenta = "#d946ef"
|
|
29
|
+
pink = "#ec4899"
|
|
30
|
+
cyan = "#06b6d4"
|
|
31
|
+
green = "#22c55e"
|
|
32
|
+
orange = "#f97316"
|
|
33
|
+
gold = "#fbbf24"
|
|
34
|
+
blue = "#3b82f6"
|
|
35
|
+
|
|
36
|
+
# Status
|
|
37
|
+
success = "#22c55e"
|
|
38
|
+
error = "#ef4444"
|
|
39
|
+
warning = "#f59e0b"
|
|
40
|
+
info = "#06b6d4"
|
|
41
|
+
|
|
42
|
+
# Text
|
|
43
|
+
text = "#e4e4e7"
|
|
44
|
+
muted = "#71717a"
|
|
45
|
+
dim = "#52525b"
|
|
46
|
+
|
|
47
|
+
# Background
|
|
48
|
+
bg = "#0a0a0a"
|
|
49
|
+
bg_surface = "#111111"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Thought category icons
|
|
53
|
+
THOUGHT_ICONS = {
|
|
54
|
+
"planning": "📋",
|
|
55
|
+
"analyzing": "🔬",
|
|
56
|
+
"deciding": "🤔",
|
|
57
|
+
"searching": "🔍",
|
|
58
|
+
"reading": "📖",
|
|
59
|
+
"writing": "✏️",
|
|
60
|
+
"debugging": "🐛",
|
|
61
|
+
"executing": "⚡",
|
|
62
|
+
"verifying": "✅",
|
|
63
|
+
"testing": "🧪",
|
|
64
|
+
"refactoring": "🔧",
|
|
65
|
+
"general": "💭",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Tool icons
|
|
69
|
+
TOOL_ICONS = {
|
|
70
|
+
"read": "↳",
|
|
71
|
+
"write": "↲",
|
|
72
|
+
"edit": "⟳",
|
|
73
|
+
"shell": "▸",
|
|
74
|
+
"bash": "▸",
|
|
75
|
+
"search": "⌕",
|
|
76
|
+
"glob": "⋮",
|
|
77
|
+
"grep": "⌕",
|
|
78
|
+
"todo": "📋",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Status indicators
|
|
82
|
+
STATUS_ICONS = {
|
|
83
|
+
"pending": ("○", Theme.muted),
|
|
84
|
+
"running": ("◐", Theme.purple),
|
|
85
|
+
"success": ("✦", Theme.success),
|
|
86
|
+
"error": ("✕", Theme.error),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Language detection for syntax highlighting
|
|
90
|
+
LANGUAGE_PATTERNS = {
|
|
91
|
+
r"\.py$": "python",
|
|
92
|
+
r"\.js$": "javascript",
|
|
93
|
+
r"\.ts$": "typescript",
|
|
94
|
+
r"\.jsx$": "jsx",
|
|
95
|
+
r"\.tsx$": "tsx",
|
|
96
|
+
r"\.rs$": "rust",
|
|
97
|
+
r"\.go$": "go",
|
|
98
|
+
r"\.java$": "java",
|
|
99
|
+
r"\.rb$": "ruby",
|
|
100
|
+
r"\.sh$": "bash",
|
|
101
|
+
r"\.bash$": "bash",
|
|
102
|
+
r"\.zsh$": "zsh",
|
|
103
|
+
r"\.json$": "json",
|
|
104
|
+
r"\.yaml$": "yaml",
|
|
105
|
+
r"\.yml$": "yaml",
|
|
106
|
+
r"\.toml$": "toml",
|
|
107
|
+
r"\.md$": "markdown",
|
|
108
|
+
r"\.html$": "html",
|
|
109
|
+
r"\.css$": "css",
|
|
110
|
+
r"\.sql$": "sql",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def detect_language(file_path: str = "", content: str = "") -> str:
|
|
115
|
+
"""Detect programming language from file path or content."""
|
|
116
|
+
# Try file path first
|
|
117
|
+
if file_path:
|
|
118
|
+
for pattern, lang in LANGUAGE_PATTERNS.items():
|
|
119
|
+
if re.search(pattern, file_path, re.IGNORECASE):
|
|
120
|
+
return lang
|
|
121
|
+
|
|
122
|
+
# Try content heuristics
|
|
123
|
+
if content:
|
|
124
|
+
content_lower = content.lower()
|
|
125
|
+
if content.startswith("#!/bin/bash") or content.startswith("#!/bin/sh"):
|
|
126
|
+
return "bash"
|
|
127
|
+
if "def " in content and ":" in content:
|
|
128
|
+
return "python"
|
|
129
|
+
if "function " in content or "const " in content or "let " in content:
|
|
130
|
+
return "javascript"
|
|
131
|
+
if "fn " in content and "->" in content:
|
|
132
|
+
return "rust"
|
|
133
|
+
if "func " in content and "{" in content:
|
|
134
|
+
return "go"
|
|
135
|
+
|
|
136
|
+
return "text"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def classify_thought(text: str) -> str:
|
|
140
|
+
"""Classify thinking text into a category."""
|
|
141
|
+
text_lower = text.lower()
|
|
142
|
+
|
|
143
|
+
if any(w in text_lower for w in ["test", "pytest", "expect", "assert"]):
|
|
144
|
+
return "testing"
|
|
145
|
+
if any(w in text_lower for w in ["verify", "confirm", "check", "validate"]):
|
|
146
|
+
return "verifying"
|
|
147
|
+
if any(w in text_lower for w in ["run", "execute", "command", "shell"]):
|
|
148
|
+
return "executing"
|
|
149
|
+
if any(w in text_lower for w in ["debug", "error", "fix", "bug"]):
|
|
150
|
+
return "debugging"
|
|
151
|
+
if any(w in text_lower for w in ["plan", "step", "approach", "let me", "i'll"]):
|
|
152
|
+
return "planning"
|
|
153
|
+
if any(w in text_lower for w in ["analyze", "understand", "examine", "look at"]):
|
|
154
|
+
return "analyzing"
|
|
155
|
+
if any(w in text_lower for w in ["search", "find", "look for", "grep"]):
|
|
156
|
+
return "searching"
|
|
157
|
+
if any(w in text_lower for w in ["read", "file", "content"]):
|
|
158
|
+
return "reading"
|
|
159
|
+
if any(w in text_lower for w in ["write", "create", "add"]):
|
|
160
|
+
return "writing"
|
|
161
|
+
if any(w in text_lower for w in ["refactor", "restructure", "clean"]):
|
|
162
|
+
return "refactoring"
|
|
163
|
+
|
|
164
|
+
return "general"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def truncate(text: str, max_len: int, suffix: str = "...") -> str:
|
|
168
|
+
"""Truncate text to max length."""
|
|
169
|
+
if len(text) <= max_len:
|
|
170
|
+
return text
|
|
171
|
+
return text[: max_len - len(suffix)] + suffix
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class UnifiedLogFormatter:
|
|
175
|
+
"""
|
|
176
|
+
Formats LogEntry objects into Rich renderables.
|
|
177
|
+
|
|
178
|
+
Provides consistent formatting across all provider modes with
|
|
179
|
+
support for different verbosity levels.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, config: Optional[LogConfig] = None):
|
|
183
|
+
self.config = config or LogConfig.normal()
|
|
184
|
+
|
|
185
|
+
def format(self, entry: LogEntry) -> Optional[RenderableType]:
|
|
186
|
+
"""Format a log entry into a Rich renderable."""
|
|
187
|
+
handlers = {
|
|
188
|
+
"thinking": self._format_thinking,
|
|
189
|
+
"tool_call": self._format_tool_call,
|
|
190
|
+
"tool_result": self._format_tool_result,
|
|
191
|
+
"tool_update": self._format_tool_update,
|
|
192
|
+
"response_delta": self._format_response_delta,
|
|
193
|
+
"response_final": self._format_response_final,
|
|
194
|
+
"code_block": self._format_code_block,
|
|
195
|
+
"info": self._format_info,
|
|
196
|
+
"warning": self._format_warning,
|
|
197
|
+
"error": self._format_error,
|
|
198
|
+
"system": self._format_system,
|
|
199
|
+
"user": self._format_user,
|
|
200
|
+
"assistant": self._format_assistant,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
handler = handlers.get(entry.kind)
|
|
204
|
+
if handler:
|
|
205
|
+
return handler(entry)
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def _format_thinking(self, entry: LogEntry) -> RenderableType:
|
|
209
|
+
"""Format thinking/reasoning entry with engaging display."""
|
|
210
|
+
text = entry.text.strip()
|
|
211
|
+
if not text:
|
|
212
|
+
return Text("")
|
|
213
|
+
|
|
214
|
+
# Get category and icon
|
|
215
|
+
category = entry.data.get("category", classify_thought(text))
|
|
216
|
+
icon = THOUGHT_ICONS.get(category, "💭")
|
|
217
|
+
|
|
218
|
+
# Clean up common prefixes that make thoughts less readable
|
|
219
|
+
cleaned_text = text
|
|
220
|
+
skip_prefixes = [
|
|
221
|
+
"I need to ",
|
|
222
|
+
"I should ",
|
|
223
|
+
"I will ",
|
|
224
|
+
"I'll ",
|
|
225
|
+
"I'm going to ",
|
|
226
|
+
"Let me ",
|
|
227
|
+
"Now I ",
|
|
228
|
+
"Next I ",
|
|
229
|
+
"First, I ",
|
|
230
|
+
"Then I ",
|
|
231
|
+
]
|
|
232
|
+
for prefix in skip_prefixes:
|
|
233
|
+
if cleaned_text.lower().startswith(prefix.lower()):
|
|
234
|
+
# Keep first letter capitalized after removing prefix
|
|
235
|
+
cleaned_text = cleaned_text[len(prefix) :]
|
|
236
|
+
if cleaned_text:
|
|
237
|
+
cleaned_text = cleaned_text[0].upper() + cleaned_text[1:]
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
# Truncate if needed
|
|
241
|
+
if self.config.verbosity == LogVerbosity.MINIMAL:
|
|
242
|
+
cleaned_text = truncate(cleaned_text, 50)
|
|
243
|
+
elif self.config.verbosity == LogVerbosity.NORMAL:
|
|
244
|
+
cleaned_text = truncate(cleaned_text, self.config.max_thinking_chars)
|
|
245
|
+
|
|
246
|
+
# Category-based styling for more engaging display
|
|
247
|
+
category_styles = {
|
|
248
|
+
"planning": (Theme.cyan, "Planning: "),
|
|
249
|
+
"analyzing": (Theme.blue, "Analyzing: "),
|
|
250
|
+
"debugging": (Theme.warning, "Debugging: "),
|
|
251
|
+
"testing": (Theme.green, "Testing: "),
|
|
252
|
+
"verifying": (Theme.green, "Verifying: "),
|
|
253
|
+
"searching": (Theme.purple, ""),
|
|
254
|
+
"reading": (Theme.purple, ""),
|
|
255
|
+
"writing": (Theme.magenta, ""),
|
|
256
|
+
"executing": (Theme.orange, ""),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
style_info = category_styles.get(category)
|
|
260
|
+
|
|
261
|
+
line = Text()
|
|
262
|
+
line.append(f" {icon} ", style=f"bold {Theme.pink}")
|
|
263
|
+
|
|
264
|
+
if style_info and style_info[1]:
|
|
265
|
+
# Add category prefix for certain types
|
|
266
|
+
line.append(style_info[1], style=f"bold {style_info[0]}")
|
|
267
|
+
line.append(cleaned_text, style=f"italic {Theme.muted}")
|
|
268
|
+
else:
|
|
269
|
+
line.append(cleaned_text, style=f"italic {Theme.muted}")
|
|
270
|
+
|
|
271
|
+
return line
|
|
272
|
+
|
|
273
|
+
def _format_tool_call(self, entry: LogEntry) -> RenderableType:
|
|
274
|
+
"""Format tool call entry with action-oriented display."""
|
|
275
|
+
name = entry.tool_name
|
|
276
|
+
args = entry.tool_args
|
|
277
|
+
file_path = entry.file_path
|
|
278
|
+
command = entry.command
|
|
279
|
+
|
|
280
|
+
# Map tool names to action verbs
|
|
281
|
+
action_verbs = {
|
|
282
|
+
"read": ("📖 Reading", Theme.cyan),
|
|
283
|
+
"write": ("✏️ Writing", Theme.magenta),
|
|
284
|
+
"edit": ("🔧 Editing", Theme.orange),
|
|
285
|
+
"bash": ("▸ Running", Theme.green),
|
|
286
|
+
"shell": ("▸ Running", Theme.green),
|
|
287
|
+
"search": ("🔍 Searching", Theme.purple),
|
|
288
|
+
"grep": ("🔍 Searching", Theme.purple),
|
|
289
|
+
"glob": ("📂 Finding", Theme.blue),
|
|
290
|
+
"todo": ("📋 Updating", Theme.cyan),
|
|
291
|
+
"task": ("📝 Managing", Theme.purple),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Find matching action
|
|
295
|
+
action_text = None
|
|
296
|
+
action_style = Theme.purple
|
|
297
|
+
name_lower = name.lower()
|
|
298
|
+
for key, (verb, style) in action_verbs.items():
|
|
299
|
+
if key in name_lower:
|
|
300
|
+
action_text = verb
|
|
301
|
+
action_style = style
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
if not action_text:
|
|
305
|
+
action_text = f"◐ {name}"
|
|
306
|
+
|
|
307
|
+
line = Text()
|
|
308
|
+
line.append(f" {action_text}", style=f"bold {action_style}")
|
|
309
|
+
|
|
310
|
+
# Add context based on verbosity
|
|
311
|
+
if self.config.verbosity == LogVerbosity.MINIMAL:
|
|
312
|
+
pass # Just action
|
|
313
|
+
elif file_path:
|
|
314
|
+
# Shorten path for display
|
|
315
|
+
display_path = file_path
|
|
316
|
+
if len(display_path) > 50:
|
|
317
|
+
display_path = "..." + display_path[-47:]
|
|
318
|
+
line.append(f" {display_path}", style=Theme.muted)
|
|
319
|
+
elif command:
|
|
320
|
+
cmd_display = truncate(
|
|
321
|
+
command, 50 if self.config.verbosity == LogVerbosity.NORMAL else 100
|
|
322
|
+
)
|
|
323
|
+
line.append(f" $ {cmd_display}", style=Theme.muted)
|
|
324
|
+
elif self.config.show_tool_args and args:
|
|
325
|
+
# Show most relevant arg
|
|
326
|
+
relevant_arg = None
|
|
327
|
+
for key in ("path", "file_path", "pattern", "query", "command", "content"):
|
|
328
|
+
if key in args:
|
|
329
|
+
val = str(args[key])
|
|
330
|
+
if len(val) > 40:
|
|
331
|
+
val = val[:37] + "..."
|
|
332
|
+
relevant_arg = val
|
|
333
|
+
break
|
|
334
|
+
if relevant_arg:
|
|
335
|
+
line.append(f" {relevant_arg}", style=Theme.muted)
|
|
336
|
+
elif self.config.verbosity == LogVerbosity.VERBOSE:
|
|
337
|
+
args_str = str(args)[:80]
|
|
338
|
+
line.append(f" {args_str}", style=Theme.dim)
|
|
339
|
+
|
|
340
|
+
return line
|
|
341
|
+
|
|
342
|
+
def _format_tool_result(self, entry: LogEntry) -> RenderableType:
|
|
343
|
+
"""Format tool result entry."""
|
|
344
|
+
name = entry.tool_name
|
|
345
|
+
success = entry.is_success
|
|
346
|
+
result = entry.tool_result_text
|
|
347
|
+
|
|
348
|
+
# Get tool icon
|
|
349
|
+
tool_icon = "•"
|
|
350
|
+
for key, icon in TOOL_ICONS.items():
|
|
351
|
+
if key in name.lower():
|
|
352
|
+
tool_icon = icon
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
status = "success" if success else "error"
|
|
356
|
+
status_icon, status_color = STATUS_ICONS.get(status, ("●", Theme.muted))
|
|
357
|
+
|
|
358
|
+
line = Text()
|
|
359
|
+
line.append(f" {status_icon} ", style=f"bold {status_color}")
|
|
360
|
+
line.append(f"{tool_icon} ", style=Theme.dim)
|
|
361
|
+
line.append(name, style=Theme.text)
|
|
362
|
+
|
|
363
|
+
# Add result based on verbosity
|
|
364
|
+
if self.config.verbosity == LogVerbosity.MINIMAL:
|
|
365
|
+
line.append(" done" if success else " failed", style=Theme.muted)
|
|
366
|
+
elif self.config.show_tool_result and result:
|
|
367
|
+
# Try to format as structured JSON first
|
|
368
|
+
formatted_json = self._format_json_result(name, result)
|
|
369
|
+
if formatted_json:
|
|
370
|
+
return Group(line, formatted_json)
|
|
371
|
+
|
|
372
|
+
# Regular result display
|
|
373
|
+
max_chars = self.config.max_tool_output_chars
|
|
374
|
+
result_display = truncate(result, max_chars)
|
|
375
|
+
|
|
376
|
+
# Check if result looks like code
|
|
377
|
+
if self._looks_like_code(result_display):
|
|
378
|
+
lang = detect_language(entry.file_path, result_display)
|
|
379
|
+
if self.config.syntax_highlight:
|
|
380
|
+
code_result = Syntax(
|
|
381
|
+
result_display,
|
|
382
|
+
lang,
|
|
383
|
+
theme=self.config.code_theme,
|
|
384
|
+
word_wrap=True,
|
|
385
|
+
line_numbers=False,
|
|
386
|
+
)
|
|
387
|
+
return Group(line, Text(" → ", style=Theme.muted), code_result)
|
|
388
|
+
|
|
389
|
+
if result_display:
|
|
390
|
+
line.append(f"\n → {result_display}", style=Theme.muted)
|
|
391
|
+
|
|
392
|
+
return line
|
|
393
|
+
|
|
394
|
+
def _format_tool_update(self, entry: LogEntry) -> RenderableType:
|
|
395
|
+
"""Format tool update entry."""
|
|
396
|
+
# Similar to tool_call but for progress updates
|
|
397
|
+
return self._format_tool_call(entry)
|
|
398
|
+
|
|
399
|
+
def _format_response_delta(self, entry: LogEntry) -> RenderableType:
|
|
400
|
+
"""Format streaming response chunk."""
|
|
401
|
+
text = entry.text
|
|
402
|
+
if not text:
|
|
403
|
+
return Text("")
|
|
404
|
+
|
|
405
|
+
# For streaming, just show plain text (final will render markdown)
|
|
406
|
+
return Text(text, style=Theme.text)
|
|
407
|
+
|
|
408
|
+
def _format_response_final(self, entry: LogEntry) -> RenderableType:
|
|
409
|
+
"""Format final complete response."""
|
|
410
|
+
text = entry.text
|
|
411
|
+
if not text:
|
|
412
|
+
return Text("")
|
|
413
|
+
|
|
414
|
+
# Render as markdown with syntax highlighting for code blocks
|
|
415
|
+
if "```" in text:
|
|
416
|
+
return Markdown(
|
|
417
|
+
text,
|
|
418
|
+
code_theme=self.config.code_theme,
|
|
419
|
+
inline_code_lexer="python",
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return Text(text, style=Theme.text)
|
|
423
|
+
|
|
424
|
+
def _format_code_block(self, entry: LogEntry) -> RenderableType:
|
|
425
|
+
"""Format a code block with syntax highlighting."""
|
|
426
|
+
code = entry.text
|
|
427
|
+
language = entry.data.get("language", "")
|
|
428
|
+
|
|
429
|
+
if not language:
|
|
430
|
+
language = detect_language(content=code)
|
|
431
|
+
|
|
432
|
+
if self.config.syntax_highlight:
|
|
433
|
+
return Syntax(
|
|
434
|
+
code,
|
|
435
|
+
language,
|
|
436
|
+
theme=self.config.code_theme,
|
|
437
|
+
word_wrap=True,
|
|
438
|
+
line_numbers=True,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return Text(code, style=Theme.text)
|
|
442
|
+
|
|
443
|
+
def _format_info(self, entry: LogEntry) -> RenderableType:
|
|
444
|
+
"""Format info message."""
|
|
445
|
+
return Text(f" ℹ️ {entry.text}", style=Theme.cyan)
|
|
446
|
+
|
|
447
|
+
def _format_warning(self, entry: LogEntry) -> RenderableType:
|
|
448
|
+
"""Format warning message."""
|
|
449
|
+
return Text(f" ⚠️ {entry.text}", style=Theme.warning)
|
|
450
|
+
|
|
451
|
+
def _format_error(self, entry: LogEntry) -> RenderableType:
|
|
452
|
+
"""Format error message."""
|
|
453
|
+
return Text(f" ❌ {entry.text}", style=Theme.error)
|
|
454
|
+
|
|
455
|
+
def _format_system(self, entry: LogEntry) -> RenderableType:
|
|
456
|
+
"""Format system message."""
|
|
457
|
+
return Text(f" ✨ {entry.text}", style=f"italic {Theme.muted}")
|
|
458
|
+
|
|
459
|
+
def _format_user(self, entry: LogEntry) -> RenderableType:
|
|
460
|
+
"""Format user message."""
|
|
461
|
+
return Panel(
|
|
462
|
+
Text(entry.text, style=Theme.text, overflow="fold"),
|
|
463
|
+
title=f"[bold {Theme.cyan}]👩💻👨💻 >[/]",
|
|
464
|
+
border_style=Theme.dim,
|
|
465
|
+
box=ROUNDED,
|
|
466
|
+
padding=(0, 1),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def _format_assistant(self, entry: LogEntry) -> RenderableType:
|
|
470
|
+
"""Format assistant message."""
|
|
471
|
+
text = entry.text
|
|
472
|
+
agent = entry.agent or "Assistant"
|
|
473
|
+
|
|
474
|
+
content = (
|
|
475
|
+
Markdown(text, code_theme=self.config.code_theme)
|
|
476
|
+
if "```" in text
|
|
477
|
+
else Text(text, style=Theme.text, overflow="fold")
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return Panel(
|
|
481
|
+
content,
|
|
482
|
+
title=f"[bold {Theme.purple}]🤖 {agent}[/]",
|
|
483
|
+
border_style=Theme.purple,
|
|
484
|
+
box=ROUNDED,
|
|
485
|
+
padding=(0, 1),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def _format_json_result(self, tool_name: str, result: str) -> Optional[RenderableType]:
|
|
489
|
+
"""
|
|
490
|
+
Intelligently format JSON tool results into engaging displays.
|
|
491
|
+
|
|
492
|
+
Handles various JSON structures:
|
|
493
|
+
- TODO lists
|
|
494
|
+
- File lists (search results)
|
|
495
|
+
- Task definitions
|
|
496
|
+
- Error reports
|
|
497
|
+
- Generic objects/arrays
|
|
498
|
+
"""
|
|
499
|
+
import json
|
|
500
|
+
|
|
501
|
+
# Skip if not JSON
|
|
502
|
+
result_stripped = result.strip()
|
|
503
|
+
if not result_stripped or (
|
|
504
|
+
not result_stripped.startswith("{") and not result_stripped.startswith("[")
|
|
505
|
+
):
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
data = json.loads(result)
|
|
510
|
+
except json.JSONDecodeError:
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
# Route to specific formatters based on tool name or data structure
|
|
514
|
+
tool_lower = tool_name.lower()
|
|
515
|
+
|
|
516
|
+
# TODO/Task list
|
|
517
|
+
if "todo" in tool_lower or self._looks_like_todo_list(data):
|
|
518
|
+
return self._format_todo_list(data)
|
|
519
|
+
|
|
520
|
+
# File search results
|
|
521
|
+
if any(x in tool_lower for x in ("glob", "search", "find", "grep")):
|
|
522
|
+
return self._format_file_results(data)
|
|
523
|
+
|
|
524
|
+
# Read/Write file result
|
|
525
|
+
if any(x in tool_lower for x in ("read", "write", "edit")):
|
|
526
|
+
return self._format_file_operation(data)
|
|
527
|
+
|
|
528
|
+
# Task list (Claude Code style)
|
|
529
|
+
if "task" in tool_lower and isinstance(data, list):
|
|
530
|
+
return self._format_task_list(data)
|
|
531
|
+
|
|
532
|
+
# Error/diagnostic result
|
|
533
|
+
if isinstance(data, dict) and ("error" in data or "errors" in data):
|
|
534
|
+
return self._format_error_result(data)
|
|
535
|
+
|
|
536
|
+
# Plan entries
|
|
537
|
+
if isinstance(data, list) and data and isinstance(data[0], dict) and "step" in data[0]:
|
|
538
|
+
return self._format_plan_entries(data)
|
|
539
|
+
|
|
540
|
+
# Generic list of items with identifiable structure
|
|
541
|
+
if isinstance(data, list) and data:
|
|
542
|
+
return self._format_generic_list(data)
|
|
543
|
+
|
|
544
|
+
# Generic dict
|
|
545
|
+
if isinstance(data, dict):
|
|
546
|
+
return self._format_generic_dict(data)
|
|
547
|
+
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
def _looks_like_todo_list(self, data) -> bool:
|
|
551
|
+
"""Check if data looks like a TODO list."""
|
|
552
|
+
if not isinstance(data, list):
|
|
553
|
+
return False
|
|
554
|
+
if not data:
|
|
555
|
+
return False
|
|
556
|
+
first = data[0]
|
|
557
|
+
if not isinstance(first, dict):
|
|
558
|
+
return False
|
|
559
|
+
return any(k in first for k in ("status", "title", "priority", "completed"))
|
|
560
|
+
|
|
561
|
+
def _format_todo_list(self, data) -> Optional[RenderableType]:
|
|
562
|
+
"""Format TODO list with visual status indicators."""
|
|
563
|
+
if not isinstance(data, list):
|
|
564
|
+
if isinstance(data, dict) and "todos" in data:
|
|
565
|
+
data = data["todos"]
|
|
566
|
+
else:
|
|
567
|
+
return None
|
|
568
|
+
|
|
569
|
+
if not data:
|
|
570
|
+
return Text(" 📋 No tasks", style=Theme.muted)
|
|
571
|
+
|
|
572
|
+
lines = []
|
|
573
|
+
for item in data:
|
|
574
|
+
if not isinstance(item, dict):
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
status = item.get("status", "pending")
|
|
578
|
+
title = item.get("title", item.get("name", item.get("description", str(item))))
|
|
579
|
+
priority = item.get("priority", "normal")
|
|
580
|
+
|
|
581
|
+
# Status icons with colors
|
|
582
|
+
status_icons = {
|
|
583
|
+
"completed": ("✅", Theme.success),
|
|
584
|
+
"done": ("✅", Theme.success),
|
|
585
|
+
"in_progress": ("🔄", Theme.purple),
|
|
586
|
+
"active": ("🔄", Theme.purple),
|
|
587
|
+
"pending": ("○", Theme.muted),
|
|
588
|
+
"blocked": ("🚫", Theme.error),
|
|
589
|
+
"failed": ("✕", Theme.error),
|
|
590
|
+
}
|
|
591
|
+
icon, icon_style = status_icons.get(status, ("○", Theme.muted))
|
|
592
|
+
|
|
593
|
+
# Priority styling
|
|
594
|
+
title_style = Theme.text
|
|
595
|
+
if priority in ("high", "important"):
|
|
596
|
+
title_style = Theme.warning
|
|
597
|
+
elif priority in ("critical", "urgent"):
|
|
598
|
+
title_style = Theme.error
|
|
599
|
+
elif status in ("completed", "done"):
|
|
600
|
+
title_style = Theme.muted
|
|
601
|
+
|
|
602
|
+
line = Text()
|
|
603
|
+
line.append(f" {icon} ", style=icon_style)
|
|
604
|
+
line.append(str(title)[:80], style=title_style)
|
|
605
|
+
lines.append(line)
|
|
606
|
+
|
|
607
|
+
# Summary line
|
|
608
|
+
completed = sum(
|
|
609
|
+
1 for t in data if isinstance(t, dict) and t.get("status") in ("completed", "done")
|
|
610
|
+
)
|
|
611
|
+
in_progress = sum(
|
|
612
|
+
1 for t in data if isinstance(t, dict) and t.get("status") in ("in_progress", "active")
|
|
613
|
+
)
|
|
614
|
+
pending = sum(
|
|
615
|
+
1 for t in data if isinstance(t, dict) and t.get("status") in ("pending", None)
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
parts = []
|
|
619
|
+
if completed:
|
|
620
|
+
parts.append(f"✅ {completed}")
|
|
621
|
+
if in_progress:
|
|
622
|
+
parts.append(f"🔄 {in_progress}")
|
|
623
|
+
if pending:
|
|
624
|
+
parts.append(f"○ {pending}")
|
|
625
|
+
|
|
626
|
+
summary = Text()
|
|
627
|
+
summary.append(" 📋 ", style=Theme.cyan)
|
|
628
|
+
summary.append(f"Tasks: {' · '.join(parts) if parts else 'none'}", style=Theme.text)
|
|
629
|
+
|
|
630
|
+
return Group(summary, *lines[:10]) # Limit to 10 items
|
|
631
|
+
|
|
632
|
+
def _format_file_results(self, data) -> Optional[RenderableType]:
|
|
633
|
+
"""Format file search/glob results."""
|
|
634
|
+
files = []
|
|
635
|
+
|
|
636
|
+
if isinstance(data, list):
|
|
637
|
+
files = [str(f) for f in data if f]
|
|
638
|
+
elif isinstance(data, dict):
|
|
639
|
+
files = data.get("files", data.get("matches", data.get("results", [])))
|
|
640
|
+
if not isinstance(files, list):
|
|
641
|
+
return None
|
|
642
|
+
|
|
643
|
+
if not files:
|
|
644
|
+
return Text(" 🔍 No matches found", style=Theme.muted)
|
|
645
|
+
|
|
646
|
+
lines = []
|
|
647
|
+
for i, f in enumerate(files[:8]): # Show max 8 files
|
|
648
|
+
line = Text()
|
|
649
|
+
line.append(" ", style=Theme.dim)
|
|
650
|
+
# File icon based on extension
|
|
651
|
+
ext = str(f).split(".")[-1].lower() if "." in str(f) else ""
|
|
652
|
+
icon = "📄"
|
|
653
|
+
if ext in ("py", "pyw"):
|
|
654
|
+
icon = "🐍"
|
|
655
|
+
elif ext in ("js", "ts", "jsx", "tsx"):
|
|
656
|
+
icon = "📜"
|
|
657
|
+
elif ext in ("rs",):
|
|
658
|
+
icon = "🦀"
|
|
659
|
+
elif ext in ("go",):
|
|
660
|
+
icon = "🐹"
|
|
661
|
+
elif ext in ("md", "txt", "rst"):
|
|
662
|
+
icon = "📝"
|
|
663
|
+
elif ext in ("json", "yaml", "yml", "toml"):
|
|
664
|
+
icon = "⚙️"
|
|
665
|
+
|
|
666
|
+
line.append(f"{icon} ", style=Theme.dim)
|
|
667
|
+
# Truncate long paths
|
|
668
|
+
path_str = str(f)
|
|
669
|
+
if len(path_str) > 60:
|
|
670
|
+
path_str = "..." + path_str[-57:]
|
|
671
|
+
line.append(path_str, style=Theme.cyan)
|
|
672
|
+
lines.append(line)
|
|
673
|
+
|
|
674
|
+
if len(files) > 8:
|
|
675
|
+
more = Text()
|
|
676
|
+
more.append(f" ... and {len(files) - 8} more", style=Theme.muted)
|
|
677
|
+
lines.append(more)
|
|
678
|
+
|
|
679
|
+
header = Text()
|
|
680
|
+
header.append(" 🔍 ", style=Theme.cyan)
|
|
681
|
+
header.append(f"Found {len(files)} file{'s' if len(files) != 1 else ''}", style=Theme.text)
|
|
682
|
+
|
|
683
|
+
return Group(header, *lines)
|
|
684
|
+
|
|
685
|
+
def _format_file_operation(self, data) -> Optional[RenderableType]:
|
|
686
|
+
"""Format read/write/edit file results."""
|
|
687
|
+
if not isinstance(data, dict):
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
path = data.get("path", data.get("file", ""))
|
|
691
|
+
success = data.get("success", data.get("ok", True))
|
|
692
|
+
content = data.get("content", data.get("data", ""))
|
|
693
|
+
lines_affected = data.get("lines", data.get("lines_changed", 0))
|
|
694
|
+
|
|
695
|
+
result = Text()
|
|
696
|
+
result.append(" ", style=Theme.dim)
|
|
697
|
+
|
|
698
|
+
if success:
|
|
699
|
+
result.append("✓ ", style=Theme.success)
|
|
700
|
+
if path:
|
|
701
|
+
result.append(f"{path}", style=Theme.cyan)
|
|
702
|
+
if lines_affected:
|
|
703
|
+
result.append(f" ({lines_affected} lines)", style=Theme.muted)
|
|
704
|
+
else:
|
|
705
|
+
result.append("✕ ", style=Theme.error)
|
|
706
|
+
error = data.get("error", "Operation failed")
|
|
707
|
+
result.append(str(error)[:60], style=Theme.error)
|
|
708
|
+
|
|
709
|
+
return result
|
|
710
|
+
|
|
711
|
+
def _format_task_list(self, data) -> Optional[RenderableType]:
|
|
712
|
+
"""Format Claude Code style task lists."""
|
|
713
|
+
if not isinstance(data, list):
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
lines = []
|
|
717
|
+
for task in data[:5]: # Limit display
|
|
718
|
+
if not isinstance(task, dict):
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
status = task.get("status", "pending")
|
|
722
|
+
subject = task.get("subject", task.get("title", ""))
|
|
723
|
+
task_id = task.get("id", "")
|
|
724
|
+
|
|
725
|
+
icons = {
|
|
726
|
+
"completed": ("✅", Theme.success),
|
|
727
|
+
"in_progress": ("⏳", Theme.purple),
|
|
728
|
+
"pending": ("○", Theme.muted),
|
|
729
|
+
}
|
|
730
|
+
icon, style = icons.get(status, ("○", Theme.muted))
|
|
731
|
+
|
|
732
|
+
line = Text()
|
|
733
|
+
line.append(f" {icon} ", style=style)
|
|
734
|
+
if task_id:
|
|
735
|
+
line.append(f"[{task_id}] ", style=Theme.dim)
|
|
736
|
+
line.append(
|
|
737
|
+
str(subject)[:60], style=Theme.text if status != "completed" else Theme.muted
|
|
738
|
+
)
|
|
739
|
+
lines.append(line)
|
|
740
|
+
|
|
741
|
+
header = Text()
|
|
742
|
+
header.append(" 📝 ", style=Theme.purple)
|
|
743
|
+
header.append(f"{len(data)} task{'s' if len(data) != 1 else ''}", style=Theme.text)
|
|
744
|
+
|
|
745
|
+
return Group(header, *lines)
|
|
746
|
+
|
|
747
|
+
def _format_error_result(self, data) -> Optional[RenderableType]:
|
|
748
|
+
"""Format error/diagnostic results."""
|
|
749
|
+
errors = data.get("errors", [data.get("error")]) if isinstance(data, dict) else []
|
|
750
|
+
if not errors:
|
|
751
|
+
return None
|
|
752
|
+
|
|
753
|
+
lines = []
|
|
754
|
+
for err in errors[:5]:
|
|
755
|
+
line = Text()
|
|
756
|
+
line.append(" ✕ ", style=Theme.error)
|
|
757
|
+
if isinstance(err, dict):
|
|
758
|
+
msg = err.get("message", err.get("error", str(err)))
|
|
759
|
+
loc = err.get("location", err.get("file", ""))
|
|
760
|
+
line.append(str(msg)[:60], style=Theme.error)
|
|
761
|
+
if loc:
|
|
762
|
+
line.append(f" @ {loc}", style=Theme.muted)
|
|
763
|
+
else:
|
|
764
|
+
line.append(str(err)[:70], style=Theme.error)
|
|
765
|
+
lines.append(line)
|
|
766
|
+
|
|
767
|
+
header = Text()
|
|
768
|
+
header.append(" ⚠️ ", style=Theme.error)
|
|
769
|
+
header.append(f"{len(errors)} error{'s' if len(errors) != 1 else ''}", style=Theme.error)
|
|
770
|
+
|
|
771
|
+
return Group(header, *lines)
|
|
772
|
+
|
|
773
|
+
def _format_plan_entries(self, data) -> Optional[RenderableType]:
|
|
774
|
+
"""Format plan/step entries."""
|
|
775
|
+
if not isinstance(data, list):
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
lines = []
|
|
779
|
+
for i, step in enumerate(data[:6], 1):
|
|
780
|
+
if not isinstance(step, dict):
|
|
781
|
+
continue
|
|
782
|
+
|
|
783
|
+
desc = step.get("description", step.get("step", step.get("action", "")))
|
|
784
|
+
status = step.get("status", "pending")
|
|
785
|
+
|
|
786
|
+
icons = {
|
|
787
|
+
"completed": "✓",
|
|
788
|
+
"done": "✓",
|
|
789
|
+
"in_progress": "→",
|
|
790
|
+
"active": "→",
|
|
791
|
+
"pending": str(i),
|
|
792
|
+
}
|
|
793
|
+
icon = icons.get(status, str(i))
|
|
794
|
+
style = Theme.success if status in ("completed", "done") else Theme.text
|
|
795
|
+
|
|
796
|
+
line = Text()
|
|
797
|
+
line.append(
|
|
798
|
+
f" {icon}. ",
|
|
799
|
+
style=Theme.purple if status not in ("completed", "done") else Theme.success,
|
|
800
|
+
)
|
|
801
|
+
line.append(str(desc)[:65], style=style)
|
|
802
|
+
lines.append(line)
|
|
803
|
+
|
|
804
|
+
header = Text()
|
|
805
|
+
header.append(" 📋 ", style=Theme.purple)
|
|
806
|
+
header.append("Plan:", style=Theme.text)
|
|
807
|
+
|
|
808
|
+
return Group(header, *lines)
|
|
809
|
+
|
|
810
|
+
def _format_generic_list(self, data) -> Optional[RenderableType]:
|
|
811
|
+
"""Format a generic list of items."""
|
|
812
|
+
if not data:
|
|
813
|
+
return None
|
|
814
|
+
|
|
815
|
+
lines = []
|
|
816
|
+
for item in data[:6]:
|
|
817
|
+
line = Text()
|
|
818
|
+
line.append(" • ", style=Theme.dim)
|
|
819
|
+
|
|
820
|
+
if isinstance(item, dict):
|
|
821
|
+
# Try to find a display field
|
|
822
|
+
display = None
|
|
823
|
+
for key in ("name", "title", "path", "message", "value", "text", "description"):
|
|
824
|
+
if key in item:
|
|
825
|
+
display = item[key]
|
|
826
|
+
break
|
|
827
|
+
if display is None:
|
|
828
|
+
display = str(item)
|
|
829
|
+
line.append(str(display)[:70], style=Theme.text)
|
|
830
|
+
else:
|
|
831
|
+
line.append(str(item)[:70], style=Theme.text)
|
|
832
|
+
|
|
833
|
+
lines.append(line)
|
|
834
|
+
|
|
835
|
+
if len(data) > 6:
|
|
836
|
+
more = Text()
|
|
837
|
+
more.append(f" ... and {len(data) - 6} more items", style=Theme.muted)
|
|
838
|
+
lines.append(more)
|
|
839
|
+
|
|
840
|
+
return Group(*lines) if lines else None
|
|
841
|
+
|
|
842
|
+
def _format_generic_dict(self, data) -> Optional[RenderableType]:
|
|
843
|
+
"""Format a generic dictionary result."""
|
|
844
|
+
if not data:
|
|
845
|
+
return None
|
|
846
|
+
|
|
847
|
+
# Check for common success/result patterns
|
|
848
|
+
if "success" in data or "ok" in data or "result" in data:
|
|
849
|
+
success = data.get("success", data.get("ok", True))
|
|
850
|
+
result_val = data.get("result", data.get("message", data.get("output", "")))
|
|
851
|
+
|
|
852
|
+
line = Text()
|
|
853
|
+
line.append(" ", style=Theme.dim)
|
|
854
|
+
if success:
|
|
855
|
+
line.append("✓ ", style=Theme.success)
|
|
856
|
+
if result_val:
|
|
857
|
+
line.append(str(result_val)[:60], style=Theme.text)
|
|
858
|
+
else:
|
|
859
|
+
line.append("Success", style=Theme.success)
|
|
860
|
+
else:
|
|
861
|
+
line.append("✕ ", style=Theme.error)
|
|
862
|
+
error = data.get("error", data.get("message", "Failed"))
|
|
863
|
+
line.append(str(error)[:60], style=Theme.error)
|
|
864
|
+
return line
|
|
865
|
+
|
|
866
|
+
# Show key-value pairs for small dicts
|
|
867
|
+
if len(data) <= 4:
|
|
868
|
+
lines = []
|
|
869
|
+
for key, val in list(data.items())[:4]:
|
|
870
|
+
line = Text()
|
|
871
|
+
line.append(" ", style=Theme.dim)
|
|
872
|
+
line.append(f"{key}: ", style=Theme.purple)
|
|
873
|
+
val_str = str(val)
|
|
874
|
+
if len(val_str) > 50:
|
|
875
|
+
val_str = val_str[:47] + "..."
|
|
876
|
+
line.append(val_str, style=Theme.text)
|
|
877
|
+
lines.append(line)
|
|
878
|
+
return Group(*lines) if lines else None
|
|
879
|
+
|
|
880
|
+
# For larger dicts, just show a summary
|
|
881
|
+
line = Text()
|
|
882
|
+
line.append(" ", style=Theme.dim)
|
|
883
|
+
line.append("{ ", style=Theme.muted)
|
|
884
|
+
keys = list(data.keys())[:4]
|
|
885
|
+
line.append(", ".join(keys), style=Theme.text)
|
|
886
|
+
if len(data) > 4:
|
|
887
|
+
line.append(f", ... +{len(data) - 4}", style=Theme.muted)
|
|
888
|
+
line.append(" }", style=Theme.muted)
|
|
889
|
+
return line
|
|
890
|
+
|
|
891
|
+
def _format_todo_result(self, result: str) -> Optional[RenderableType]:
|
|
892
|
+
"""Format todo list result nicely. (Legacy - redirects to new formatter)"""
|
|
893
|
+
try:
|
|
894
|
+
import json
|
|
895
|
+
|
|
896
|
+
data = json.loads(result)
|
|
897
|
+
return self._format_todo_list(data)
|
|
898
|
+
except (json.JSONDecodeError, TypeError):
|
|
899
|
+
return None
|
|
900
|
+
|
|
901
|
+
def _looks_like_code(self, text: str) -> bool:
|
|
902
|
+
"""Check if text looks like code."""
|
|
903
|
+
indicators = [
|
|
904
|
+
"def ",
|
|
905
|
+
"class ",
|
|
906
|
+
"function ",
|
|
907
|
+
"const ",
|
|
908
|
+
"let ",
|
|
909
|
+
"var ",
|
|
910
|
+
"import ",
|
|
911
|
+
"from ",
|
|
912
|
+
"fn ",
|
|
913
|
+
"func ",
|
|
914
|
+
"pub ",
|
|
915
|
+
"async ",
|
|
916
|
+
"await ",
|
|
917
|
+
"return ",
|
|
918
|
+
"{",
|
|
919
|
+
"}",
|
|
920
|
+
"=>",
|
|
921
|
+
"->",
|
|
922
|
+
]
|
|
923
|
+
return any(ind in text for ind in indicators) and len(text) > 20
|