aider-ce 0.87.2.dev9__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 aider-ce might be problematic. Click here for more details.

Files changed (264) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +34 -0
  4. aider/analytics.py +258 -0
  5. aider/args.py +1014 -0
  6. aider/args_formatter.py +228 -0
  7. aider/change_tracker.py +133 -0
  8. aider/coders/__init__.py +36 -0
  9. aider/coders/architect_coder.py +48 -0
  10. aider/coders/architect_prompts.py +40 -0
  11. aider/coders/ask_coder.py +9 -0
  12. aider/coders/ask_prompts.py +35 -0
  13. aider/coders/base_coder.py +3013 -0
  14. aider/coders/base_prompts.py +87 -0
  15. aider/coders/chat_chunks.py +64 -0
  16. aider/coders/context_coder.py +53 -0
  17. aider/coders/context_prompts.py +75 -0
  18. aider/coders/editblock_coder.py +657 -0
  19. aider/coders/editblock_fenced_coder.py +10 -0
  20. aider/coders/editblock_fenced_prompts.py +143 -0
  21. aider/coders/editblock_func_coder.py +141 -0
  22. aider/coders/editblock_func_prompts.py +27 -0
  23. aider/coders/editblock_prompts.py +177 -0
  24. aider/coders/editor_diff_fenced_coder.py +9 -0
  25. aider/coders/editor_diff_fenced_prompts.py +11 -0
  26. aider/coders/editor_editblock_coder.py +9 -0
  27. aider/coders/editor_editblock_prompts.py +21 -0
  28. aider/coders/editor_whole_coder.py +9 -0
  29. aider/coders/editor_whole_prompts.py +12 -0
  30. aider/coders/help_coder.py +16 -0
  31. aider/coders/help_prompts.py +46 -0
  32. aider/coders/navigator_coder.py +2711 -0
  33. aider/coders/navigator_legacy_prompts.py +338 -0
  34. aider/coders/navigator_prompts.py +530 -0
  35. aider/coders/patch_coder.py +706 -0
  36. aider/coders/patch_prompts.py +161 -0
  37. aider/coders/search_replace.py +757 -0
  38. aider/coders/shell.py +37 -0
  39. aider/coders/single_wholefile_func_coder.py +102 -0
  40. aider/coders/single_wholefile_func_prompts.py +27 -0
  41. aider/coders/udiff_coder.py +429 -0
  42. aider/coders/udiff_prompts.py +117 -0
  43. aider/coders/udiff_simple.py +14 -0
  44. aider/coders/udiff_simple_prompts.py +25 -0
  45. aider/coders/wholefile_coder.py +144 -0
  46. aider/coders/wholefile_func_coder.py +134 -0
  47. aider/coders/wholefile_func_prompts.py +27 -0
  48. aider/coders/wholefile_prompts.py +70 -0
  49. aider/commands.py +1946 -0
  50. aider/copypaste.py +72 -0
  51. aider/deprecated.py +126 -0
  52. aider/diffs.py +128 -0
  53. aider/dump.py +29 -0
  54. aider/editor.py +147 -0
  55. aider/exceptions.py +107 -0
  56. aider/format_settings.py +26 -0
  57. aider/gui.py +545 -0
  58. aider/help.py +163 -0
  59. aider/help_pats.py +19 -0
  60. aider/history.py +178 -0
  61. aider/io.py +1257 -0
  62. aider/linter.py +304 -0
  63. aider/llm.py +47 -0
  64. aider/main.py +1297 -0
  65. aider/mcp/__init__.py +94 -0
  66. aider/mcp/server.py +119 -0
  67. aider/mdstream.py +243 -0
  68. aider/models.py +1344 -0
  69. aider/onboarding.py +428 -0
  70. aider/openrouter.py +129 -0
  71. aider/prompts.py +56 -0
  72. aider/queries/tree-sitter-language-pack/README.md +7 -0
  73. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  74. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  75. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  76. aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  77. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  91. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  92. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  93. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  94. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  95. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  96. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  97. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  98. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  99. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  100. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  101. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  102. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  103. aider/queries/tree-sitter-languages/README.md +23 -0
  104. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  105. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  106. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  107. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  108. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  109. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  110. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  111. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  112. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  113. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  114. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  115. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  116. aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  117. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  118. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  119. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  120. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  121. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  122. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  123. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  124. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  125. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  126. aider/reasoning_tags.py +82 -0
  127. aider/repo.py +621 -0
  128. aider/repomap.py +988 -0
  129. aider/report.py +200 -0
  130. aider/resources/__init__.py +3 -0
  131. aider/resources/model-metadata.json +699 -0
  132. aider/resources/model-settings.yml +2046 -0
  133. aider/run_cmd.py +132 -0
  134. aider/scrape.py +284 -0
  135. aider/sendchat.py +61 -0
  136. aider/special.py +203 -0
  137. aider/tools/__init__.py +26 -0
  138. aider/tools/command.py +58 -0
  139. aider/tools/command_interactive.py +53 -0
  140. aider/tools/delete_block.py +120 -0
  141. aider/tools/delete_line.py +112 -0
  142. aider/tools/delete_lines.py +137 -0
  143. aider/tools/extract_lines.py +276 -0
  144. aider/tools/grep.py +171 -0
  145. aider/tools/indent_lines.py +155 -0
  146. aider/tools/insert_block.py +211 -0
  147. aider/tools/list_changes.py +51 -0
  148. aider/tools/ls.py +49 -0
  149. aider/tools/make_editable.py +46 -0
  150. aider/tools/make_readonly.py +29 -0
  151. aider/tools/remove.py +48 -0
  152. aider/tools/replace_all.py +77 -0
  153. aider/tools/replace_line.py +125 -0
  154. aider/tools/replace_lines.py +160 -0
  155. aider/tools/replace_text.py +125 -0
  156. aider/tools/show_numbered_context.py +101 -0
  157. aider/tools/tool_utils.py +313 -0
  158. aider/tools/undo_change.py +60 -0
  159. aider/tools/view.py +13 -0
  160. aider/tools/view_files_at_glob.py +65 -0
  161. aider/tools/view_files_matching.py +103 -0
  162. aider/tools/view_files_with_symbol.py +121 -0
  163. aider/urls.py +17 -0
  164. aider/utils.py +454 -0
  165. aider/versioncheck.py +113 -0
  166. aider/voice.py +187 -0
  167. aider/waiting.py +221 -0
  168. aider/watch.py +318 -0
  169. aider/watch_prompts.py +12 -0
  170. aider/website/Gemfile +8 -0
  171. aider/website/_includes/blame.md +162 -0
  172. aider/website/_includes/get-started.md +22 -0
  173. aider/website/_includes/help-tip.md +5 -0
  174. aider/website/_includes/help.md +24 -0
  175. aider/website/_includes/install.md +5 -0
  176. aider/website/_includes/keys.md +4 -0
  177. aider/website/_includes/model-warnings.md +67 -0
  178. aider/website/_includes/multi-line.md +22 -0
  179. aider/website/_includes/python-m-aider.md +5 -0
  180. aider/website/_includes/recording.css +228 -0
  181. aider/website/_includes/recording.md +34 -0
  182. aider/website/_includes/replit-pipx.md +9 -0
  183. aider/website/_includes/works-best.md +1 -0
  184. aider/website/_sass/custom/custom.scss +103 -0
  185. aider/website/docs/config/adv-model-settings.md +2260 -0
  186. aider/website/docs/config/aider_conf.md +548 -0
  187. aider/website/docs/config/api-keys.md +90 -0
  188. aider/website/docs/config/dotenv.md +493 -0
  189. aider/website/docs/config/editor.md +127 -0
  190. aider/website/docs/config/mcp.md +95 -0
  191. aider/website/docs/config/model-aliases.md +104 -0
  192. aider/website/docs/config/options.md +890 -0
  193. aider/website/docs/config/reasoning.md +210 -0
  194. aider/website/docs/config.md +44 -0
  195. aider/website/docs/faq.md +384 -0
  196. aider/website/docs/git.md +76 -0
  197. aider/website/docs/index.md +47 -0
  198. aider/website/docs/install/codespaces.md +39 -0
  199. aider/website/docs/install/docker.md +57 -0
  200. aider/website/docs/install/optional.md +100 -0
  201. aider/website/docs/install/replit.md +8 -0
  202. aider/website/docs/install.md +115 -0
  203. aider/website/docs/languages.md +264 -0
  204. aider/website/docs/legal/contributor-agreement.md +111 -0
  205. aider/website/docs/legal/privacy.md +104 -0
  206. aider/website/docs/llms/anthropic.md +77 -0
  207. aider/website/docs/llms/azure.md +48 -0
  208. aider/website/docs/llms/bedrock.md +132 -0
  209. aider/website/docs/llms/cohere.md +34 -0
  210. aider/website/docs/llms/deepseek.md +32 -0
  211. aider/website/docs/llms/gemini.md +49 -0
  212. aider/website/docs/llms/github.md +111 -0
  213. aider/website/docs/llms/groq.md +36 -0
  214. aider/website/docs/llms/lm-studio.md +39 -0
  215. aider/website/docs/llms/ollama.md +75 -0
  216. aider/website/docs/llms/openai-compat.md +39 -0
  217. aider/website/docs/llms/openai.md +58 -0
  218. aider/website/docs/llms/openrouter.md +78 -0
  219. aider/website/docs/llms/other.md +111 -0
  220. aider/website/docs/llms/vertex.md +50 -0
  221. aider/website/docs/llms/warnings.md +10 -0
  222. aider/website/docs/llms/xai.md +53 -0
  223. aider/website/docs/llms.md +54 -0
  224. aider/website/docs/more/analytics.md +127 -0
  225. aider/website/docs/more/edit-formats.md +116 -0
  226. aider/website/docs/more/infinite-output.md +159 -0
  227. aider/website/docs/more-info.md +8 -0
  228. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  229. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  230. aider/website/docs/recordings/index.md +21 -0
  231. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  232. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  233. aider/website/docs/repomap.md +112 -0
  234. aider/website/docs/scripting.md +100 -0
  235. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  236. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  237. aider/website/docs/troubleshooting/imports.md +62 -0
  238. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  239. aider/website/docs/troubleshooting/support.md +79 -0
  240. aider/website/docs/troubleshooting/token-limits.md +96 -0
  241. aider/website/docs/troubleshooting/warnings.md +12 -0
  242. aider/website/docs/troubleshooting.md +11 -0
  243. aider/website/docs/usage/browser.md +57 -0
  244. aider/website/docs/usage/caching.md +49 -0
  245. aider/website/docs/usage/commands.md +133 -0
  246. aider/website/docs/usage/conventions.md +119 -0
  247. aider/website/docs/usage/copypaste.md +121 -0
  248. aider/website/docs/usage/images-urls.md +48 -0
  249. aider/website/docs/usage/lint-test.md +118 -0
  250. aider/website/docs/usage/modes.md +211 -0
  251. aider/website/docs/usage/not-code.md +179 -0
  252. aider/website/docs/usage/notifications.md +87 -0
  253. aider/website/docs/usage/tips.md +79 -0
  254. aider/website/docs/usage/tutorials.md +30 -0
  255. aider/website/docs/usage/voice.md +121 -0
  256. aider/website/docs/usage/watch.md +294 -0
  257. aider/website/docs/usage.md +102 -0
  258. aider/website/share/index.md +101 -0
  259. aider_ce-0.87.2.dev9.dist-info/METADATA +543 -0
  260. aider_ce-0.87.2.dev9.dist-info/RECORD +264 -0
  261. aider_ce-0.87.2.dev9.dist-info/WHEEL +5 -0
  262. aider_ce-0.87.2.dev9.dist-info/entry_points.txt +3 -0
  263. aider_ce-0.87.2.dev9.dist-info/licenses/LICENSE.txt +202 -0
  264. aider_ce-0.87.2.dev9.dist-info/top_level.txt +1 -0
