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.
Files changed (120) hide show
  1. batrachian_toad-0.5.22.dist-info/METADATA +197 -0
  2. batrachian_toad-0.5.22.dist-info/RECORD +120 -0
  3. batrachian_toad-0.5.22.dist-info/WHEEL +4 -0
  4. batrachian_toad-0.5.22.dist-info/entry_points.txt +2 -0
  5. batrachian_toad-0.5.22.dist-info/licenses/LICENSE +661 -0
  6. toad/__init__.py +46 -0
  7. toad/__main__.py +4 -0
  8. toad/_loop.py +86 -0
  9. toad/about.py +90 -0
  10. toad/acp/agent.py +671 -0
  11. toad/acp/api.py +47 -0
  12. toad/acp/encode_tool_call_id.py +12 -0
  13. toad/acp/messages.py +138 -0
  14. toad/acp/prompt.py +54 -0
  15. toad/acp/protocol.py +426 -0
  16. toad/agent.py +62 -0
  17. toad/agent_schema.py +70 -0
  18. toad/agents.py +45 -0
  19. toad/ansi/__init__.py +1 -0
  20. toad/ansi/_ansi.py +1612 -0
  21. toad/ansi/_ansi_colors.py +264 -0
  22. toad/ansi/_control_codes.py +37 -0
  23. toad/ansi/_keys.py +251 -0
  24. toad/ansi/_sgr_styles.py +64 -0
  25. toad/ansi/_stream_parser.py +418 -0
  26. toad/answer.py +22 -0
  27. toad/app.py +557 -0
  28. toad/atomic.py +37 -0
  29. toad/cli.py +257 -0
  30. toad/code_analyze.py +28 -0
  31. toad/complete.py +34 -0
  32. toad/constants.py +58 -0
  33. toad/conversation_markdown.py +19 -0
  34. toad/danger.py +371 -0
  35. toad/data/agents/ampcode.com.toml +51 -0
  36. toad/data/agents/augmentcode.com.toml +40 -0
  37. toad/data/agents/claude.com.toml +41 -0
  38. toad/data/agents/docker.com.toml +59 -0
  39. toad/data/agents/geminicli.com.toml +28 -0
  40. toad/data/agents/goose.ai.toml +51 -0
  41. toad/data/agents/inference.huggingface.co.toml +33 -0
  42. toad/data/agents/kimi.com.toml +35 -0
  43. toad/data/agents/openai.com.toml +53 -0
  44. toad/data/agents/opencode.ai.toml +61 -0
  45. toad/data/agents/openhands.dev.toml +44 -0
  46. toad/data/agents/stakpak.dev.toml +61 -0
  47. toad/data/agents/vibe.mistral.ai.toml +27 -0
  48. toad/data/agents/vtcode.dev.toml +62 -0
  49. toad/data/images/frog.png +0 -0
  50. toad/data/sounds/turn-over.wav +0 -0
  51. toad/db.py +5 -0
  52. toad/dec.py +332 -0
  53. toad/directory.py +234 -0
  54. toad/directory_watcher.py +96 -0
  55. toad/fuzzy.py +140 -0
  56. toad/gist.py +2 -0
  57. toad/history.py +138 -0
  58. toad/jsonrpc.py +576 -0
  59. toad/menus.py +14 -0
  60. toad/messages.py +74 -0
  61. toad/option_content.py +51 -0
  62. toad/os.py +0 -0
  63. toad/path_complete.py +145 -0
  64. toad/path_filter.py +124 -0
  65. toad/paths.py +71 -0
  66. toad/pill.py +23 -0
  67. toad/prompt/extract.py +19 -0
  68. toad/prompt/resource.py +68 -0
  69. toad/protocol.py +28 -0
  70. toad/screens/action_modal.py +94 -0
  71. toad/screens/agent_modal.py +172 -0
  72. toad/screens/command_edit_modal.py +58 -0
  73. toad/screens/main.py +192 -0
  74. toad/screens/permissions.py +390 -0
  75. toad/screens/permissions.tcss +72 -0
  76. toad/screens/settings.py +254 -0
  77. toad/screens/settings.tcss +101 -0
  78. toad/screens/store.py +476 -0
  79. toad/screens/store.tcss +261 -0
  80. toad/settings.py +354 -0
  81. toad/settings_schema.py +318 -0
  82. toad/shell.py +263 -0
  83. toad/shell_read.py +42 -0
  84. toad/slash_command.py +34 -0
  85. toad/toad.tcss +752 -0
  86. toad/version.py +80 -0
  87. toad/visuals/columns.py +273 -0
  88. toad/widgets/agent_response.py +79 -0
  89. toad/widgets/agent_thought.py +41 -0
  90. toad/widgets/command_pane.py +224 -0
  91. toad/widgets/condensed_path.py +93 -0
  92. toad/widgets/conversation.py +1626 -0
  93. toad/widgets/danger_warning.py +65 -0
  94. toad/widgets/diff_view.py +709 -0
  95. toad/widgets/flash.py +81 -0
  96. toad/widgets/future_text.py +126 -0
  97. toad/widgets/grid_select.py +223 -0
  98. toad/widgets/highlighted_textarea.py +180 -0
  99. toad/widgets/mandelbrot.py +294 -0
  100. toad/widgets/markdown_note.py +13 -0
  101. toad/widgets/menu.py +147 -0
  102. toad/widgets/non_selectable_label.py +5 -0
  103. toad/widgets/note.py +18 -0
  104. toad/widgets/path_search.py +381 -0
  105. toad/widgets/plan.py +180 -0
  106. toad/widgets/project_directory_tree.py +74 -0
  107. toad/widgets/prompt.py +741 -0
  108. toad/widgets/question.py +337 -0
  109. toad/widgets/shell_result.py +35 -0
  110. toad/widgets/shell_terminal.py +18 -0
  111. toad/widgets/side_bar.py +74 -0
  112. toad/widgets/slash_complete.py +211 -0
  113. toad/widgets/strike_text.py +66 -0
  114. toad/widgets/terminal.py +526 -0
  115. toad/widgets/terminal_tool.py +338 -0
  116. toad/widgets/throbber.py +90 -0
  117. toad/widgets/tool_call.py +303 -0
  118. toad/widgets/user_input.py +23 -0
  119. toad/widgets/version.py +5 -0
  120. toad/widgets/welcome.py +31 -0
