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,418 @@
1
+ from functools import lru_cache
2
+ import io
3
+ import re2 as re
4
+
5
+ import rich.repr
6
+
7
+ from textual.cache import LRUCache
8
+ from typing import Callable, Generator, Iterable
9
+
10
+ type TokenMatch = tuple[str, str]
11
+
12
+ type ParseResult[ParseType] = Generator[StreamRead | ParseType, Token, None]
13
+ type PatternCheck = Generator[None, str, TokenMatch | bool | None]
14
+
15
+
16
+ @rich.repr.auto
17
+ class Pattern[ValueType]:
18
+ __slots__ = ["_send", "value"]
19
+
20
+ def __init__(self) -> None:
21
+ self._send: Callable[[str], None] | None = None
22
+ self.value: ValueType | None = None
23
+
24
+ def feed(self, character: str) -> bool | TokenMatch | None:
25
+ if self._send is None:
26
+ generator = self.check()
27
+ self._send = generator.send
28
+ next(generator)
29
+ try:
30
+ self._send(character)
31
+ except StopIteration as stop_iteration:
32
+ return stop_iteration.value
33
+ else:
34
+ return None
35
+
36
+ def check(self) -> PatternCheck:
37
+ return False
38
+ yield
39
+
40
+
41
+ class StreamRead[ResultType]:
42
+ pass
43
+
44
+
45
+ @rich.repr.auto
46
+ class Read[ResultType](StreamRead[ResultType]):
47
+ __slots__ = ["remaining"]
48
+
49
+ def __init__(self, count: int) -> None:
50
+ self.remaining = count
51
+
52
+
53
+ @rich.repr.auto
54
+ class ReadUntil[ResultType](StreamRead[ResultType]):
55
+ __slots__ = ["characters", "_regex"]
56
+
57
+ def __init__(self, *characters: str) -> None:
58
+ self.characters = characters
59
+ self._regex = re.compile(
60
+ "|".join(re.escape(character) for character in characters)
61
+ )
62
+
63
+ def __rich_repr__(self) -> rich.repr.Result:
64
+ yield from self.characters
65
+
66
+
67
+ @rich.repr.auto
68
+ class ReadRegex[ResultType](StreamRead[ResultType]):
69
+ __slots__ = ["regex", "max_length", "_buffer"]
70
+
71
+ def __init__(self, regex: str, max_length: int | None = None) -> None:
72
+ self.regex = regex
73
+ self.max_length = max_length
74
+ self._buffer = io.StringIO()
75
+
76
+ def __rich_repr__(self) -> rich.repr.Result:
77
+ yield self.regex
78
+
79
+ @property
80
+ def buffer_size(self) -> int:
81
+ return self._buffer.tell()
82
+
83
+
84
+ @rich.repr.auto
85
+ class ReadPatterns[ResultType](StreamRead[ResultType]):
86
+ __slots__ = ["patterns", "_text"]
87
+
88
+ def __init__(self, start: str = "", **patterns: Pattern) -> None:
89
+ self.patterns = patterns
90
+ self._text = io.StringIO()
91
+ self._text.write(start)
92
+
93
+ @property
94
+ def unconsumed_text(self) -> str:
95
+ return self._text.getvalue()
96
+
97
+ def __rich_repr__(self) -> rich.repr.Result:
98
+ for key, value in self.patterns.items():
99
+ yield key, value
100
+
101
+ @property
102
+ def is_exhausted(self) -> bool:
103
+ return not self.patterns
104
+
105
+ def feed(self, text: str) -> tuple[int, TokenMatch | None]:
106
+ consumed = 0
107
+ new_patterns = patterns = self.patterns
108
+ for character in text:
109
+ consumed += 1
110
+ for name, sequence_validator in patterns.items():
111
+ if (value := sequence_validator.feed(character)) is False:
112
+ new_patterns = patterns.copy()
113
+ new_patterns.pop(name)
114
+ elif value:
115
+ return consumed, (name, value)
116
+ patterns = self._patterns = new_patterns
117
+ self._text.write(text[:consumed])
118
+ return consumed, None
119
+
120
+
121
+ @rich.repr.auto
122
+ class ReadPattern[ResultType](StreamRead[ResultType]):
123
+ """Special case for a single pattern."""
124
+
125
+ __slots__ = ["name", "pattern", "_text", "_exhaused"]
126
+
127
+ def __init__(self, start: str, name: str, pattern: Pattern) -> None:
128
+ self.name = name
129
+ self.pattern: Pattern = pattern
130
+ self._text = io.StringIO()
131
+ self._text.write(start)
132
+ self._exhaused = False
133
+
134
+ @property
135
+ def unconsumed_text(self) -> str:
136
+ return self._text.getvalue()
137
+
138
+ def __rich_repr__(self) -> rich.repr.Result:
139
+ yield self.name
140
+ yield self.pattern
141
+
142
+ @property
143
+ def is_exhausted(self) -> bool:
144
+ return self._exhaused
145
+
146
+ def feed(self, text: str) -> tuple[int, TokenMatch | None]:
147
+ consumed = 0
148
+ feed = self.pattern.feed
149
+ for character in text:
150
+ consumed += 1
151
+ if (value := feed(character)) is False:
152
+ self._exhaused = True
153
+ break
154
+ elif value:
155
+ self._exhaused = True
156
+ return consumed, ("pattern", value)
157
+ self._text.write(text[:consumed])
158
+ return consumed, None
159
+
160
+
161
+ @rich.repr.auto
162
+ class Token:
163
+ """A token containing text."""
164
+
165
+ __slots__ = "text"
166
+
167
+ def __init__(self, text: str = "") -> None:
168
+ self.text = text
169
+
170
+ def __rich_repr__(self) -> rich.repr.Result:
171
+ yield self.text
172
+
173
+ def __str__(self) -> str:
174
+ return self.text
175
+
176
+
177
+ class SeparatorToken(Token):
178
+ pass
179
+
180
+
181
+ class MatchToken(Token):
182
+ __slots__ = ["match"]
183
+
184
+ def __init__(self, text: str, match: re.Match) -> None:
185
+ self.match = match
186
+ super().__init__(text)
187
+
188
+ def __rich_repr__(self) -> rich.repr.Result:
189
+ yield self.match
190
+
191
+
192
+ class EOFToken(Token):
193
+ pass
194
+
195
+
196
+ class PatternToken(Token):
197
+ __slots__ = ["name", "value"]
198
+
199
+ def __init__(self, name: str, value: TokenMatch) -> None:
200
+ self.name = name
201
+ self.value = value
202
+ super().__init__("")
203
+
204
+ def __rich_repr__(self) -> rich.repr.Result:
205
+ yield self.name
206
+ yield None, self.value
207
+
208
+
209
+ class StreamParser[ParseType]:
210
+ """Parses a stream of text into tokens."""
211
+
212
+ def __init__(self):
213
+ self._gen = self.parse()
214
+ self._reading: StreamRead | ParseType = next(self._gen)
215
+ self._cache = LRUCache(1024 * 4)
216
+
217
+ def read(self, count: int) -> Read:
218
+ """Read a specific number of bytes.
219
+
220
+ Args:
221
+ count: Number of bytes to read.
222
+ """
223
+ return Read(count)
224
+
225
+ @lru_cache(1024)
226
+ def read_until(self, *characters: str) -> ReadUntil:
227
+ """Read until the given characters.
228
+
229
+ Args:
230
+ characters: Set of characters to stop read.
231
+
232
+ """
233
+ return ReadUntil(*characters)
234
+
235
+ def read_regex(self, regex: str) -> ReadRegex:
236
+ """Search for the matching regex.
237
+
238
+ Args:
239
+ regex: Regular expression.
240
+ """
241
+ return ReadRegex(regex)
242
+
243
+ def read_patterns(self, start: str = "", **patterns) -> ReadPattern | ReadPatterns:
244
+ """Read until a pattern matches, or the patterns have been exhausted.
245
+
246
+ Args:
247
+ start: Initial part of the string.
248
+ **patterns: One or more patterns.
249
+ """
250
+ if len(patterns) == 1:
251
+ name, pattern = patterns.popitem()
252
+ return ReadPattern(start, name, pattern)
253
+ return ReadPatterns(start, **patterns)
254
+
255
+ def feed(self, text: str) -> Iterable[Token | ParseType]:
256
+ sequences = text.splitlines(keepends=True)
257
+ # TODO: Cache
258
+ for sequence in sequences:
259
+ yield from self._feed(sequence)
260
+
261
+ def _feed(self, text: str) -> Iterable[Token | ParseType]:
262
+ """Feed text in to parser.
263
+
264
+ Args:
265
+ text: Text from stream.
266
+
267
+ Returns:
268
+ A generator of tokens or the parse type.
269
+
270
+ """
271
+ if not text or self._gen is None:
272
+ yield EOFToken()
273
+ return
274
+
275
+ def send(token: Token) -> Iterable[Token]:
276
+ try:
277
+ while True:
278
+ new_token = self._gen.send(token)
279
+ if isinstance(new_token, StreamRead):
280
+ self._reading = new_token
281
+ break
282
+ else:
283
+ token = new_token
284
+ yield token
285
+
286
+ except StopIteration:
287
+ self._gen.close()
288
+ self._gen = None
289
+
290
+ while text:
291
+ if isinstance(self._reading, (ReadPattern, ReadPatterns)):
292
+ consumed, pattern_match = self._reading.feed(text)
293
+
294
+ if pattern_match is not None:
295
+ name, value = pattern_match
296
+ yield from send(PatternToken(name, value))
297
+ text = text[consumed:]
298
+ else:
299
+ if self._reading.is_exhausted:
300
+ unconsumed_text = self._reading.unconsumed_text
301
+ yield from send(Token(unconsumed_text))
302
+ text = text[consumed:]
303
+ else:
304
+ text = ""
305
+
306
+ elif isinstance(self._reading, Read):
307
+ if self._reading.remaining:
308
+ read_text = text[: self._reading.remaining]
309
+ read_text_length = len(read_text)
310
+ self._reading.remaining -= read_text_length
311
+ text = text[read_text_length:]
312
+ yield from send(Token(read_text))
313
+ else:
314
+ yield from send(Token(""))
315
+
316
+ elif isinstance(self._reading, ReadUntil):
317
+ if (match := self._reading._regex.search(text)) is not None:
318
+ start, end = match.span(0)
319
+ read_text = text[:start]
320
+
321
+ if read_text:
322
+ yield from send(Token(read_text))
323
+ text = text[start:]
324
+ else:
325
+ yield from send(SeparatorToken(text[start:end]))
326
+ text = text[end:]
327
+ else:
328
+ yield from send(Token(text))
329
+ text = ""
330
+
331
+ elif isinstance(self._reading, ReadRegex):
332
+ self._reading._buffer.write(text)
333
+ match_text = self._reading._buffer.getvalue()
334
+ if (
335
+ match := re.search(self._reading.regex, match_text, re.VERBOSE)
336
+ ) is not None:
337
+ token_text = match_text[: match.start(0)]
338
+ if token_text:
339
+ yield from send(Token(token_text))
340
+ end = match.end(0)
341
+ yield from send(MatchToken(match.group(0), match))
342
+ text = text[end:]
343
+ else:
344
+ yield from send(Token(match_text))
345
+ text = ""
346
+
347
+ def parse(self) -> ParseResult[ParseType]:
348
+ yield from ()
349
+
350
+
351
+ if __name__ == "__main__":
352
+ # from rich import print
353
+
354
+ import string
355
+
356
+ class KeyValue(Pattern):
357
+ def check(self) -> PatternCheck:
358
+ """Parses text in the form key:'value'
359
+
360
+ e.g
361
+
362
+ """
363
+ key: str = ""
364
+ value: str = ""
365
+ is_letter = string.ascii_lowercase.__contains__
366
+ if not is_letter(character := (yield)):
367
+ return False
368
+ key += character
369
+ while is_letter(character := (yield)):
370
+ key += character
371
+ if character != ":":
372
+ return False
373
+ if (yield) != "'":
374
+ return False
375
+ while is_letter(character := (yield)):
376
+ value += character
377
+ if character != "'":
378
+ return False
379
+ self.value = (key, value)
380
+ return True
381
+
382
+ class TestParser(StreamParser):
383
+ def parse(self) -> ParseResult:
384
+ token = yield self.read_patterns(key_value=KeyValue())
385
+ print("!", repr(token))
386
+ yield token
387
+ while token := (yield self.read(1)):
388
+ print(repr(token))
389
+ # while True:
390
+ # token = yield self.read_until(":")
391
+ # if not token:
392
+ # break
393
+ # yield token
394
+ # if isinstance(token, SeparatorToken):
395
+ # break
396
+ # key += token.text
397
+
398
+ # while token := (yield self.read_regex(r"\'.*?\'")):
399
+ # yield token
400
+ # if isinstance(token, MatchToken):
401
+ # break
402
+
403
+ # string = yield self.read_regex("'.*?'")
404
+ # print("VALUE=", string)
405
+
406
+ # yield (yield self.read(3))
407
+ # while (text := (yield self.read_until("'"))) != "'":
408
+ # yield text
409
+ # yield text
410
+ # while (text := (yield self.read_until("'"))) != "'":
411
+ # yield text
412
+ # yield text
413
+
414
+ parser = TestParser()
415
+
416
+ for chunk in ["foo", ":", "'bar", "asd", "asdasd", "';"]:
417
+ for token in parser.feed(chunk):
418
+ print(repr(token))
toad/answer.py ADDED
@@ -0,0 +1,22 @@
1
+ from typing import Literal, NamedTuple
2
+
3
+
4
+ """
5
+ allow_once - Allow this operation only this time
6
+ allow_always - Allow this operation and remember the choice
7
+ reject_once - Reject this operation only this time
8
+ reject_always - Reject this operation and remember the choice
9
+ """
10
+
11
+
12
+ class Answer(NamedTuple):
13
+ """An answer to a question posed by the agent."""
14
+
15
+ text: str
16
+ """The textual response."""
17
+ id: str
18
+ """The id of the response."""
19
+ kind: (
20
+ Literal["allow_once", "allow_always", "reject_once", "reject_always"] | None
21
+ ) = None
22
+ """Enumeration to potentially influence UI"""