aider/voice.py ADDED
@@ -0,0 +1,187 @@
1
+ import math
2
+ import os
3
+ import queue
4
+ import tempfile
5
+ import time
6
+ import warnings
7
+
8
+ from prompt_toolkit.shortcuts import prompt
9
+
10
+ from aider.llm import litellm
11
+
12
+ from .dump import dump # noqa: F401
13
+
14
+ warnings.filterwarnings(
15
+ "ignore", message="Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work"
16
+ )
17
+ warnings.filterwarnings("ignore", category=SyntaxWarning)
18
+
19
+
20
+ from pydub import AudioSegment # noqa
21
+ from pydub.exceptions import CouldntDecodeError, CouldntEncodeError # noqa
22
+
23
+ try:
24
+ import soundfile as sf
25
+ except (OSError, ModuleNotFoundError):
26
+ sf = None
27
+
28
+
29
+ class SoundDeviceError(Exception):
30
+ pass
31
+
32
+
33
+ class Voice:
34
+ max_rms = 0
35
+ min_rms = 1e5
36
+ pct = 0
37
+
38
+ threshold = 0.15
39
+
40
+ def __init__(self, audio_format="wav", device_name=None):
41
+ if sf is None:
42
+ raise SoundDeviceError
43
+ try:
44
+ print("Initializing sound device...")
45
+ import sounddevice as sd
46
+
47
+ self.sd = sd
48
+
49
+ devices = sd.query_devices()
50
+
51
+ if device_name:
52
+ # Find the device with matching name
53
+ device_id = None
54
+ for i, device in enumerate(devices):
55
+ if device_name in device["name"]:
56
+ device_id = i
57
+ break
58
+ if device_id is None:
59
+ available_inputs = [d["name"] for d in devices if d["max_input_channels"] > 0]
60
+ raise ValueError(
61
+ f"Device '{device_name}' not found. Available input devices:"
62
+ f" {available_inputs}"
63
+ )
64
+
65
+ print(f"Using input device: {device_name} (ID: {device_id})")
66
+
67
+ self.device_id = device_id
68
+ else:
69
+ self.device_id = None
70
+
71
+ except (OSError, ModuleNotFoundError):
72
+ raise SoundDeviceError
73
+ if audio_format not in ["wav", "mp3", "webm"]:
74
+ raise ValueError(f"Unsupported audio format: {audio_format}")
75
+ self.audio_format = audio_format
76
+
77
+ def callback(self, indata, frames, time, status):
78
+ """This is called (from a separate thread) for each audio block."""
79
+ import numpy as np
80
+
81
+ rms = np.sqrt(np.mean(indata**2))
82
+ self.max_rms = max(self.max_rms, rms)
83
+ self.min_rms = min(self.min_rms, rms)
84
+
85
+ rng = self.max_rms - self.min_rms
86
+ if rng > 0.001:
87
+ self.pct = (rms - self.min_rms) / rng
88
+ else:
89
+ self.pct = 0.5
90
+
91
+ self.q.put(indata.copy())
92
+
93
+ def get_prompt(self):
94
+ num = 10
95
+ if math.isnan(self.pct) or self.pct < self.threshold:
96
+ cnt = 0
97
+ else:
98
+ cnt = int(self.pct * 10)
99
+
100
+ bar = "░" * cnt + "█" * (num - cnt)
101
+ bar = bar[:num]
102
+
103
+ dur = time.time() - self.start_time
104
+ return f"Recording, press ENTER when done... {dur:.1f}sec {bar}"
105
+
106
+ def record_and_transcribe(self, history=None, language=None):
107
+ try:
108
+ return self.raw_record_and_transcribe(history, language)
109
+ except KeyboardInterrupt:
110
+ return
111
+ except SoundDeviceError as e:
112
+ print(f"Error: {e}")
113
+ print("Please ensure you have a working audio input device connected and try again.")
114
+ return
115
+
116
+ def raw_record_and_transcribe(self, history, language):
117
+ self.q = queue.Queue()
118
+
119
+ temp_wav = tempfile.mktemp(suffix=".wav")
120
+
121
+ try:
122
+ sample_rate = int(self.sd.query_devices(self.device_id, "input")["default_samplerate"])
123
+ except (TypeError, ValueError):
124
+ sample_rate = 16000 # fallback to 16kHz if unable to query device
125
+ except self.sd.PortAudioError:
126
+ raise SoundDeviceError(
127
+ "No audio input device detected. Please check your audio settings and try again."
128
+ )
129
+
130
+ self.start_time = time.time()
131
+
132
+ try:
133
+ with self.sd.InputStream(
134
+ samplerate=sample_rate, channels=1, callback=self.callback, device=self.device_id
135
+ ):
136
+ prompt(self.get_prompt, refresh_interval=0.1)
137
+ except self.sd.PortAudioError as err:
138
+ raise SoundDeviceError(f"Error accessing audio input device: {err}")
139
+
140
+ with sf.SoundFile(temp_wav, mode="x", samplerate=sample_rate, channels=1) as file:
141
+ while not self.q.empty():
142
+ file.write(self.q.get())
143
+
144
+ use_audio_format = self.audio_format
145
+
146
+ # Check file size and offer to convert to mp3 if too large
147
+ file_size = os.path.getsize(temp_wav)
148
+ if file_size > 24.9 * 1024 * 1024 and self.audio_format == "wav":
149
+ print("\nWarning: {temp_wav} is too large, switching to mp3 format.")
150
+ use_audio_format = "mp3"
151
+
152
+ filename = temp_wav
153
+ if use_audio_format != "wav":
154
+ try:
155
+ new_filename = tempfile.mktemp(suffix=f".{use_audio_format}")
156
+ audio = AudioSegment.from_wav(temp_wav)
157
+ audio.export(new_filename, format=use_audio_format)
158
+ os.remove(temp_wav)
159
+ filename = new_filename
160
+ except (CouldntDecodeError, CouldntEncodeError) as e:
161
+ print(f"Error converting audio: {e}")
162
+ except (OSError, FileNotFoundError) as e:
163
+ print(f"File system error during conversion: {e}")
164
+ except Exception as e:
165
+ print(f"Unexpected error during audio conversion: {e}")
166
+
167
+ with open(filename, "rb") as fh:
168
+ try:
169
+ transcript = litellm.transcription(
170
+ model="whisper-1", file=fh, prompt=history, language=language
171
+ )
172
+ except Exception as err:
173
+ print(f"Unable to transcribe {filename}: {err}")
174
+ return
175
+
176
+ if filename != temp_wav:
177
+ os.remove(filename)
178
+
179
+ text = transcript.text
180
+ return text
181
+
182
+
183
+ if __name__ == "__main__":
184
+ api_key = os.getenv("OPENAI_API_KEY")
185
+ if not api_key:
186
+ raise ValueError("Please set the OPENAI_API_KEY environment variable.")
187
+ print(Voice().record_and_transcribe())
aider/waiting.py ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Thread-based, killable spinner utility.
5
+
6
+ Use it like:
7
+
8
+ from aider.waiting import WaitingSpinner
9
+
10
+ spinner = WaitingSpinner("Waiting for LLM")
11
+ spinner.start()
12
+ ... # long task
13
+ spinner.stop()
14
+ """
15
+
16
+ import sys
17
+ import threading
18
+ import time
19
+
20
+ from rich.console import Console
21
+
22
+
23
+ class Spinner:
24
+ """
25
+ Minimal spinner that scans a single marker back and forth across a line.
26
+
27
+ The animation is pre-rendered into a list of frames. If the terminal
28
+ cannot display unicode the frames are converted to plain ASCII.
29
+ """
30
+
31
+ last_frame_idx = 0 # Class variable to store the last frame index
32
+
33
+ def __init__(self, text: str, width: int = 7):
34
+ self.text = text
35
+ self.start_time = time.time()
36
+ self.last_update = 0.0
37
+ self.visible = False
38
+ self.is_tty = sys.stdout.isatty()
39
+ self.console = Console()
40
+
41
+ # Pre-render the animation frames using pure ASCII so they will
42
+ # always display, even on very limited terminals.
43
+ ascii_frames = [
44
+ "#= ", # C1 C2 space(8)
45
+ "=# ", # C2 C1 space(8)
46
+ " =# ", # space(1) C2 C1 space(7)
47
+ " =# ", # space(2) C2 C1 space(6)
48
+ " =# ", # space(3) C2 C1 space(5)
49
+ " =# ", # space(4) C2 C1 space(4)
50
+ " =# ", # space(5) C2 C1 space(3)
51
+ " =# ", # space(6) C2 C1 space(2)
52
+ " =# ", # space(7) C2 C1 space(1)
53
+ " =#", # space(8) C2 C1
54
+ " #=", # space(8) C1 C2
55
+ " #= ", # space(7) C1 C2 space(1)
56
+ " #= ", # space(6) C1 C2 space(2)
57
+ " #= ", # space(5) C1 C2 space(3)
58
+ " #= ", # space(4) C1 C2 space(4)
59
+ " #= ", # space(3) C1 C2 space(5)
60
+ " #= ", # space(2) C1 C2 space(6)
61
+ " #= ", # space(1) C1 C2 space(7)
62
+ ]
63
+
64
+ self.unicode_palette = "░█"
65
+ xlate_from, xlate_to = ("=#", self.unicode_palette)
66
+
67
+ # If unicode is supported, swap the ASCII chars for nicer glyphs.
68
+ if self._supports_unicode():
69
+ translation_table = str.maketrans(xlate_from, xlate_to)
70
+ frames = [f.translate(translation_table) for f in ascii_frames]
71
+ self.scan_char = xlate_to[xlate_from.find("#")]
72
+ else:
73
+ frames = ascii_frames
74
+ self.scan_char = "#"
75
+
76
+ # Bounce the scanner back and forth.
77
+ self.frames = frames
78
+ self.frame_idx = Spinner.last_frame_idx # Initialize from class variable
79
+ self.width = len(frames[0]) - 2 # number of chars between the brackets
80
+ self.animation_len = len(frames[0])
81
+ self.last_display_len = 0 # Length of the last spinner line (frame + text)
82
+
83
+ def _supports_unicode(self) -> bool:
84
+ if not self.is_tty:
85
+ return False
86
+ try:
87
+ out = self.unicode_palette
88
+ out += "\b" * len(self.unicode_palette)
89
+ out += " " * len(self.unicode_palette)
90
+ out += "\b" * len(self.unicode_palette)
91
+ sys.stdout.write(out)
92
+ sys.stdout.flush()
93
+ return True
94
+ except UnicodeEncodeError:
95
+ return False
96
+ except Exception:
97
+ return False
98
+
99
+ def _next_frame(self) -> str:
100
+ frame = self.frames[self.frame_idx]
101
+ self.frame_idx = (self.frame_idx + 1) % len(self.frames)
102
+ Spinner.last_frame_idx = self.frame_idx # Update class variable
103
+ return frame
104
+
105
+ def step(self, text: str = None) -> None:
106
+ if text is not None:
107
+ self.text = text
108
+
109
+ if not self.is_tty:
110
+ return
111
+
112
+ now = time.time()
113
+ if not self.visible and now - self.start_time >= 0.5:
114
+ self.visible = True
115
+ self.last_update = 0.0
116
+ if self.is_tty:
117
+ self.console.show_cursor(False)
118
+
119
+ if not self.visible or now - self.last_update < 0.1:
120
+ return
121
+
122
+ self.last_update = now
123
+ frame_str = self._next_frame()
124
+
125
+ # Determine the maximum width for the spinner line
126
+ # Subtract 2 as requested, to leave a margin or prevent cursor wrapping issues
127
+ max_spinner_width = self.console.width - 2
128
+ if max_spinner_width < 0: # Handle extremely narrow terminals
129
+ max_spinner_width = 0
130
+
131
+ current_text_payload = f" {self.text}"
132
+ line_to_display = f"{frame_str}{current_text_payload}"
133
+
134
+ # Truncate the line if it's too long for the console width
135
+ if len(line_to_display) > max_spinner_width:
136
+ line_to_display = line_to_display[:max_spinner_width]
137
+
138
+ len_line_to_display = len(line_to_display)
139
+
140
+ # Calculate padding to clear any remnants from a longer previous line
141
+ padding_to_clear = " " * max(0, self.last_display_len - len_line_to_display)
142
+
143
+ # Write the spinner frame, text, and any necessary clearing spaces
144
+ sys.stdout.write(f"\r{line_to_display}{padding_to_clear}")
145
+ self.last_display_len = len_line_to_display
146
+
147
+ # Calculate number of backspaces to position cursor at the scanner character
148
+ scan_char_abs_pos = frame_str.find(self.scan_char)
149
+
150
+ # Total characters written to the line (frame + text + padding)
151
+ total_chars_written_on_line = len_line_to_display + len(padding_to_clear)
152
+
153
+ # num_backspaces will be non-positive if scan_char_abs_pos is beyond
154
+ # total_chars_written_on_line (e.g., if the scan char itself was truncated).
155
+ # (e.g., if the scan char itself was truncated).
156
+ # In such cases, (effectively) 0 backspaces are written,
157
+ # and the cursor stays at the end of the line.
158
+ num_backspaces = total_chars_written_on_line - scan_char_abs_pos
159
+ sys.stdout.write("\b" * num_backspaces)
160
+ sys.stdout.flush()
161
+
162
+ def end(self) -> None:
163
+ if self.visible and self.is_tty:
164
+ clear_len = self.last_display_len # Use the length of the last displayed content
165
+ sys.stdout.write("\r" + " " * clear_len + "\r")
166
+ sys.stdout.flush()
167
+ self.console.show_cursor(True)
168
+ self.visible = False
169
+
170
+
171
+ class WaitingSpinner:
172
+ """Background spinner that can be started/stopped safely."""
173
+
174
+ def __init__(self, text: str = "Waiting for LLM", delay: float = 0.15):
175
+ self.spinner = Spinner(text)
176
+ self.delay = delay
177
+ self._stop_event = threading.Event()
178
+ self._thread = threading.Thread(target=self._spin, daemon=True)
179
+
180
+ def _spin(self):
181
+ while not self._stop_event.is_set():
182
+ self.spinner.step()
183
+ time.sleep(self.delay)
184
+ self.spinner.end()
185
+
186
+ def start(self):
187
+ """Start the spinner in a background thread."""
188
+ if not self._thread.is_alive():
189
+ self._thread.start()
190
+
191
+ def stop(self):
192
+ """Request the spinner to stop and wait briefly for the thread to exit."""
193
+ self._stop_event.set()
194
+ if self._thread.is_alive():
195
+ self._thread.join(timeout=self.delay)
196
+ self.spinner.end()
197
+
198
+ # Allow use as a context-manager
199
+ def __enter__(self):
200
+ self.start()
201
+ return self
202
+
203
+ def __exit__(self, exc_type, exc_val, exc_tb):
204
+ self.stop()
205
+
206
+
207
+ def main():
208
+ spinner = Spinner("Running spinner...")
209
+ try:
210
+ for _ in range(100):
211
+ time.sleep(0.15)
212
+ spinner.step()
213
+ print("Success!")
214
+ except KeyboardInterrupt:
215
+ print("\nInterrupted by user.")
216
+ finally:
217
+ spinner.end()
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()