@@ -0,0 +1,390 @@
1
+ import asyncio
2
+
3
+ import os
4
+ from typing import Awaitable, Callable
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual import containers
8
+
9
+ from textual import getters
10
+ from textual.binding import Binding
11
+ from textual.screen import Screen
12
+ from textual.reactive import var, Initialize
13
+
14
+
15
+ from toad.answer import Answer
16
+ from toad.widgets.question import Question
17
+ from toad.widgets.diff_view import DiffView
18
+
19
+ from textual.widgets import OptionList, Footer, Static, Select
20
+ from textual.widgets.option_list import Option
21
+
22
+ from toad.app import ToadApp
23
+
24
+ SOURCE1 = '''\
25
+ def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
26
+ """Iterate and generate a tuple with a flag for first value."""
27
+ iter_values = iter(values)
28
+ try:
29
+ value = next(iter_values)
30
+ except StopIteration:
31
+ return
32
+ yield True, value
33
+ for value in iter_values:
34
+ yield False, value
35
+
36
+
37
+ def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
38
+ """Iterate and generate a tuple with a flag for first and last value."""
39
+ iter_values = iter(values)
40
+ try:
41
+ previous_value = next(iter_values)
42
+ except StopIteration:
43
+ return
44
+ first = True
45
+ for value in iter_values:
46
+ yield first, False, previous_value
47
+ first = False
48
+ previous_value = value
49
+ yield first, True, previous_value
50
+
51
+ '''
52
+
53
+ SOURCE2 = '''\
54
+ def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
55
+ """Iterate and generate a tuple with a flag for first value.
56
+
57
+ Args:
58
+ values: iterables of values.
59
+
60
+ Returns:
61
+ Iterable of a boolean to indicate first value, and a value from the iterable.
62
+ """
63
+ iter_values = iter(values)
64
+ try:
65
+ value = next(iter_values)
66
+ except StopIteration:
67
+ return
68
+ yield True, value
69
+ for value in iter_values:
70
+ yield False, value
71
+
72
+
73
+ def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
74
+ """Iterate and generate a tuple with a flag for last value."""
75
+ iter_values = iter(values)
76
+ try:
77
+ previous_value = next(iter_values)
78
+ except StopIteration:
79
+ return
80
+ for value in iter_values:
81
+ yield False, previous_value
82
+ previous_value = value
83
+ yield True, previous_value
84
+
85
+
86
+ def loop_first_last(values: Iterable[ValueType]) -> Iterable[tuple[bool, bool, ValueType]]:
87
+ """Iterate and generate a tuple with a flag for first and last value."""
88
+ iter_values = iter(values)
89
+ try:
90
+ previous_value = next(iter_values) # Get previous value
91
+ except StopIteration:
92
+ return
93
+ first = True
94
+
95
+ '''
96
+
97
+
98
+ class PermissionsQuestion(Question):
99
+ BINDING_GROUP_TITLE = "Permissions Options"
100
+
101
+
102
+ class ChangesOptionList(OptionList):
103
+ BINDING_GROUP_TITLE = "Changes list"
104
+
105
+
106
+ class DiffViewSelect(Select):
107
+ BINDING_GROUP_TITLE = "Diff view select"
108
+
109
+
110
+ class ToolScroll(containers.VerticalScroll):
111
+ BINDING_GROUP_TITLE = "Changes window"
112
+
113
+
114
+ class PermissionsScreen(Screen[Answer]):
115
+ BINDING_GROUP_TITLE = "Permissions"
116
+ AUTO_FOCUS = "Question"
117
+ CSS_PATH = "permissions.tcss"
118
+
119
+ TAB_GROUP = Binding.Group("Focus")
120
+ NAVIGATION_GROUP = Binding.Group("Navigation", compact=True)
121
+ ALLOW_GROUP = Binding.Group("Allow once/always", compact=True)
122
+ REJECT_GROUP = Binding.Group("Reject once/always", compact=True)
123
+ BINDINGS = [
124
+ Binding("j", "next", "Next", group=NAVIGATION_GROUP),
125
+ Binding("k", "previous", "Previous", group=NAVIGATION_GROUP),
126
+ Binding(
127
+ "tab",
128
+ "app.focus_next",
129
+ "Focus next",
130
+ group=TAB_GROUP,
131
+ show=True,
132
+ priority=True,
133
+ ),
134
+ Binding(
135
+ "shift+tab",
136
+ "app.focus_previous",
137
+ "Focus previous",
138
+ group=TAB_GROUP,
139
+ show=True,
140
+ priority=True,
141
+ ),
142
+ Binding(
143
+ "a",
144
+ "select_kind(('allow_once', 'allow'))",
145
+ "Allow once",
146
+ group=ALLOW_GROUP,
147
+ priority=True,
148
+ ),
149
+ Binding(
150
+ "A",
151
+ "select_kind('allow_always')",
152
+ "Allow always",
153
+ group=ALLOW_GROUP,
154
+ priority=True,
155
+ ),
156
+ Binding(
157
+ "r",
158
+ "select_kind(('reject_once', 'reject'))",
159
+ "Reject once",
160
+ group=REJECT_GROUP,
161
+ priority=True,
162
+ ),
163
+ Binding(
164
+ "R",
165
+ "select_kind('reject_always')",
166
+ "Reject always",
167
+ group=REJECT_GROUP,
168
+ priority=True,
169
+ ),
170
+ ]
171
+
172
+ tool_container = getters.query_one("#tool-container", containers.VerticalScroll)
173
+ navigator = getters.query_one("#navigator", OptionList)
174
+ question = getters.query_one(PermissionsQuestion)
175
+ index: var[int] = var(0)
176
+
177
+ def __init__(
178
+ self,
179
+ options: list[Answer],
180
+ populate_callback: Callable[["PermissionsScreen"], Awaitable] | None,
181
+ *,
182
+ name: str | None = None,
183
+ id: str | None = None,
184
+ classes: str | None = None,
185
+ ):
186
+ super().__init__(name=name, id=id, classes=classes)
187
+ self.options = options
188
+ self.populate_callback = populate_callback
189
+
190
+ def get_diff_type(self) -> str:
191
+ app = self.app
192
+ diff_type = "auto"
193
+ if isinstance(app, ToadApp):
194
+ diff_type = app.settings.get("diff.view", str)
195
+ return diff_type
196
+
197
+ diff_type: var[str] = var(Initialize(get_diff_type))
198
+
199
+ def compose(self) -> ComposeResult:
200
+ with containers.Grid(classes="top"):
201
+ yield DiffViewSelect(
202
+ [
203
+ ("Unified diff", "unified"),
204
+ ("Split diff", "split"),
205
+ ("Auto diff", "auto"),
206
+ ],
207
+ value=self.diff_type,
208
+ allow_blank=False,
209
+ id="diff-select",
210
+ )
211
+ yield Static(
212
+ "[b]Approval request[/b] [dim]The Agent wishes to make the following changes",
213
+ id="instructions",
214
+ )
215
+ with containers.Vertical(id="nav-container"):
216
+ yield PermissionsQuestion("", options=self.options)
217
+ yield ChangesOptionList(id="navigator")
218
+ yield ToolScroll(id="tool-container")
219
+
220
+ yield Footer()
221
+
222
+ def action_select_kind(self, kind: str | tuple[str]) -> None:
223
+ self.question.action_select_kind(kind)
224
+
225
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
226
+ if action == "select_kind":
227
+ kinds = {
228
+ answer.kind
229
+ for answer in self.question.options
230
+ if answer.kind is not None
231
+ }
232
+ check_kinds = set()
233
+ for parameter in parameters:
234
+ if isinstance(parameter, str):
235
+ check_kinds.add(parameter)
236
+ elif isinstance(parameter, tuple):
237
+ check_kinds.update(parameter)
238
+
239
+ return any(kind in kinds for kind in check_kinds)
240
+
241
+ return True
242
+
243
+ async def on_mount(self):
244
+ app = self.app
245
+ if isinstance(app, ToadApp):
246
+ diff_view_setting = app.settings.get("diff.view", str)
247
+ self.query_one("#diff-select", Select).value = diff_view_setting
248
+ self.navigator.highlighted = 0
249
+
250
+ if self.populate_callback is not None:
251
+
252
+ async def run_populate():
253
+ if self.populate_callback is not None:
254
+ await self.populate_callback(self)
255
+
256
+ asyncio.create_task(run_populate())
257
+ self.question.focus()
258
+
259
+ async def add_diff(
260
+ self, path1: str, path2: str, before: str | None, after: str
261
+ ) -> None:
262
+ self.index += 1
263
+ option_id = f"item-{self.index}"
264
+ diff_view = DiffView(path1, path2, before or "", after, id=option_id)
265
+ await diff_view.prepare()
266
+ app = self.app
267
+ if isinstance(app, ToadApp):
268
+ diff_view_setting = app.settings.get("diff.view", str)
269
+ diff_view.split = diff_view_setting == "split"
270
+ diff_view.auto_split = diff_view_setting == "auto"
271
+ await self.tool_container.mount(diff_view)
272
+
273
+ option_text = f"📄 {os.path.basename(path1)}"
274
+ self.navigator.add_option(Option(option_text, option_id))
275
+
276
+ @on(OptionList.OptionHighlighted)
277
+ def on_option_highlighted(self, event: OptionList.OptionHighlighted):
278
+ self.tool_container.query_one(f"#{event.option_id}").scroll_visible(top=True)
279
+
280
+ @on(Question.Answer)
281
+ def on_question_answer(self, event: Question.Answer) -> None:
282
+ def dismiss():
283
+ self.dismiss(event.answer)
284
+
285
+ self.set_timer(0.4, dismiss)
286
+
287
+ @on(Select.Changed, "#diff-select")
288
+ def on_diff_select(self, event: Select.Changed) -> None:
289
+ diff_type = event.value
290
+ for diff_view in self.query(DiffView):
291
+ diff_view.auto_split = diff_type == "auto"
292
+ diff_view.split = diff_type == "split"
293
+
294
+ def action_next(self) -> None:
295
+ self.navigator.action_cursor_down()
296
+
297
+ def action_previous(self) -> None:
298
+ self.navigator.action_cursor_up()
299
+
300
+
301
+ if __name__ == "__main__":
302
+ SOURCE1 = '''\
303
+ def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
304
+ """Iterate and generate a tuple with a flag for first value."""
305
+ iter_values = iter(values)
306
+ try:
307
+ value = next(iter_values)
308
+ except StopIteration:
309
+ return
310
+ yield True, value
311
+ for value in iter_values:
312
+ yield False, value
313
+
314
+
315
+ def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
316
+ """Iterate and generate a tuple with a flag for first and last value."""
317
+ iter_values = iter(values)
318
+ try:
319
+ previous_value = next(iter_values)
320
+ except StopIteration:
321
+ return
322
+ first = True
323
+ for value in iter_values:
324
+ yield first, False, previous_value
325
+ first = False
326
+ previous_value = value
327
+ yield first, True, previous_value
328
+
329
+ '''
330
+
331
+ SOURCE2 = '''\
332
+ def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
333
+ """Iterate and generate a tuple with a flag for first value.
334
+
335
+ Args:
336
+ values: iterables of values.
337
+
338
+ Returns:
339
+ Iterable of a boolean to indicate first value, and a value from the iterable.
340
+ """
341
+ iter_values = iter(values)
342
+ try:
343
+ value = next(iter_values)
344
+ except StopIteration:
345
+ return
346
+ yield True, value
347
+ for value in iter_values:
348
+ yield False, value
349
+
350
+
351
+ def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
352
+ """Iterate and generate a tuple with a flag for last value."""
353
+ iter_values = iter(values)
354
+ try:
355
+ previous_value = next(iter_values)
356
+ except StopIteration:
357
+ return
358
+ for value in iter_values:
359
+ yield False, previous_value
360
+ previous_value = value
361
+ yield True, previous_value
362
+
363
+
364
+ def loop_first_last(values: Iterable[ValueType]) -> Iterable[tuple[bool, bool, ValueType]]:
365
+ """Iterate and generate a tuple with a flag for first and last value."""
366
+ iter_values = iter(values)
367
+ try:
368
+ previous_value = next(iter_values) # Get previous value
369
+ except StopIteration:
370
+ return
371
+ first = True
372
+
373
+ '''
374
+ from textual import work
375
+ from textual.app import App
376
+
377
+ class PermissionTestApp(App):
378
+ @work
379
+ async def on_mount(self) -> None:
380
+ screen = PermissionsScreen(
381
+ [Answer("Foo", "allow_once", kind="allow_once"), Answer("Bar", "bar")],
382
+ None,
383
+ )
384
+ result = await self.push_screen_wait(screen)
385
+ self.notify(str(result))
386
+ # for repeat in range(5):
387
+ # await screen.add_diff("foo.py", "foo.py", SOURCE1, SOURCE2)
388
+
389
+ app = PermissionTestApp()
390
+ app.run()
@@ -0,0 +1,72 @@
1
+ # Styles for the Permissions Screen
2
+
3
+
4
+ PermissionsScreen {
5
+
6
+
7
+ background: $background;
8
+
9
+ #instructions {
10
+ background: black 10%;
11
+ border: tall black 10%;
12
+ padding: 0 2;
13
+ # margin: 0 0 1 0;
14
+
15
+ }
16
+
17
+ .top {
18
+ height: 1fr;
19
+ background: $background;
20
+ margin: 1;
21
+ padding: 0 0;
22
+ grid-size: 2 2;
23
+ grid-columns: auto 1fr;
24
+ grid-rows: auto 1fr;
25
+ }
26
+
27
+ #changes {
28
+ width: 1fr;
29
+ height: 1fr;
30
+ }
31
+
32
+ OptionList#navigator {
33
+ width: auto;
34
+ height: 1fr;
35
+ min-width: 20;
36
+ width: 1fr;
37
+ expand: optimal;
38
+ }
39
+ Question {
40
+ width: auto;
41
+ expand: optimal;
42
+ border: tall black 10%;
43
+ background: black 10%;
44
+ # dock: bottom;
45
+ height: auto;
46
+ margin: 1 0;
47
+ padding: 0 2 0 1;
48
+ &:focus {
49
+ border: tall $primary;
50
+ }
51
+ }
52
+
53
+ #tool-container {
54
+ background: $background;
55
+ height: 1fr;
56
+ border: tall black 10%;
57
+ margin-top: 1;
58
+ &:focus {
59
+ border: tall $primary;
60
+ }
61
+ }
62
+
63
+ #nav-container {
64
+ width: auto;
65
+ height: 1fr;
66
+ Select {
67
+ width: auto;
68
+ height: auto;
69
+ expand: optimal;
70
+ }
71
+ }
72
+ }