batrachian-toad 0.5.22__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.
- batrachian_toad-0.5.22.dist-info/METADATA +197 -0
- batrachian_toad-0.5.22.dist-info/RECORD +120 -0
- batrachian_toad-0.5.22.dist-info/WHEEL +4 -0
- batrachian_toad-0.5.22.dist-info/entry_points.txt +2 -0
- batrachian_toad-0.5.22.dist-info/licenses/LICENSE +661 -0
- toad/__init__.py +46 -0
- toad/__main__.py +4 -0
- toad/_loop.py +86 -0
- toad/about.py +90 -0
- toad/acp/agent.py +671 -0
- toad/acp/api.py +47 -0
- toad/acp/encode_tool_call_id.py +12 -0
- toad/acp/messages.py +138 -0
- toad/acp/prompt.py +54 -0
- toad/acp/protocol.py +426 -0
- toad/agent.py +62 -0
- toad/agent_schema.py +70 -0
- toad/agents.py +45 -0
- toad/ansi/__init__.py +1 -0
- toad/ansi/_ansi.py +1612 -0
- toad/ansi/_ansi_colors.py +264 -0
- toad/ansi/_control_codes.py +37 -0
- toad/ansi/_keys.py +251 -0
- toad/ansi/_sgr_styles.py +64 -0
- toad/ansi/_stream_parser.py +418 -0
- toad/answer.py +22 -0
- toad/app.py +557 -0
- toad/atomic.py +37 -0
- toad/cli.py +257 -0
- toad/code_analyze.py +28 -0
- toad/complete.py +34 -0
- toad/constants.py +58 -0
- toad/conversation_markdown.py +19 -0
- toad/danger.py +371 -0
- toad/data/agents/ampcode.com.toml +51 -0
- toad/data/agents/augmentcode.com.toml +40 -0
- toad/data/agents/claude.com.toml +41 -0
- toad/data/agents/docker.com.toml +59 -0
- toad/data/agents/geminicli.com.toml +28 -0
- toad/data/agents/goose.ai.toml +51 -0
- toad/data/agents/inference.huggingface.co.toml +33 -0
- toad/data/agents/kimi.com.toml +35 -0
- toad/data/agents/openai.com.toml +53 -0
- toad/data/agents/opencode.ai.toml +61 -0
- toad/data/agents/openhands.dev.toml +44 -0
- toad/data/agents/stakpak.dev.toml +61 -0
- toad/data/agents/vibe.mistral.ai.toml +27 -0
- toad/data/agents/vtcode.dev.toml +62 -0
- toad/data/images/frog.png +0 -0
- toad/data/sounds/turn-over.wav +0 -0
- toad/db.py +5 -0
- toad/dec.py +332 -0
- toad/directory.py +234 -0
- toad/directory_watcher.py +96 -0
- toad/fuzzy.py +140 -0
- toad/gist.py +2 -0
- toad/history.py +138 -0
- toad/jsonrpc.py +576 -0
- toad/menus.py +14 -0
- toad/messages.py +74 -0
- toad/option_content.py +51 -0
- toad/os.py +0 -0
- toad/path_complete.py +145 -0
- toad/path_filter.py +124 -0
- toad/paths.py +71 -0
- toad/pill.py +23 -0
- toad/prompt/extract.py +19 -0
- toad/prompt/resource.py +68 -0
- toad/protocol.py +28 -0
- toad/screens/action_modal.py +94 -0
- toad/screens/agent_modal.py +172 -0
- toad/screens/command_edit_modal.py +58 -0
- toad/screens/main.py +192 -0
- toad/screens/permissions.py +390 -0
- toad/screens/permissions.tcss +72 -0
- toad/screens/settings.py +254 -0
- toad/screens/settings.tcss +101 -0
- toad/screens/store.py +476 -0
- toad/screens/store.tcss +261 -0
- toad/settings.py +354 -0
- toad/settings_schema.py +318 -0
- toad/shell.py +263 -0
- toad/shell_read.py +42 -0
- toad/slash_command.py +34 -0
- toad/toad.tcss +752 -0
- toad/version.py +80 -0
- toad/visuals/columns.py +273 -0
- toad/widgets/agent_response.py +79 -0
- toad/widgets/agent_thought.py +41 -0
- toad/widgets/command_pane.py +224 -0
- toad/widgets/condensed_path.py +93 -0
- toad/widgets/conversation.py +1626 -0
- toad/widgets/danger_warning.py +65 -0
- toad/widgets/diff_view.py +709 -0
- toad/widgets/flash.py +81 -0
- toad/widgets/future_text.py +126 -0
- toad/widgets/grid_select.py +223 -0
- toad/widgets/highlighted_textarea.py +180 -0
- toad/widgets/mandelbrot.py +294 -0
- toad/widgets/markdown_note.py +13 -0
- toad/widgets/menu.py +147 -0
- toad/widgets/non_selectable_label.py +5 -0
- toad/widgets/note.py +18 -0
- toad/widgets/path_search.py +381 -0
- toad/widgets/plan.py +180 -0
- toad/widgets/project_directory_tree.py +74 -0
- toad/widgets/prompt.py +741 -0
- toad/widgets/question.py +337 -0
- toad/widgets/shell_result.py +35 -0
- toad/widgets/shell_terminal.py +18 -0
- toad/widgets/side_bar.py +74 -0
- toad/widgets/slash_complete.py +211 -0
- toad/widgets/strike_text.py +66 -0
- toad/widgets/terminal.py +526 -0
- toad/widgets/terminal_tool.py +338 -0
- toad/widgets/throbber.py +90 -0
- toad/widgets/tool_call.py +303 -0
- toad/widgets/user_input.py +23 -0
- toad/widgets/version.py +5 -0
- toad/widgets/welcome.py +31 -0
toad/screens/store.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from itertools import zip_longest
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal, Self
|
|
7
|
+
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.screen import Screen
|
|
10
|
+
from textual import events
|
|
11
|
+
from textual import work
|
|
12
|
+
from textual import getters
|
|
13
|
+
from textual import on
|
|
14
|
+
from textual.app import ComposeResult
|
|
15
|
+
from textual.content import Content
|
|
16
|
+
from textual.css.query import NoMatches
|
|
17
|
+
from textual.message import Message
|
|
18
|
+
from textual import containers
|
|
19
|
+
from textual import widgets
|
|
20
|
+
|
|
21
|
+
import toad
|
|
22
|
+
from toad.app import ToadApp
|
|
23
|
+
from toad.pill import pill
|
|
24
|
+
from toad.widgets.mandelbrot import Mandelbrot
|
|
25
|
+
from toad.widgets.grid_select import GridSelect
|
|
26
|
+
from toad.agent_schema import Agent
|
|
27
|
+
from toad.agents import read_agents
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
QR = """\
|
|
31
|
+
█▀▀▀▀▀█ ▄█ ▄▄█▄▄█ █▀▀▀▀▀█
|
|
32
|
+
█ ███ █ ▄█▀█▄▄█▄ █ ███ █
|
|
33
|
+
█ ▀▀▀ █ ▄ █ ▀▀▄▄▀ █ ▀▀▀ █
|
|
34
|
+
▀▀▀▀▀▀▀ ▀ ▀ ▀ █ █ ▀▀▀▀▀▀▀
|
|
35
|
+
█▀██▀ ▀█▀█▀▄▄█ ▀ █ ▀ █
|
|
36
|
+
█ ▀▄▄▀▄▄█▄▄█▀██▄▄▄▄ ▀ ▀█
|
|
37
|
+
▄▀▄▀▀▄▀ █▀▄▄▄▀▄ ▄▀▀█▀▄▀█▀
|
|
38
|
+
█ ▄ ▀▀▀█▀ █ ▀ █▀ ▀ ██▀ ▀█
|
|
39
|
+
▀ ▀▀ ▀▀▄▀▄▄▀▀▄▀█▀▀▀█▄▀
|
|
40
|
+
█▀▀▀▀▀█ ▀▄█▄▀▀ █ ▀ █▄▀▀█
|
|
41
|
+
█ ███ █ ██▄▄▀▀█▀▀██▀█▄██▄
|
|
42
|
+
█ ▀▀▀ █ ██▄▄ ▀ ▄▀ ▄▄█▀ █
|
|
43
|
+
▀▀▀▀▀▀▀ ▀▀▀ ▀ ▀▀▀▀▀▀▀▀"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class LaunchAgent(Message):
|
|
48
|
+
identity: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AgentItem(containers.VerticalGroup):
|
|
52
|
+
"""An entry in the Agent grid select."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, agent: Agent) -> None:
|
|
55
|
+
self._agent = agent
|
|
56
|
+
super().__init__()
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def agent(self) -> Agent:
|
|
60
|
+
return self._agent
|
|
61
|
+
|
|
62
|
+
def compose(self) -> ComposeResult:
|
|
63
|
+
agent = self._agent
|
|
64
|
+
with containers.Grid():
|
|
65
|
+
yield widgets.Label(agent["name"], id="name")
|
|
66
|
+
tag = pill(agent["type"], "$secondary-muted", "$text-primary")
|
|
67
|
+
yield widgets.Label(tag, id="type")
|
|
68
|
+
yield widgets.Label(agent["author_name"], id="author")
|
|
69
|
+
yield widgets.Static(agent["description"], id="description")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LauncherGridSelect(GridSelect):
|
|
73
|
+
BINDING_GROUP_TITLE = "Launcher"
|
|
74
|
+
|
|
75
|
+
app = getters.app(ToadApp)
|
|
76
|
+
BINDINGS = [
|
|
77
|
+
Binding(
|
|
78
|
+
"enter",
|
|
79
|
+
"select",
|
|
80
|
+
"Details",
|
|
81
|
+
tooltip="Open agent details",
|
|
82
|
+
),
|
|
83
|
+
Binding(
|
|
84
|
+
"space",
|
|
85
|
+
"launch",
|
|
86
|
+
"Launch",
|
|
87
|
+
tooltip="Launch highlighted agent",
|
|
88
|
+
),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
def action_details(self) -> None:
|
|
92
|
+
if self.highlighted is None:
|
|
93
|
+
return
|
|
94
|
+
agent_item = self.children[self.highlighted]
|
|
95
|
+
assert isinstance(agent_item, LauncherItem)
|
|
96
|
+
self.post_message(StoreScreen.OpenAgentDetails(agent_item._agent["identity"]))
|
|
97
|
+
|
|
98
|
+
def action_remove(self) -> None:
|
|
99
|
+
agents = self.app.settings.get("launcher.agents", str).splitlines()
|
|
100
|
+
if self.highlighted is None:
|
|
101
|
+
return
|
|
102
|
+
try:
|
|
103
|
+
del agents[self.highlighted]
|
|
104
|
+
except IndexError:
|
|
105
|
+
pass
|
|
106
|
+
else:
|
|
107
|
+
self.app.settings.set("launcher.agents", "\n".join(agents))
|
|
108
|
+
|
|
109
|
+
def action_launch(self) -> None:
|
|
110
|
+
if self.highlighted is None:
|
|
111
|
+
return
|
|
112
|
+
child = self.children[self.highlighted]
|
|
113
|
+
assert isinstance(child, LauncherItem)
|
|
114
|
+
self.post_message(LaunchAgent(child.agent["identity"]))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Launcher(containers.VerticalGroup):
|
|
118
|
+
app = getters.app(ToadApp)
|
|
119
|
+
grid_select = getters.query_one("#launcher-grid-select", GridSelect)
|
|
120
|
+
DIGITS = "123456789ABCDEF"
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
agents: dict[str, Agent],
|
|
125
|
+
*,
|
|
126
|
+
name: str | None = None,
|
|
127
|
+
id: str | None = None,
|
|
128
|
+
classes: str | None = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
self._agents = agents
|
|
131
|
+
super().__init__(name=name, id=id, classes=classes)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def highlighted(self) -> int | None:
|
|
135
|
+
return self.grid_select.highlighted
|
|
136
|
+
|
|
137
|
+
@highlighted.setter
|
|
138
|
+
def highlighted(self, value: int) -> None:
|
|
139
|
+
self.grid_select.highlighted = value
|
|
140
|
+
|
|
141
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
142
|
+
try:
|
|
143
|
+
self.grid_select.focus(scroll_visible=scroll_visible)
|
|
144
|
+
except NoMatches:
|
|
145
|
+
pass
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def compose(self) -> ComposeResult:
|
|
149
|
+
launcher_agents = list(
|
|
150
|
+
dict.fromkeys(
|
|
151
|
+
identity
|
|
152
|
+
for identity in self.app.settings.get(
|
|
153
|
+
"launcher.agents", str
|
|
154
|
+
).splitlines()
|
|
155
|
+
if identity.strip()
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
agents = self._agents
|
|
159
|
+
self.set_class(not launcher_agents, "-empty")
|
|
160
|
+
if launcher_agents:
|
|
161
|
+
with LauncherGridSelect(
|
|
162
|
+
id="launcher-grid-select", min_column_width=32, max_column_width=32
|
|
163
|
+
):
|
|
164
|
+
for digit, identity in zip_longest(self.DIGITS, launcher_agents):
|
|
165
|
+
if identity is None:
|
|
166
|
+
break
|
|
167
|
+
yield LauncherItem(digit or "", agents[identity])
|
|
168
|
+
|
|
169
|
+
if not launcher_agents:
|
|
170
|
+
yield widgets.Label("Chose your fighter below!", classes="no-agents")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class LauncherItem(containers.VerticalGroup):
|
|
174
|
+
"""An entry in the Agent grid select."""
|
|
175
|
+
|
|
176
|
+
def __init__(self, digit: str, agent: Agent) -> None:
|
|
177
|
+
self._digit = digit
|
|
178
|
+
self._agent = agent
|
|
179
|
+
super().__init__()
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def agent(self) -> Agent:
|
|
183
|
+
return self._agent
|
|
184
|
+
|
|
185
|
+
def compose(self) -> ComposeResult:
|
|
186
|
+
agent = self._agent
|
|
187
|
+
with containers.HorizontalGroup():
|
|
188
|
+
if self._digit:
|
|
189
|
+
yield widgets.Digits(self._digit)
|
|
190
|
+
with containers.VerticalGroup():
|
|
191
|
+
yield widgets.Label(agent["name"], id="name")
|
|
192
|
+
yield widgets.Label(agent["author_name"], id="author")
|
|
193
|
+
yield widgets.Static(agent["description"], id="description")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class AgentGridSelect(GridSelect):
|
|
197
|
+
BINDINGS = [
|
|
198
|
+
Binding("enter", "select", "Details", tooltip="Open agent details"),
|
|
199
|
+
Binding("space", "launch", "Launch", tooltip="Launch highlighted agent"),
|
|
200
|
+
]
|
|
201
|
+
BINDING_GROUP_TITLE = "Agent Select"
|
|
202
|
+
|
|
203
|
+
def action_launch(self) -> None:
|
|
204
|
+
if self.highlighted is None:
|
|
205
|
+
return
|
|
206
|
+
child = self.children[self.highlighted]
|
|
207
|
+
assert isinstance(child, AgentItem)
|
|
208
|
+
self.post_message(LaunchAgent(child.agent["identity"]))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class Container(containers.VerticalScroll):
|
|
212
|
+
BINDING_GROUP_TITLE = "View"
|
|
213
|
+
|
|
214
|
+
def allow_focus(self) -> bool:
|
|
215
|
+
"""Only allow focus when we can scroll."""
|
|
216
|
+
return super().allow_focus() and self.show_vertical_scrollbar
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class StoreScreen(Screen):
|
|
220
|
+
BINDING_GROUP_TITLE = "Screen"
|
|
221
|
+
CSS_PATH = "store.tcss"
|
|
222
|
+
FOCUS_GROUP = Binding.Group("Focus")
|
|
223
|
+
BINDINGS = [
|
|
224
|
+
Binding(
|
|
225
|
+
"tab",
|
|
226
|
+
"app.focus_next",
|
|
227
|
+
"Focus Next",
|
|
228
|
+
group=FOCUS_GROUP,
|
|
229
|
+
),
|
|
230
|
+
Binding(
|
|
231
|
+
"shift+tab",
|
|
232
|
+
"app.focus_previous",
|
|
233
|
+
"Focus Previous",
|
|
234
|
+
group=FOCUS_GROUP,
|
|
235
|
+
),
|
|
236
|
+
Binding(
|
|
237
|
+
"null",
|
|
238
|
+
"quick_launch",
|
|
239
|
+
"Quick launch",
|
|
240
|
+
key_display="1-9 a-f",
|
|
241
|
+
),
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
agents_view = getters.query_one("#agents-view", AgentGridSelect)
|
|
245
|
+
launcher = getters.query_one("#launcher", Launcher)
|
|
246
|
+
container = getters.query_one("#container", Container)
|
|
247
|
+
|
|
248
|
+
app = getters.app(ToadApp)
|
|
249
|
+
|
|
250
|
+
@dataclass
|
|
251
|
+
class OpenAgentDetails(Message):
|
|
252
|
+
identity: str
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self, name: str | None = None, id: str | None = None, classes: str | None = None
|
|
256
|
+
):
|
|
257
|
+
self._agents: dict[str, Agent] = {}
|
|
258
|
+
super().__init__(name=name, id=id, classes=classes)
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def agents(self) -> dict[str, Agent]:
|
|
262
|
+
return self._agents
|
|
263
|
+
|
|
264
|
+
def compose(self) -> ComposeResult:
|
|
265
|
+
with containers.VerticalGroup(id="title-container"):
|
|
266
|
+
with containers.Grid(id="title-grid"):
|
|
267
|
+
yield Mandelbrot()
|
|
268
|
+
yield widgets.Label(self.get_info(), id="info")
|
|
269
|
+
|
|
270
|
+
yield Container(id="container", can_focus=False)
|
|
271
|
+
yield widgets.Footer()
|
|
272
|
+
|
|
273
|
+
def get_info(self) -> Content:
|
|
274
|
+
toad_version = toad.get_version()
|
|
275
|
+
content = Content.assemble(
|
|
276
|
+
Content.from_markup("🐸 Toad"),
|
|
277
|
+
pill(f"v{toad_version}", "$primary-muted", "$text-primary"),
|
|
278
|
+
("\nThe universal interface for AI in your terminal", "$text-success"),
|
|
279
|
+
(
|
|
280
|
+
"\nSoftware lovingly crafted by hand (with a dash of AI) in Edinburgh, Scotland",
|
|
281
|
+
"dim",
|
|
282
|
+
),
|
|
283
|
+
"\n",
|
|
284
|
+
(
|
|
285
|
+
Content.from_markup(
|
|
286
|
+
"\nConsider sponsoring [@click=screen.url('https://github.com/sponsors/willmcgugan')]@willmcgugan[/] to support future updates"
|
|
287
|
+
)
|
|
288
|
+
),
|
|
289
|
+
"\n\n",
|
|
290
|
+
(
|
|
291
|
+
Content.from_markup(
|
|
292
|
+
"[dim]Code: [@click=screen.url('https://github.com/batrachianai/toad')]Repository[/] "
|
|
293
|
+
"Bugs: [@click=screen.url('https://github.com/batrachianai/toad/discussions')]Discussions[/]"
|
|
294
|
+
)
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return content
|
|
299
|
+
|
|
300
|
+
def action_url(self, url: str) -> None:
|
|
301
|
+
import webbrowser
|
|
302
|
+
|
|
303
|
+
webbrowser.open(url)
|
|
304
|
+
|
|
305
|
+
def compose_agents(self) -> ComposeResult:
|
|
306
|
+
agents = self._agents
|
|
307
|
+
|
|
308
|
+
yield Launcher(agents, id="launcher")
|
|
309
|
+
|
|
310
|
+
ordered_agents = sorted(
|
|
311
|
+
agents.values(), key=lambda agent: agent["name"].casefold()
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
recommended_agents = [
|
|
315
|
+
agent for agent in ordered_agents if agent.get("recommended", False)
|
|
316
|
+
]
|
|
317
|
+
if recommended_agents:
|
|
318
|
+
with containers.VerticalGroup(id="sponsored-agents", classes="recommended"):
|
|
319
|
+
yield widgets.Static("Recommended", classes="heading")
|
|
320
|
+
with AgentGridSelect(classes="agents-picker", min_column_width=40):
|
|
321
|
+
for agent in recommended_agents:
|
|
322
|
+
yield AgentItem(agent)
|
|
323
|
+
|
|
324
|
+
coding_agents = [agent for agent in ordered_agents if agent["type"] == "coding"]
|
|
325
|
+
if coding_agents:
|
|
326
|
+
yield widgets.Static("Coding agents", classes="heading")
|
|
327
|
+
with AgentGridSelect(classes="agents-picker", min_column_width=40):
|
|
328
|
+
for agent in coding_agents:
|
|
329
|
+
yield AgentItem(agent)
|
|
330
|
+
|
|
331
|
+
chat_bots = [agent for agent in ordered_agents if agent["type"] == "chat"]
|
|
332
|
+
if chat_bots:
|
|
333
|
+
yield widgets.Static("Chat & more", classes="heading")
|
|
334
|
+
with AgentGridSelect(classes="agents-picker", min_column_width=40):
|
|
335
|
+
for agent in chat_bots:
|
|
336
|
+
yield AgentItem(agent)
|
|
337
|
+
|
|
338
|
+
def move_focus(self, direction: Literal[-1] | Literal[+1]) -> None:
|
|
339
|
+
if isinstance(self.focused, GridSelect):
|
|
340
|
+
focus_chain = list(self.query(GridSelect))
|
|
341
|
+
if self.focused in focus_chain:
|
|
342
|
+
index = focus_chain.index(self.focused)
|
|
343
|
+
new_focus = focus_chain[(index + direction) % len(focus_chain)]
|
|
344
|
+
if direction == -1:
|
|
345
|
+
new_focus.highlight_last()
|
|
346
|
+
else:
|
|
347
|
+
new_focus.highlight_first()
|
|
348
|
+
new_focus.focus(scroll_visible=False)
|
|
349
|
+
|
|
350
|
+
@on(GridSelect.LeaveUp)
|
|
351
|
+
def on_grid_select_leave_up(self, event: GridSelect.LeaveUp):
|
|
352
|
+
event.stop()
|
|
353
|
+
self.move_focus(-1)
|
|
354
|
+
|
|
355
|
+
@on(GridSelect.LeaveDown)
|
|
356
|
+
def on_grid_select_leave_down(self, event: GridSelect.LeaveUp):
|
|
357
|
+
event.stop()
|
|
358
|
+
self.move_focus(+1)
|
|
359
|
+
|
|
360
|
+
@on(GridSelect.Selected, ".agents-picker")
|
|
361
|
+
@work
|
|
362
|
+
async def on_grid_select_selected(self, event: GridSelect.Selected):
|
|
363
|
+
assert isinstance(event.selected_widget, AgentItem)
|
|
364
|
+
from toad.screens.agent_modal import AgentModal
|
|
365
|
+
|
|
366
|
+
modal_response = await self.app.push_screen_wait(
|
|
367
|
+
AgentModal(event.selected_widget.agent)
|
|
368
|
+
)
|
|
369
|
+
self.app.save_settings()
|
|
370
|
+
if modal_response == "launch":
|
|
371
|
+
self.post_message(LaunchAgent(event.selected_widget.agent["identity"]))
|
|
372
|
+
|
|
373
|
+
@on(OpenAgentDetails)
|
|
374
|
+
@work
|
|
375
|
+
async def open_agent_detail(self, message: OpenAgentDetails) -> None:
|
|
376
|
+
from toad.screens.agent_modal import AgentModal
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
agent = self._agents[message.identity]
|
|
380
|
+
except KeyError:
|
|
381
|
+
return
|
|
382
|
+
modal_response = await self.app.push_screen_wait(AgentModal(agent))
|
|
383
|
+
self.app.save_settings()
|
|
384
|
+
if modal_response == "launch":
|
|
385
|
+
self.post_message(LaunchAgent(agent["identity"]))
|
|
386
|
+
|
|
387
|
+
@on(GridSelect.Selected, "#launcher GridSelect")
|
|
388
|
+
@work
|
|
389
|
+
async def on_launcher_selected(self, event: GridSelect.Selected):
|
|
390
|
+
launcher_item = event.selected_widget
|
|
391
|
+
assert isinstance(launcher_item, LauncherItem)
|
|
392
|
+
|
|
393
|
+
from toad.screens.agent_modal import AgentModal
|
|
394
|
+
|
|
395
|
+
modal_response = await self.app.push_screen_wait(
|
|
396
|
+
AgentModal(launcher_item.agent)
|
|
397
|
+
)
|
|
398
|
+
self.app.save_settings()
|
|
399
|
+
if modal_response == "launch":
|
|
400
|
+
self.post_message(LaunchAgent(launcher_item.agent["identity"]))
|
|
401
|
+
|
|
402
|
+
@work
|
|
403
|
+
async def launch_agent(self, agent_identity: str) -> None:
|
|
404
|
+
from toad.screens.main import MainScreen
|
|
405
|
+
|
|
406
|
+
agent = self.agents[agent_identity]
|
|
407
|
+
project_path = Path(self.app.project_dir or os.getcwd())
|
|
408
|
+
screen = MainScreen(project_path, agent).data_bind(
|
|
409
|
+
column=ToadApp.column,
|
|
410
|
+
column_width=ToadApp.column_width,
|
|
411
|
+
)
|
|
412
|
+
await self.app.push_screen_wait(screen)
|
|
413
|
+
|
|
414
|
+
@on(LaunchAgent)
|
|
415
|
+
def on_launch_agent(self, message: LaunchAgent) -> None:
|
|
416
|
+
self.launch_agent(message.identity)
|
|
417
|
+
|
|
418
|
+
@work
|
|
419
|
+
async def on_mount(self) -> None:
|
|
420
|
+
self.app.settings_changed_signal.subscribe(self, self.setting_updated)
|
|
421
|
+
try:
|
|
422
|
+
self._agents = await read_agents()
|
|
423
|
+
except Exception as error:
|
|
424
|
+
self.notify(
|
|
425
|
+
f"Failed to read agents data ({error})",
|
|
426
|
+
title="Agents data",
|
|
427
|
+
severity="error",
|
|
428
|
+
)
|
|
429
|
+
else:
|
|
430
|
+
await self.container.mount_compose(self.compose_agents())
|
|
431
|
+
with suppress(NoMatches):
|
|
432
|
+
first_grid = self.container.query(GridSelect).first()
|
|
433
|
+
first_grid.focus(scroll_visible=False)
|
|
434
|
+
|
|
435
|
+
async def setting_updated(self, setting: tuple[str, object]) -> None:
|
|
436
|
+
key, value = setting
|
|
437
|
+
if key == "launcher.agents":
|
|
438
|
+
await self.launcher.recompose()
|
|
439
|
+
|
|
440
|
+
def focus_screen():
|
|
441
|
+
try:
|
|
442
|
+
self.screen.query(GridSelect).focus()
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
self.call_later(focus_screen)
|
|
447
|
+
|
|
448
|
+
def on_key(self, event: events.Key) -> None:
|
|
449
|
+
if event.character is None:
|
|
450
|
+
return
|
|
451
|
+
LAUNCHER_KEYS = "123456789abcdef"
|
|
452
|
+
if event.character in LAUNCHER_KEYS:
|
|
453
|
+
launch_item_offset = LAUNCHER_KEYS.find(event.character)
|
|
454
|
+
try:
|
|
455
|
+
self.launcher.grid_select.children[launch_item_offset]
|
|
456
|
+
except IndexError:
|
|
457
|
+
self.notify(
|
|
458
|
+
f"No agent on key [b]{LAUNCHER_KEYS[launch_item_offset]}",
|
|
459
|
+
title="Quick launch",
|
|
460
|
+
severity="error",
|
|
461
|
+
)
|
|
462
|
+
self.app.bell()
|
|
463
|
+
return
|
|
464
|
+
self.launcher.focus()
|
|
465
|
+
self.launcher.highlighted = launch_item_offset
|
|
466
|
+
|
|
467
|
+
def action_quick_launch(self) -> None:
|
|
468
|
+
self.launcher.focus()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
if __name__ == "__main__":
|
|
472
|
+
from toad.app import ToadApp
|
|
473
|
+
|
|
474
|
+
app = ToadApp(mode="store")
|
|
475
|
+
|
|
476
|
+
app.run()
|