shotgun-sh 0.1.0.dev1__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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/__init__.py +3 -0
- shotgun/agents/__init__.py +1 -0
- shotgun/agents/agent_manager.py +196 -0
- shotgun/agents/common.py +295 -0
- shotgun/agents/config/__init__.py +13 -0
- shotgun/agents/config/manager.py +215 -0
- shotgun/agents/config/models.py +120 -0
- shotgun/agents/config/provider.py +91 -0
- shotgun/agents/history/__init__.py +5 -0
- shotgun/agents/history/history_processors.py +213 -0
- shotgun/agents/models.py +94 -0
- shotgun/agents/plan.py +119 -0
- shotgun/agents/research.py +131 -0
- shotgun/agents/tasks.py +122 -0
- shotgun/agents/tools/__init__.py +26 -0
- shotgun/agents/tools/codebase/__init__.py +28 -0
- shotgun/agents/tools/codebase/codebase_shell.py +256 -0
- shotgun/agents/tools/codebase/directory_lister.py +141 -0
- shotgun/agents/tools/codebase/file_read.py +144 -0
- shotgun/agents/tools/codebase/models.py +252 -0
- shotgun/agents/tools/codebase/query_graph.py +67 -0
- shotgun/agents/tools/codebase/retrieve_code.py +81 -0
- shotgun/agents/tools/file_management.py +130 -0
- shotgun/agents/tools/user_interaction.py +36 -0
- shotgun/agents/tools/web_search.py +69 -0
- shotgun/cli/__init__.py +1 -0
- shotgun/cli/codebase/__init__.py +5 -0
- shotgun/cli/codebase/commands.py +202 -0
- shotgun/cli/codebase/models.py +21 -0
- shotgun/cli/config.py +261 -0
- shotgun/cli/models.py +10 -0
- shotgun/cli/plan.py +65 -0
- shotgun/cli/research.py +78 -0
- shotgun/cli/tasks.py +71 -0
- shotgun/cli/utils.py +25 -0
- shotgun/codebase/__init__.py +12 -0
- shotgun/codebase/core/__init__.py +46 -0
- shotgun/codebase/core/change_detector.py +358 -0
- shotgun/codebase/core/code_retrieval.py +243 -0
- shotgun/codebase/core/ingestor.py +1497 -0
- shotgun/codebase/core/language_config.py +297 -0
- shotgun/codebase/core/manager.py +1554 -0
- shotgun/codebase/core/nl_query.py +327 -0
- shotgun/codebase/core/parser_loader.py +152 -0
- shotgun/codebase/models.py +107 -0
- shotgun/codebase/service.py +148 -0
- shotgun/logging_config.py +172 -0
- shotgun/main.py +73 -0
- shotgun/prompts/__init__.py +5 -0
- shotgun/prompts/agents/__init__.py +1 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
- shotgun/prompts/agents/plan.j2 +57 -0
- shotgun/prompts/agents/research.j2 +38 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
- shotgun/prompts/agents/state/system_state.j2 +1 -0
- shotgun/prompts/agents/tasks.j2 +67 -0
- shotgun/prompts/codebase/__init__.py +1 -0
- shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
- shotgun/prompts/codebase/cypher_system.j2 +28 -0
- shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
- shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
- shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
- shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
- shotgun/prompts/history/__init__.py +1 -0
- shotgun/prompts/history/summarization.j2 +46 -0
- shotgun/prompts/loader.py +140 -0
- shotgun/prompts/user/research.j2 +5 -0
- shotgun/py.typed +0 -0
- shotgun/sdk/__init__.py +13 -0
- shotgun/sdk/codebase.py +195 -0
- shotgun/sdk/exceptions.py +17 -0
- shotgun/sdk/models.py +189 -0
- shotgun/sdk/services.py +23 -0
- shotgun/telemetry.py +68 -0
- shotgun/tui/__init__.py +0 -0
- shotgun/tui/app.py +49 -0
- shotgun/tui/components/prompt_input.py +69 -0
- shotgun/tui/components/spinner.py +86 -0
- shotgun/tui/components/splash.py +25 -0
- shotgun/tui/components/vertical_tail.py +28 -0
- shotgun/tui/screens/chat.py +415 -0
- shotgun/tui/screens/chat.tcss +28 -0
- shotgun/tui/screens/provider_config.py +221 -0
- shotgun/tui/screens/splash.py +31 -0
- shotgun/tui/styles.tcss +10 -0
- shotgun/utils/__init__.py +5 -0
- shotgun/utils/file_system_utils.py +31 -0
- shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
- shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
- shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
- shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
- shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from textual.app import RenderResult
|
|
2
|
+
from textual.widgets import Static
|
|
3
|
+
|
|
4
|
+
ART = """
|
|
5
|
+
|
|
6
|
+
███████╗██╗ ██╗ ██████╗ ████████╗ ██████╗ ██╗ ██╗███╗ ██╗
|
|
7
|
+
██╔════╝██║ ██║██╔═══██╗╚══██╔══╝██╔════╝ ██║ ██║████╗ ██║
|
|
8
|
+
███████╗███████║██║ ██║ ██║ ██║ ███╗██║ ██║██╔██╗ ██║
|
|
9
|
+
╚════██║██╔══██║██║ ██║ ██║ ██║ ██║██║ ██║██║╚██╗██║
|
|
10
|
+
███████║██║ ██║╚██████╔╝ ██║ ╚██████╔╝╚██████╔╝██║ ╚████║
|
|
11
|
+
╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SplashWidget(Static):
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
SplashWidget {
|
|
19
|
+
text-align: center;
|
|
20
|
+
width: 64;
|
|
21
|
+
}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def render(self) -> RenderResult:
|
|
25
|
+
return ART
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from textual.containers import VerticalScroll
|
|
2
|
+
from textual.reactive import reactive
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class VerticalTail(VerticalScroll):
|
|
6
|
+
"""A vertical scroll container that automatically scrolls to the bottom when content is added."""
|
|
7
|
+
|
|
8
|
+
auto_scroll = reactive(True, layout=False)
|
|
9
|
+
|
|
10
|
+
def on_mount(self) -> None:
|
|
11
|
+
"""Set up auto-scrolling when the widget is mounted."""
|
|
12
|
+
# Start at the bottom
|
|
13
|
+
if self.auto_scroll:
|
|
14
|
+
self.scroll_end(animate=False)
|
|
15
|
+
|
|
16
|
+
def on_descendant_mount(self) -> None:
|
|
17
|
+
"""Auto-scroll when a new child is added."""
|
|
18
|
+
if self.auto_scroll:
|
|
19
|
+
# Check if we're near the bottom (within 1 line of scroll)
|
|
20
|
+
at_bottom = self.scroll_y >= self.max_scroll_y - 1
|
|
21
|
+
if at_bottom:
|
|
22
|
+
# Use call_after_refresh to ensure layout is updated first
|
|
23
|
+
self.call_after_refresh(self.scroll_end, animate=False)
|
|
24
|
+
|
|
25
|
+
def watch_auto_scroll(self, value: bool) -> None:
|
|
26
|
+
"""Handle auto_scroll property changes."""
|
|
27
|
+
if value:
|
|
28
|
+
self.scroll_end(animate=False)
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from pydantic_ai import DeferredToolResults
|
|
5
|
+
from pydantic_ai.messages import (
|
|
6
|
+
BuiltinToolCallPart,
|
|
7
|
+
BuiltinToolReturnPart,
|
|
8
|
+
ModelMessage,
|
|
9
|
+
ModelRequest,
|
|
10
|
+
ModelResponse,
|
|
11
|
+
TextPart,
|
|
12
|
+
ThinkingPart,
|
|
13
|
+
ToolCallPart,
|
|
14
|
+
)
|
|
15
|
+
from textual import on, work
|
|
16
|
+
from textual.app import ComposeResult
|
|
17
|
+
from textual.command import DiscoveryHit, Hit, Provider
|
|
18
|
+
from textual.containers import Container
|
|
19
|
+
from textual.reactive import reactive
|
|
20
|
+
from textual.screen import Screen
|
|
21
|
+
from textual.widget import Widget
|
|
22
|
+
from textual.widgets import Markdown
|
|
23
|
+
|
|
24
|
+
from shotgun.agents.agent_manager import AgentManager, AgentType, MessageHistoryUpdated
|
|
25
|
+
from shotgun.agents.config import get_provider_model
|
|
26
|
+
from shotgun.agents.models import AgentDeps, UserAnswer, UserQuestion
|
|
27
|
+
from shotgun.sdk.services import get_codebase_service
|
|
28
|
+
|
|
29
|
+
from ..components.prompt_input import PromptInput
|
|
30
|
+
from ..components.spinner import Spinner
|
|
31
|
+
from ..components.vertical_tail import VerticalTail
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PromptHistory:
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self.prompts: list[str] = ["Hello there!"]
|
|
37
|
+
self.curr: int | None = None
|
|
38
|
+
|
|
39
|
+
def next(self) -> str:
|
|
40
|
+
if self.curr is None:
|
|
41
|
+
self.curr = -1
|
|
42
|
+
else:
|
|
43
|
+
self.curr = -1
|
|
44
|
+
return self.prompts[self.curr]
|
|
45
|
+
|
|
46
|
+
def prev(self) -> str:
|
|
47
|
+
if self.curr is None:
|
|
48
|
+
raise Exception("current entry is none")
|
|
49
|
+
if self.curr == -1:
|
|
50
|
+
self.curr = None
|
|
51
|
+
return ""
|
|
52
|
+
self.curr += 1
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
def append(self, text: str) -> None:
|
|
56
|
+
self.prompts.append(text)
|
|
57
|
+
self.curr = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChatHistory(Widget):
|
|
61
|
+
DEFAULT_CSS = """
|
|
62
|
+
VerticalTail {
|
|
63
|
+
align: left bottom;
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
VerticalTail > * {
|
|
67
|
+
height: auto;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Horizontal {
|
|
71
|
+
height: auto;
|
|
72
|
+
background: $secondary-muted;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Markdown {
|
|
76
|
+
height: auto;
|
|
77
|
+
}
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
super().__init__()
|
|
82
|
+
self.items: list[ModelMessage] = []
|
|
83
|
+
self.vertical_tail: VerticalTail | None = None
|
|
84
|
+
|
|
85
|
+
def compose(self) -> ComposeResult:
|
|
86
|
+
self.vertical_tail = VerticalTail()
|
|
87
|
+
yield self.vertical_tail
|
|
88
|
+
|
|
89
|
+
def update_messages(self, messages: list[ModelMessage]) -> None:
|
|
90
|
+
"""Update the displayed messages without recomposing."""
|
|
91
|
+
if not self.vertical_tail:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Clear existing widgets
|
|
95
|
+
self.vertical_tail.remove_children()
|
|
96
|
+
|
|
97
|
+
# Add new message widgets
|
|
98
|
+
for item in messages:
|
|
99
|
+
if isinstance(item, ModelRequest):
|
|
100
|
+
self.vertical_tail.mount(UserQuestionWidget(item))
|
|
101
|
+
elif isinstance(item, ModelResponse):
|
|
102
|
+
self.vertical_tail.mount(AgentResponseWidget(item))
|
|
103
|
+
|
|
104
|
+
self.items = messages
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class UserQuestionWidget(Widget):
|
|
108
|
+
def __init__(self, item: ModelRequest) -> None:
|
|
109
|
+
super().__init__()
|
|
110
|
+
self.item = item
|
|
111
|
+
|
|
112
|
+
def compose(self) -> ComposeResult:
|
|
113
|
+
prompt = "".join(str(part.content) for part in self.item.parts if part.content)
|
|
114
|
+
yield Markdown(markdown=f"**>** {prompt}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AgentResponseWidget(Widget):
|
|
118
|
+
def __init__(self, item: ModelResponse) -> None:
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.item = item
|
|
121
|
+
|
|
122
|
+
def compose(self) -> ComposeResult:
|
|
123
|
+
yield Markdown(markdown=f"**⏺** {self.compute_output()}")
|
|
124
|
+
|
|
125
|
+
def compute_output(self) -> str:
|
|
126
|
+
acc = ""
|
|
127
|
+
for part in self.item.parts: # TextPart | ToolCallPart | BuiltinToolCallPart | BuiltinToolReturnPart | ThinkingPart
|
|
128
|
+
if isinstance(part, TextPart):
|
|
129
|
+
acc += part.content
|
|
130
|
+
elif isinstance(part, ToolCallPart):
|
|
131
|
+
acc += f"{part.tool_name}({part.args})\n"
|
|
132
|
+
elif isinstance(part, BuiltinToolCallPart):
|
|
133
|
+
acc += f"{part.tool_name}({part.args})\n"
|
|
134
|
+
elif isinstance(part, BuiltinToolReturnPart):
|
|
135
|
+
acc += f"{part.tool_name}()\n"
|
|
136
|
+
elif isinstance(part, ThinkingPart):
|
|
137
|
+
acc += f"Thinking: {part.content}\n"
|
|
138
|
+
return acc
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class StatusBar(Widget):
|
|
142
|
+
DEFAULT_CSS = """
|
|
143
|
+
StatusBar {
|
|
144
|
+
text-wrap: wrap;
|
|
145
|
+
}
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def render(self) -> str:
|
|
149
|
+
return """[$foreground-muted]Press [bold $text]Enter[/] to send • [bold $text]Ctrl+P[/] for command palette • /help for commands[/]"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ModeIndicator(Widget):
|
|
153
|
+
"""Widget to display the current agent mode."""
|
|
154
|
+
|
|
155
|
+
DEFAULT_CSS = """
|
|
156
|
+
ModeIndicator {
|
|
157
|
+
text-wrap: wrap;
|
|
158
|
+
}
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, mode: AgentType) -> None:
|
|
162
|
+
"""Initialize the mode indicator.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
mode: The current agent type/mode.
|
|
166
|
+
"""
|
|
167
|
+
super().__init__()
|
|
168
|
+
self.mode = mode
|
|
169
|
+
|
|
170
|
+
def render(self) -> str:
|
|
171
|
+
"""Render the mode indicator."""
|
|
172
|
+
mode_display = {
|
|
173
|
+
AgentType.RESEARCH: "Research",
|
|
174
|
+
AgentType.PLAN: "Planning",
|
|
175
|
+
AgentType.TASKS: "Tasks",
|
|
176
|
+
}
|
|
177
|
+
mode_description = {
|
|
178
|
+
AgentType.RESEARCH: "Research topics with web search and synthesize findings",
|
|
179
|
+
AgentType.PLAN: "Create comprehensive, actionable plans with milestones",
|
|
180
|
+
AgentType.TASKS: "Generate specific, actionable tasks from research and plans",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
mode_title = mode_display.get(self.mode, self.mode.value.title())
|
|
184
|
+
description = mode_description.get(self.mode, "")
|
|
185
|
+
|
|
186
|
+
return f"[bold $text-accent]{mode_title} mode[/][$foreground-muted] ({description})[/]"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class AgentModeProvider(Provider):
|
|
190
|
+
"""Command provider for agent mode switching."""
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def chat_screen(self) -> "ChatScreen":
|
|
194
|
+
return cast(ChatScreen, self.screen)
|
|
195
|
+
|
|
196
|
+
def set_mode(self, mode: AgentType) -> None:
|
|
197
|
+
"""Switch to research mode."""
|
|
198
|
+
self.chat_screen.mode = mode
|
|
199
|
+
|
|
200
|
+
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
201
|
+
"""Provide default mode switching commands when palette opens."""
|
|
202
|
+
yield DiscoveryHit(
|
|
203
|
+
"Switch to Research Mode",
|
|
204
|
+
lambda: self.set_mode(AgentType.RESEARCH),
|
|
205
|
+
help="🔬 Research topics with web search and synthesize findings",
|
|
206
|
+
)
|
|
207
|
+
yield DiscoveryHit(
|
|
208
|
+
"Switch to Plan Mode",
|
|
209
|
+
lambda: self.set_mode(AgentType.PLAN),
|
|
210
|
+
help="📋 Create comprehensive, actionable plans with milestones",
|
|
211
|
+
)
|
|
212
|
+
yield DiscoveryHit(
|
|
213
|
+
"Switch to Tasks Mode",
|
|
214
|
+
lambda: self.set_mode(AgentType.TASKS),
|
|
215
|
+
help="✅ Generate specific, actionable tasks from research and plans",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
219
|
+
"""Search for mode commands."""
|
|
220
|
+
matcher = self.matcher(query)
|
|
221
|
+
|
|
222
|
+
commands = [
|
|
223
|
+
(
|
|
224
|
+
"Switch to Research Mode",
|
|
225
|
+
"🔬 Research topics with web search and synthesize findings",
|
|
226
|
+
lambda: self.set_mode(AgentType.RESEARCH),
|
|
227
|
+
AgentType.RESEARCH,
|
|
228
|
+
),
|
|
229
|
+
(
|
|
230
|
+
"Switch to Plan Mode",
|
|
231
|
+
"📋 Create comprehensive, actionable plans with milestones",
|
|
232
|
+
lambda: self.set_mode(AgentType.PLAN),
|
|
233
|
+
AgentType.PLAN,
|
|
234
|
+
),
|
|
235
|
+
(
|
|
236
|
+
"Switch to Tasks Mode",
|
|
237
|
+
"✅ Generate specific, actionable tasks from research and plans",
|
|
238
|
+
lambda: self.set_mode(AgentType.TASKS),
|
|
239
|
+
AgentType.TASKS,
|
|
240
|
+
),
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
for title, help_text, callback, mode in commands:
|
|
244
|
+
if self.chat_screen.mode == mode:
|
|
245
|
+
continue
|
|
246
|
+
score = matcher.match(title)
|
|
247
|
+
if score > 0:
|
|
248
|
+
yield Hit(score, matcher.highlight(title), callback, help=help_text)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ProviderSetupProvider(Provider):
|
|
252
|
+
"""Command palette entries for provider configuration."""
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def chat_screen(self) -> "ChatScreen":
|
|
256
|
+
return cast(ChatScreen, self.screen)
|
|
257
|
+
|
|
258
|
+
def open_provider_config(self) -> None:
|
|
259
|
+
"""Show the provider configuration screen."""
|
|
260
|
+
self.chat_screen.app.push_screen("provider_config")
|
|
261
|
+
|
|
262
|
+
async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
|
|
263
|
+
yield DiscoveryHit(
|
|
264
|
+
"Open Provider Setup",
|
|
265
|
+
self.open_provider_config,
|
|
266
|
+
help="⚙️ Manage API keys for available providers",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
270
|
+
matcher = self.matcher(query)
|
|
271
|
+
title = "Open Provider Setup"
|
|
272
|
+
score = matcher.match(title)
|
|
273
|
+
if score > 0:
|
|
274
|
+
yield Hit(
|
|
275
|
+
score,
|
|
276
|
+
matcher.highlight(title),
|
|
277
|
+
self.open_provider_config,
|
|
278
|
+
help="⚙️ Manage API keys for available providers",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ChatScreen(Screen[None]):
|
|
283
|
+
CSS_PATH = "chat.tcss"
|
|
284
|
+
|
|
285
|
+
BINDINGS = [
|
|
286
|
+
("ctrl+p", "command_palette", "Command Palette"),
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
COMMANDS = {AgentModeProvider, ProviderSetupProvider}
|
|
290
|
+
|
|
291
|
+
value = reactive("")
|
|
292
|
+
mode = reactive(AgentType.RESEARCH, recompose=True)
|
|
293
|
+
history: PromptHistory = PromptHistory()
|
|
294
|
+
messages = reactive(list[ModelMessage]())
|
|
295
|
+
working = reactive(False)
|
|
296
|
+
question: reactive[UserQuestion | None] = reactive(None)
|
|
297
|
+
|
|
298
|
+
def __init__(self) -> None:
|
|
299
|
+
super().__init__()
|
|
300
|
+
# Get the model configuration and codebase service
|
|
301
|
+
model_config = get_provider_model()
|
|
302
|
+
codebase_service = get_codebase_service()
|
|
303
|
+
self.deps = AgentDeps(
|
|
304
|
+
interactive_mode=True,
|
|
305
|
+
llm_model=model_config,
|
|
306
|
+
codebase_service=codebase_service,
|
|
307
|
+
)
|
|
308
|
+
self.agent_manager = AgentManager(deps=self.deps, initial_type=self.mode)
|
|
309
|
+
|
|
310
|
+
def on_mount(self) -> None:
|
|
311
|
+
self.query_one(PromptInput).focus(scroll_visible=True)
|
|
312
|
+
# Hide spinner initially
|
|
313
|
+
self.query_one("#spinner").display = False
|
|
314
|
+
|
|
315
|
+
def watch_mode(self, new_mode: AgentType) -> None:
|
|
316
|
+
"""React to mode changes by updating the agent manager."""
|
|
317
|
+
if hasattr(self, "agent_manager"):
|
|
318
|
+
self.agent_manager.set_agent(new_mode)
|
|
319
|
+
|
|
320
|
+
def watch_working(self, is_working: bool) -> None:
|
|
321
|
+
"""Show or hide the spinner based on working state."""
|
|
322
|
+
if self.is_mounted:
|
|
323
|
+
spinner = self.query_one("#spinner")
|
|
324
|
+
spinner.display = is_working
|
|
325
|
+
|
|
326
|
+
def watch_messages(self, messages: list[ModelMessage]) -> None:
|
|
327
|
+
"""Update the chat history when messages change."""
|
|
328
|
+
if self.is_mounted:
|
|
329
|
+
chat_history = self.query_one(ChatHistory)
|
|
330
|
+
chat_history.update_messages(messages)
|
|
331
|
+
|
|
332
|
+
def watch_question(self, question: UserQuestion | None) -> None:
|
|
333
|
+
"""Update the question display."""
|
|
334
|
+
if self.is_mounted:
|
|
335
|
+
question_display = self.query_one("#question-display", Markdown)
|
|
336
|
+
if question:
|
|
337
|
+
question_display.update(f"Question:\n\n{question.question}")
|
|
338
|
+
question_display.display = True
|
|
339
|
+
else:
|
|
340
|
+
question_display.update("")
|
|
341
|
+
question_display.display = False
|
|
342
|
+
|
|
343
|
+
@work
|
|
344
|
+
async def add_question_listener(self) -> None:
|
|
345
|
+
while True:
|
|
346
|
+
question = await self.deps.queue.get()
|
|
347
|
+
self.question = question
|
|
348
|
+
await question.result
|
|
349
|
+
self.deps.queue.task_done()
|
|
350
|
+
|
|
351
|
+
def compose(self) -> ComposeResult:
|
|
352
|
+
"""Create child widgets for the app."""
|
|
353
|
+
with Container(id="window"):
|
|
354
|
+
yield ChatHistory()
|
|
355
|
+
yield Markdown(markdown="", id="question-display")
|
|
356
|
+
yield self.agent_manager
|
|
357
|
+
with Container(id="footer"):
|
|
358
|
+
yield Spinner(text="Processing...", id="spinner")
|
|
359
|
+
yield StatusBar()
|
|
360
|
+
yield PromptInput(
|
|
361
|
+
text=self.value,
|
|
362
|
+
highlight_cursor_line=False,
|
|
363
|
+
id="prompt-input",
|
|
364
|
+
placeholder="Type your message",
|
|
365
|
+
)
|
|
366
|
+
yield ModeIndicator(mode=self.mode)
|
|
367
|
+
|
|
368
|
+
@on(MessageHistoryUpdated)
|
|
369
|
+
def handle_message_history_updated(self, event: MessageHistoryUpdated) -> None:
|
|
370
|
+
"""Handle message history updates from the agent manager."""
|
|
371
|
+
self.messages = event.messages
|
|
372
|
+
|
|
373
|
+
@on(PromptInput.Submitted)
|
|
374
|
+
async def handle_submit(self, message: PromptInput.Submitted) -> None:
|
|
375
|
+
self.history.append(message.text)
|
|
376
|
+
|
|
377
|
+
# Clear the input
|
|
378
|
+
self.value = ""
|
|
379
|
+
self.run_agent(message.text)
|
|
380
|
+
|
|
381
|
+
prompt_input = self.query_one(PromptInput)
|
|
382
|
+
prompt_input.clear()
|
|
383
|
+
prompt_input.focus()
|
|
384
|
+
|
|
385
|
+
@work
|
|
386
|
+
async def run_agent(self, message: str) -> None:
|
|
387
|
+
deferred_tool_results = None
|
|
388
|
+
prompt = None
|
|
389
|
+
self.working = True
|
|
390
|
+
|
|
391
|
+
if self.question:
|
|
392
|
+
# This is a response to a question from the agent
|
|
393
|
+
self.question.result.set_result(
|
|
394
|
+
UserAnswer(answer=message, tool_call_id=self.question.tool_call_id)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
deferred_tool_results = DeferredToolResults()
|
|
398
|
+
|
|
399
|
+
deferred_tool_results.calls[self.question.tool_call_id] = UserAnswer(
|
|
400
|
+
answer=message, tool_call_id=self.question.tool_call_id
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
self.question = None
|
|
404
|
+
else:
|
|
405
|
+
# This is a new user prompt
|
|
406
|
+
prompt = message
|
|
407
|
+
|
|
408
|
+
await self.agent_manager.run(
|
|
409
|
+
prompt=prompt,
|
|
410
|
+
deferred_tool_results=deferred_tool_results,
|
|
411
|
+
)
|
|
412
|
+
self.working = False
|
|
413
|
+
|
|
414
|
+
prompt_input = self.query_one(PromptInput)
|
|
415
|
+
prompt_input.focus()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
ChatHistory {
|
|
2
|
+
height: auto;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
PromptInput {
|
|
6
|
+
min-height: 3;
|
|
7
|
+
max-height: 7;
|
|
8
|
+
height: auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
StatusBar {
|
|
12
|
+
height: auto;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ModeIndicator {
|
|
16
|
+
height: auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#footer {
|
|
20
|
+
dock: bottom;
|
|
21
|
+
height: auto;
|
|
22
|
+
padding: 1 1 1 2;
|
|
23
|
+
max-height: 14;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#window {
|
|
27
|
+
align: left bottom;
|
|
28
|
+
}
|