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,470 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conversation History Widget - Navigate and Search Past Messages.
|
|
3
|
+
|
|
4
|
+
Provides conversation history management:
|
|
5
|
+
- Message timeline navigation
|
|
6
|
+
- Search through history
|
|
7
|
+
- Jump to specific messages
|
|
8
|
+
- Copy/export messages
|
|
9
|
+
- Session summaries
|
|
10
|
+
|
|
11
|
+
Makes it easy to reference and navigate long conversations.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from rich.console import RenderableType
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
from rich.box import ROUNDED
|
|
26
|
+
from textual.reactive import reactive
|
|
27
|
+
from textual.widgets import Static, Input
|
|
28
|
+
from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
|
|
29
|
+
from textual import events
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MessageType(Enum):
|
|
33
|
+
"""Type of conversation message."""
|
|
34
|
+
|
|
35
|
+
USER = "user"
|
|
36
|
+
ASSISTANT = "assistant"
|
|
37
|
+
SYSTEM = "system"
|
|
38
|
+
TOOL = "tool"
|
|
39
|
+
ERROR = "error"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class HistoryMessage:
|
|
44
|
+
"""A message in the conversation history."""
|
|
45
|
+
|
|
46
|
+
id: str
|
|
47
|
+
message_type: MessageType
|
|
48
|
+
content: str
|
|
49
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
50
|
+
|
|
51
|
+
# Optional metadata
|
|
52
|
+
agent_name: str = ""
|
|
53
|
+
model_name: str = ""
|
|
54
|
+
tool_name: str = ""
|
|
55
|
+
token_count: int = 0
|
|
56
|
+
duration_ms: float = 0
|
|
57
|
+
|
|
58
|
+
# File references
|
|
59
|
+
files_mentioned: List[str] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def preview(self) -> str:
|
|
63
|
+
"""Get a short preview of the message."""
|
|
64
|
+
text = self.content.strip()
|
|
65
|
+
if len(text) > 80:
|
|
66
|
+
return text[:77] + "..."
|
|
67
|
+
return text
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def time_str(self) -> str:
|
|
71
|
+
"""Get formatted timestamp."""
|
|
72
|
+
return self.timestamp.strftime("%H:%M")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
MESSAGE_STYLES = {
|
|
76
|
+
MessageType.USER: {"icon": "👤", "color": "#3b82f6", "label": "You"},
|
|
77
|
+
MessageType.ASSISTANT: {"icon": "🤖", "color": "#a855f7", "label": "Agent"},
|
|
78
|
+
MessageType.SYSTEM: {"icon": "⚙️", "color": "#6b7280", "label": "System"},
|
|
79
|
+
MessageType.TOOL: {"icon": "🔧", "color": "#f59e0b", "label": "Tool"},
|
|
80
|
+
MessageType.ERROR: {"icon": "❌", "color": "#ef4444", "label": "Error"},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MessagePreview(Static):
|
|
85
|
+
"""Preview widget for a single message."""
|
|
86
|
+
|
|
87
|
+
DEFAULT_CSS = """
|
|
88
|
+
MessagePreview {
|
|
89
|
+
height: 2;
|
|
90
|
+
padding: 0 1;
|
|
91
|
+
margin: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
MessagePreview:hover {
|
|
95
|
+
background: #1a1a1a;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
MessagePreview.selected {
|
|
99
|
+
background: #1a1a2a;
|
|
100
|
+
border-left: tall #3b82f6;
|
|
101
|
+
}
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
selected: reactive[bool] = reactive(False)
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
message: HistoryMessage,
|
|
109
|
+
on_select: Optional[Callable[[], None]] = None,
|
|
110
|
+
**kwargs,
|
|
111
|
+
):
|
|
112
|
+
super().__init__(**kwargs)
|
|
113
|
+
self.message = message
|
|
114
|
+
self._on_select = on_select
|
|
115
|
+
|
|
116
|
+
def on_click(self, event: events.Click) -> None:
|
|
117
|
+
"""Handle click."""
|
|
118
|
+
if self._on_select:
|
|
119
|
+
self._on_select()
|
|
120
|
+
|
|
121
|
+
def watch_selected(self, selected: bool) -> None:
|
|
122
|
+
"""React to selection changes."""
|
|
123
|
+
if selected:
|
|
124
|
+
self.add_class("selected")
|
|
125
|
+
else:
|
|
126
|
+
self.remove_class("selected")
|
|
127
|
+
|
|
128
|
+
def render(self) -> Text:
|
|
129
|
+
style = MESSAGE_STYLES.get(self.message.message_type, MESSAGE_STYLES[MessageType.ASSISTANT])
|
|
130
|
+
|
|
131
|
+
result = Text()
|
|
132
|
+
|
|
133
|
+
# Time and icon
|
|
134
|
+
result.append(f"{self.message.time_str} ", style="#52525b")
|
|
135
|
+
result.append(f"{style['icon']} ", style=style["color"])
|
|
136
|
+
|
|
137
|
+
# Sender
|
|
138
|
+
label = self.message.agent_name or style["label"]
|
|
139
|
+
result.append(f"{label}: ", style=f"bold {style['color']}")
|
|
140
|
+
|
|
141
|
+
# Preview
|
|
142
|
+
result.append(self.message.preview, style="#a1a1aa")
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ConversationTimeline(Container):
|
|
148
|
+
"""
|
|
149
|
+
Timeline view of conversation history.
|
|
150
|
+
|
|
151
|
+
Shows messages in chronological order with quick navigation.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
DEFAULT_CSS = """
|
|
155
|
+
ConversationTimeline {
|
|
156
|
+
height: 100%;
|
|
157
|
+
border: solid #27272a;
|
|
158
|
+
background: #0a0a0a;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ConversationTimeline .timeline-header {
|
|
162
|
+
height: 3;
|
|
163
|
+
border-bottom: solid #27272a;
|
|
164
|
+
padding: 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
ConversationTimeline .timeline-search {
|
|
168
|
+
height: 3;
|
|
169
|
+
border-bottom: solid #27272a;
|
|
170
|
+
padding: 0 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ConversationTimeline .timeline-content {
|
|
174
|
+
height: 1fr;
|
|
175
|
+
overflow-y: auto;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ConversationTimeline .timeline-footer {
|
|
179
|
+
height: 2;
|
|
180
|
+
border-top: solid #27272a;
|
|
181
|
+
padding: 0 1;
|
|
182
|
+
}
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
selected_index: reactive[int] = reactive(-1)
|
|
186
|
+
search_query: reactive[str] = reactive("")
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
on_message_select: Optional[Callable[[HistoryMessage], None]] = None,
|
|
191
|
+
**kwargs,
|
|
192
|
+
):
|
|
193
|
+
super().__init__(**kwargs)
|
|
194
|
+
self._messages: List[HistoryMessage] = []
|
|
195
|
+
self._filtered: List[HistoryMessage] = []
|
|
196
|
+
self._on_message_select = on_message_select
|
|
197
|
+
|
|
198
|
+
def add_message(self, message: HistoryMessage) -> None:
|
|
199
|
+
"""Add a message to history."""
|
|
200
|
+
self._messages.append(message)
|
|
201
|
+
self._apply_filter()
|
|
202
|
+
self._update_display()
|
|
203
|
+
|
|
204
|
+
def set_messages(self, messages: List[HistoryMessage]) -> None:
|
|
205
|
+
"""Set all messages."""
|
|
206
|
+
self._messages = list(messages)
|
|
207
|
+
self._apply_filter()
|
|
208
|
+
self._update_display()
|
|
209
|
+
|
|
210
|
+
def clear(self) -> None:
|
|
211
|
+
"""Clear history."""
|
|
212
|
+
self._messages.clear()
|
|
213
|
+
self._filtered.clear()
|
|
214
|
+
self.selected_index = -1
|
|
215
|
+
self._update_display()
|
|
216
|
+
|
|
217
|
+
def _apply_filter(self) -> None:
|
|
218
|
+
"""Apply search filter."""
|
|
219
|
+
if not self.search_query:
|
|
220
|
+
self._filtered = list(self._messages)
|
|
221
|
+
else:
|
|
222
|
+
query = self.search_query.lower()
|
|
223
|
+
self._filtered = [
|
|
224
|
+
m
|
|
225
|
+
for m in self._messages
|
|
226
|
+
if query in m.content.lower() or query in m.agent_name.lower()
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
def watch_search_query(self, query: str) -> None:
|
|
230
|
+
"""React to search query changes."""
|
|
231
|
+
self._apply_filter()
|
|
232
|
+
self._update_display()
|
|
233
|
+
|
|
234
|
+
def watch_selected_index(self, index: int) -> None:
|
|
235
|
+
"""React to selection changes."""
|
|
236
|
+
self._update_selection()
|
|
237
|
+
|
|
238
|
+
if 0 <= index < len(self._filtered) and self._on_message_select:
|
|
239
|
+
self._on_message_select(self._filtered[index])
|
|
240
|
+
|
|
241
|
+
def _update_selection(self) -> None:
|
|
242
|
+
"""Update selection state."""
|
|
243
|
+
try:
|
|
244
|
+
content = self.query_one(".timeline-content", ScrollableContainer)
|
|
245
|
+
for i, widget in enumerate(content.children):
|
|
246
|
+
if isinstance(widget, MessagePreview):
|
|
247
|
+
widget.selected = i == self.selected_index
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
def _update_display(self) -> None:
|
|
252
|
+
"""Update the display."""
|
|
253
|
+
try:
|
|
254
|
+
header = self.query_one(".timeline-header", Static)
|
|
255
|
+
content = self.query_one(".timeline-content", ScrollableContainer)
|
|
256
|
+
footer = self.query_one(".timeline-footer", Static)
|
|
257
|
+
except Exception:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# Header
|
|
261
|
+
header_text = Text()
|
|
262
|
+
header_text.append("📜 ", style="bold #3b82f6")
|
|
263
|
+
header_text.append("Conversation History", style="bold #e4e4e7")
|
|
264
|
+
header_text.append(f" ({len(self._messages)} messages)", style="#6b7280")
|
|
265
|
+
header.update(header_text)
|
|
266
|
+
|
|
267
|
+
# Content - message previews
|
|
268
|
+
content.remove_children()
|
|
269
|
+
for i, message in enumerate(self._filtered[-50:]): # Show last 50
|
|
270
|
+
preview = MessagePreview(
|
|
271
|
+
message,
|
|
272
|
+
on_select=lambda idx=i: self._select_message(idx),
|
|
273
|
+
)
|
|
274
|
+
if i == self.selected_index:
|
|
275
|
+
preview.selected = True
|
|
276
|
+
content.mount(preview)
|
|
277
|
+
|
|
278
|
+
# Footer
|
|
279
|
+
footer_text = Text()
|
|
280
|
+
footer_text.append("[↑/↓] Navigate ", style="#52525b")
|
|
281
|
+
footer_text.append("[Enter] View ", style="#52525b")
|
|
282
|
+
footer_text.append("[/] Search", style="#52525b")
|
|
283
|
+
footer.update(footer_text)
|
|
284
|
+
|
|
285
|
+
def _select_message(self, index: int) -> None:
|
|
286
|
+
"""Select a message by index."""
|
|
287
|
+
self.selected_index = index
|
|
288
|
+
|
|
289
|
+
def on_key(self, event: events.Key) -> None:
|
|
290
|
+
"""Handle keyboard navigation."""
|
|
291
|
+
if event.key == "up":
|
|
292
|
+
self.selected_index = max(0, self.selected_index - 1)
|
|
293
|
+
event.prevent_default()
|
|
294
|
+
elif event.key == "down":
|
|
295
|
+
self.selected_index = min(len(self._filtered) - 1, self.selected_index + 1)
|
|
296
|
+
event.prevent_default()
|
|
297
|
+
elif event.key == "home":
|
|
298
|
+
self.selected_index = 0
|
|
299
|
+
event.prevent_default()
|
|
300
|
+
elif event.key == "end":
|
|
301
|
+
self.selected_index = len(self._filtered) - 1
|
|
302
|
+
event.prevent_default()
|
|
303
|
+
|
|
304
|
+
def compose(self):
|
|
305
|
+
"""Compose the timeline."""
|
|
306
|
+
yield Static("", classes="timeline-header")
|
|
307
|
+
yield Input(placeholder="Search messages...", classes="timeline-search")
|
|
308
|
+
yield ScrollableContainer(classes="timeline-content")
|
|
309
|
+
yield Static("", classes="timeline-footer")
|
|
310
|
+
|
|
311
|
+
def on_mount(self) -> None:
|
|
312
|
+
"""Initialize."""
|
|
313
|
+
self._update_display()
|
|
314
|
+
|
|
315
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
316
|
+
"""Handle search input."""
|
|
317
|
+
self.search_query = event.value
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class MessageDetail(Container):
|
|
321
|
+
"""
|
|
322
|
+
Detailed view of a single message.
|
|
323
|
+
|
|
324
|
+
Shows full message content with metadata.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
DEFAULT_CSS = """
|
|
328
|
+
MessageDetail {
|
|
329
|
+
height: auto;
|
|
330
|
+
border: solid #27272a;
|
|
331
|
+
background: #0a0a0a;
|
|
332
|
+
padding: 1;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
MessageDetail .detail-header {
|
|
336
|
+
height: 2;
|
|
337
|
+
margin-bottom: 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
MessageDetail .detail-content {
|
|
341
|
+
height: auto;
|
|
342
|
+
padding: 0 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
MessageDetail .detail-meta {
|
|
346
|
+
height: auto;
|
|
347
|
+
margin-top: 1;
|
|
348
|
+
border-top: solid #27272a;
|
|
349
|
+
padding-top: 1;
|
|
350
|
+
}
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(self, **kwargs):
|
|
354
|
+
super().__init__(**kwargs)
|
|
355
|
+
self._message: Optional[HistoryMessage] = None
|
|
356
|
+
|
|
357
|
+
def set_message(self, message: Optional[HistoryMessage]) -> None:
|
|
358
|
+
"""Set the message to display."""
|
|
359
|
+
self._message = message
|
|
360
|
+
self._update_display()
|
|
361
|
+
|
|
362
|
+
def _update_display(self) -> None:
|
|
363
|
+
"""Update the display."""
|
|
364
|
+
try:
|
|
365
|
+
header = self.query_one(".detail-header", Static)
|
|
366
|
+
content = self.query_one(".detail-content", Static)
|
|
367
|
+
meta = self.query_one(".detail-meta", Static)
|
|
368
|
+
except Exception:
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
if not self._message:
|
|
372
|
+
header.update(Text("No message selected", style="#52525b"))
|
|
373
|
+
content.update("")
|
|
374
|
+
meta.update("")
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
msg = self._message
|
|
378
|
+
style = MESSAGE_STYLES.get(msg.message_type, MESSAGE_STYLES[MessageType.ASSISTANT])
|
|
379
|
+
|
|
380
|
+
# Header
|
|
381
|
+
header_text = Text()
|
|
382
|
+
header_text.append(f"{style['icon']} ", style=style["color"])
|
|
383
|
+
|
|
384
|
+
label = msg.agent_name or style["label"]
|
|
385
|
+
header_text.append(label, style=f"bold {style['color']}")
|
|
386
|
+
|
|
387
|
+
if msg.model_name:
|
|
388
|
+
header_text.append(f" ({msg.model_name})", style="#6b7280")
|
|
389
|
+
|
|
390
|
+
header_text.append(f"\n{msg.timestamp.strftime('%Y-%m-%d %H:%M:%S')}", style="#52525b")
|
|
391
|
+
|
|
392
|
+
header.update(header_text)
|
|
393
|
+
|
|
394
|
+
# Content
|
|
395
|
+
content.update(Text(msg.content, style="#e4e4e7"))
|
|
396
|
+
|
|
397
|
+
# Metadata
|
|
398
|
+
meta_text = Text()
|
|
399
|
+
|
|
400
|
+
if msg.token_count:
|
|
401
|
+
meta_text.append(f"📊 {msg.token_count} tokens", style="#6b7280")
|
|
402
|
+
|
|
403
|
+
if msg.duration_ms:
|
|
404
|
+
if meta_text:
|
|
405
|
+
meta_text.append(" │ ", style="#27272a")
|
|
406
|
+
meta_text.append(f"⏱️ {msg.duration_ms:.0f}ms", style="#6b7280")
|
|
407
|
+
|
|
408
|
+
if msg.files_mentioned:
|
|
409
|
+
if meta_text:
|
|
410
|
+
meta_text.append("\n")
|
|
411
|
+
meta_text.append(f"📁 Files: {', '.join(msg.files_mentioned[:3])}", style="#6b7280")
|
|
412
|
+
if len(msg.files_mentioned) > 3:
|
|
413
|
+
meta_text.append(f" +{len(msg.files_mentioned) - 3} more", style="#52525b")
|
|
414
|
+
|
|
415
|
+
meta.update(meta_text)
|
|
416
|
+
|
|
417
|
+
def compose(self):
|
|
418
|
+
"""Compose the detail view."""
|
|
419
|
+
yield Static("", classes="detail-header")
|
|
420
|
+
yield Static("", classes="detail-content")
|
|
421
|
+
yield Static("", classes="detail-meta")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class ConversationNavigator(Static):
|
|
425
|
+
"""
|
|
426
|
+
Compact conversation navigator for quick jumping.
|
|
427
|
+
|
|
428
|
+
Shows message count and allows jumping to messages.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
DEFAULT_CSS = """
|
|
432
|
+
ConversationNavigator {
|
|
433
|
+
width: auto;
|
|
434
|
+
height: 1;
|
|
435
|
+
padding: 0 1;
|
|
436
|
+
}
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
def __init__(
|
|
440
|
+
self,
|
|
441
|
+
on_open_history: Optional[Callable[[], None]] = None,
|
|
442
|
+
**kwargs,
|
|
443
|
+
):
|
|
444
|
+
super().__init__(**kwargs)
|
|
445
|
+
self._message_count = 0
|
|
446
|
+
self._current_index = 0
|
|
447
|
+
self._on_open_history = on_open_history
|
|
448
|
+
|
|
449
|
+
def set_counts(self, total: int, current: int = 0) -> None:
|
|
450
|
+
"""Set message counts."""
|
|
451
|
+
self._message_count = total
|
|
452
|
+
self._current_index = current
|
|
453
|
+
self.refresh()
|
|
454
|
+
|
|
455
|
+
def on_click(self, event: events.Click) -> None:
|
|
456
|
+
"""Handle click to open history."""
|
|
457
|
+
if self._on_open_history:
|
|
458
|
+
self._on_open_history()
|
|
459
|
+
|
|
460
|
+
def render(self) -> Text:
|
|
461
|
+
text = Text()
|
|
462
|
+
|
|
463
|
+
text.append("📜 ", style="#3b82f6")
|
|
464
|
+
|
|
465
|
+
if self._message_count == 0:
|
|
466
|
+
text.append("No messages", style="#52525b")
|
|
467
|
+
else:
|
|
468
|
+
text.append(f"{self._current_index + 1}/{self._message_count}", style="#a1a1aa")
|
|
469
|
+
|
|
470
|
+
return text
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diff Indicator Widget - Compact visual representation of file changes.
|
|
3
|
+
|
|
4
|
+
DiffChanges component, adapted for SuperQode's style.
|
|
5
|
+
Shows colored bars/indicators representing additions/deletions ratio.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# SuperQode colors
|
|
15
|
+
COLORS = {
|
|
16
|
+
"addition": "#22c55e",
|
|
17
|
+
"deletion": "#ef4444",
|
|
18
|
+
"neutral": "#52525b",
|
|
19
|
+
"text_dim": "#71717a",
|
|
20
|
+
"text_muted": "#a1a1aa",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def render_diff_indicator(
|
|
25
|
+
additions: int,
|
|
26
|
+
deletions: int,
|
|
27
|
+
variant: str = "bars",
|
|
28
|
+
max_bars: int = 5,
|
|
29
|
+
) -> Text:
|
|
30
|
+
"""
|
|
31
|
+
Render a compact diff indicator showing additions/deletions.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
additions: Number of lines added
|
|
35
|
+
deletions: Number of lines deleted
|
|
36
|
+
variant: "bars" for visual bars, "text" for +X -Y text
|
|
37
|
+
max_bars: Maximum number of bars to show (default 5)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Rich Text object with the indicator
|
|
41
|
+
"""
|
|
42
|
+
total = additions + deletions
|
|
43
|
+
|
|
44
|
+
if variant == "text":
|
|
45
|
+
# Simple text format: +X -Y
|
|
46
|
+
text = Text()
|
|
47
|
+
if additions > 0:
|
|
48
|
+
text.append(f"+{additions}", style=f"bold {COLORS['addition']}")
|
|
49
|
+
if deletions > 0:
|
|
50
|
+
if additions > 0:
|
|
51
|
+
text.append(" / ", style=COLORS["text_dim"])
|
|
52
|
+
text.append(f"-{deletions}", style=f"bold {COLORS['deletion']}")
|
|
53
|
+
if total == 0:
|
|
54
|
+
text.append("0", style=COLORS["text_muted"])
|
|
55
|
+
return text
|
|
56
|
+
|
|
57
|
+
# Bars variant - visual representation
|
|
58
|
+
if total == 0:
|
|
59
|
+
# Show all neutral bars
|
|
60
|
+
bars = Text()
|
|
61
|
+
for _ in range(max_bars):
|
|
62
|
+
bars.append("█", style=COLORS["neutral"])
|
|
63
|
+
return bars
|
|
64
|
+
|
|
65
|
+
# Calculate bar distribution
|
|
66
|
+
if total < max_bars:
|
|
67
|
+
# Small changes - show 1 bar each if present
|
|
68
|
+
added_bars = 1 if additions > 0 else 0
|
|
69
|
+
deleted_bars = 1 if deletions > 0 else 0
|
|
70
|
+
neutral_bars = max_bars - added_bars - deleted_bars
|
|
71
|
+
else:
|
|
72
|
+
# Larger changes - proportional distribution
|
|
73
|
+
percent_added = additions / total if total > 0 else 0
|
|
74
|
+
percent_deleted = deletions / total if total > 0 else 0
|
|
75
|
+
|
|
76
|
+
# Reserve at least 1 bar for each if present, but cap based on magnitude
|
|
77
|
+
BLOCKS_FOR_COLORS = max_bars - 1 if total < 20 else max_bars
|
|
78
|
+
|
|
79
|
+
added_raw = percent_added * BLOCKS_FOR_COLORS
|
|
80
|
+
deleted_raw = percent_deleted * BLOCKS_FOR_COLORS
|
|
81
|
+
|
|
82
|
+
added_bars = max(1, round(added_raw)) if additions > 0 else 0
|
|
83
|
+
deleted_bars = max(1, round(deleted_raw)) if deletions > 0 else 0
|
|
84
|
+
|
|
85
|
+
# Cap based on actual magnitude
|
|
86
|
+
if additions > 0 and additions <= 5:
|
|
87
|
+
added_bars = min(added_bars, 1)
|
|
88
|
+
elif additions > 5 and additions <= 10:
|
|
89
|
+
added_bars = min(added_bars, 2)
|
|
90
|
+
|
|
91
|
+
if deletions > 0 and deletions <= 5:
|
|
92
|
+
deleted_bars = min(deleted_bars, 1)
|
|
93
|
+
elif deletions > 5 and deletions <= 10:
|
|
94
|
+
deleted_bars = min(deleted_bars, 2)
|
|
95
|
+
|
|
96
|
+
# Ensure we don't exceed max_bars
|
|
97
|
+
total_allocated = added_bars + deleted_bars
|
|
98
|
+
if total_allocated > BLOCKS_FOR_COLORS:
|
|
99
|
+
if added_raw > deleted_raw:
|
|
100
|
+
added_bars = BLOCKS_FOR_COLORS - deleted_bars
|
|
101
|
+
else:
|
|
102
|
+
deleted_bars = BLOCKS_FOR_COLORS - added_bars
|
|
103
|
+
|
|
104
|
+
neutral_bars = max(0, max_bars - added_bars - deleted_bars)
|
|
105
|
+
|
|
106
|
+
# Render bars
|
|
107
|
+
bars = Text()
|
|
108
|
+
for _ in range(added_bars):
|
|
109
|
+
bars.append("█", style=COLORS["addition"])
|
|
110
|
+
for _ in range(deleted_bars):
|
|
111
|
+
bars.append("█", style=COLORS["deletion"])
|
|
112
|
+
for _ in range(neutral_bars):
|
|
113
|
+
bars.append("█", style=COLORS["neutral"])
|
|
114
|
+
|
|
115
|
+
return bars
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def render_diff_indicator_with_text(
|
|
119
|
+
additions: int,
|
|
120
|
+
deletions: int,
|
|
121
|
+
show_bars: bool = True,
|
|
122
|
+
show_text: bool = True,
|
|
123
|
+
) -> Text:
|
|
124
|
+
"""
|
|
125
|
+
Render diff indicator with both bars and text.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
additions: Number of lines added
|
|
129
|
+
deletions: Number of lines deleted
|
|
130
|
+
show_bars: Whether to show visual bars
|
|
131
|
+
show_text: Whether to show +X -Y text
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Rich Text object with both bars and text
|
|
135
|
+
"""
|
|
136
|
+
result = Text()
|
|
137
|
+
|
|
138
|
+
if show_bars:
|
|
139
|
+
bars = render_diff_indicator(additions, deletions, variant="bars")
|
|
140
|
+
result.append(bars)
|
|
141
|
+
if show_text:
|
|
142
|
+
result.append(" ", style="")
|
|
143
|
+
|
|
144
|
+
if show_text:
|
|
145
|
+
text = render_diff_indicator(additions, deletions, variant="text")
|
|
146
|
+
result.append(text)
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = [
|
|
152
|
+
"render_diff_indicator",
|
|
153
|
+
"render_diff_indicator_with_text",
|
|
154
|
+
"COLORS",
|
|
155
|
+
]
|