cecli-dev 0.95.5__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 (366) hide show
  1. cecli/__init__.py +20 -0
  2. cecli/__main__.py +4 -0
  3. cecli/_version.py +34 -0
  4. cecli/args.py +1092 -0
  5. cecli/args_formatter.py +228 -0
  6. cecli/change_tracker.py +133 -0
  7. cecli/coders/__init__.py +38 -0
  8. cecli/coders/agent_coder.py +1872 -0
  9. cecli/coders/architect_coder.py +63 -0
  10. cecli/coders/ask_coder.py +8 -0
  11. cecli/coders/base_coder.py +3993 -0
  12. cecli/coders/chat_chunks.py +116 -0
  13. cecli/coders/context_coder.py +52 -0
  14. cecli/coders/copypaste_coder.py +269 -0
  15. cecli/coders/editblock_coder.py +656 -0
  16. cecli/coders/editblock_fenced_coder.py +9 -0
  17. cecli/coders/editblock_func_coder.py +140 -0
  18. cecli/coders/editor_diff_fenced_coder.py +8 -0
  19. cecli/coders/editor_editblock_coder.py +8 -0
  20. cecli/coders/editor_whole_coder.py +8 -0
  21. cecli/coders/help_coder.py +15 -0
  22. cecli/coders/patch_coder.py +705 -0
  23. cecli/coders/search_replace.py +757 -0
  24. cecli/coders/shell.py +37 -0
  25. cecli/coders/single_wholefile_func_coder.py +101 -0
  26. cecli/coders/udiff_coder.py +428 -0
  27. cecli/coders/udiff_simple.py +12 -0
  28. cecli/coders/wholefile_coder.py +143 -0
  29. cecli/coders/wholefile_func_coder.py +133 -0
  30. cecli/commands/__init__.py +192 -0
  31. cecli/commands/add.py +226 -0
  32. cecli/commands/agent.py +51 -0
  33. cecli/commands/architect.py +46 -0
  34. cecli/commands/ask.py +44 -0
  35. cecli/commands/chat_mode.py +0 -0
  36. cecli/commands/clear.py +37 -0
  37. cecli/commands/code.py +46 -0
  38. cecli/commands/command_prefix.py +44 -0
  39. cecli/commands/commit.py +52 -0
  40. cecli/commands/context.py +47 -0
  41. cecli/commands/context_blocks.py +124 -0
  42. cecli/commands/context_management.py +51 -0
  43. cecli/commands/copy.py +62 -0
  44. cecli/commands/copy_context.py +81 -0
  45. cecli/commands/core.py +287 -0
  46. cecli/commands/diff.py +68 -0
  47. cecli/commands/drop.py +217 -0
  48. cecli/commands/editor.py +78 -0
  49. cecli/commands/exit.py +55 -0
  50. cecli/commands/git.py +57 -0
  51. cecli/commands/help.py +140 -0
  52. cecli/commands/history_search.py +40 -0
  53. cecli/commands/lint.py +109 -0
  54. cecli/commands/list_sessions.py +56 -0
  55. cecli/commands/load.py +85 -0
  56. cecli/commands/load_session.py +48 -0
  57. cecli/commands/load_skill.py +68 -0
  58. cecli/commands/ls.py +75 -0
  59. cecli/commands/map.py +37 -0
  60. cecli/commands/map_refresh.py +35 -0
  61. cecli/commands/model.py +118 -0
  62. cecli/commands/models.py +41 -0
  63. cecli/commands/multiline_mode.py +38 -0
  64. cecli/commands/paste.py +91 -0
  65. cecli/commands/quit.py +32 -0
  66. cecli/commands/read_only.py +267 -0
  67. cecli/commands/read_only_stub.py +270 -0
  68. cecli/commands/reasoning_effort.py +70 -0
  69. cecli/commands/remove_skill.py +68 -0
  70. cecli/commands/report.py +40 -0
  71. cecli/commands/reset.py +88 -0
  72. cecli/commands/run.py +99 -0
  73. cecli/commands/save.py +49 -0
  74. cecli/commands/save_session.py +43 -0
  75. cecli/commands/settings.py +69 -0
  76. cecli/commands/test.py +58 -0
  77. cecli/commands/think_tokens.py +74 -0
  78. cecli/commands/tokens.py +207 -0
  79. cecli/commands/undo.py +145 -0
  80. cecli/commands/utils/__init__.py +0 -0
  81. cecli/commands/utils/base_command.py +131 -0
  82. cecli/commands/utils/helpers.py +142 -0
  83. cecli/commands/utils/registry.py +53 -0
  84. cecli/commands/utils/save_load_manager.py +98 -0
  85. cecli/commands/voice.py +78 -0
  86. cecli/commands/weak_model.py +123 -0
  87. cecli/commands/web.py +87 -0
  88. cecli/deprecated_args.py +185 -0
  89. cecli/diffs.py +129 -0
  90. cecli/dump.py +29 -0
  91. cecli/editor.py +147 -0
  92. cecli/exceptions.py +115 -0
  93. cecli/format_settings.py +26 -0
  94. cecli/help.py +119 -0
  95. cecli/help_pats.py +19 -0
  96. cecli/helpers/__init__.py +9 -0
  97. cecli/helpers/copypaste.py +123 -0
  98. cecli/helpers/coroutines.py +8 -0
  99. cecli/helpers/file_searcher.py +142 -0
  100. cecli/helpers/model_providers.py +552 -0
  101. cecli/helpers/plugin_manager.py +81 -0
  102. cecli/helpers/profiler.py +162 -0
  103. cecli/helpers/requests.py +77 -0
  104. cecli/helpers/similarity.py +98 -0
  105. cecli/helpers/skills.py +577 -0
  106. cecli/history.py +186 -0
  107. cecli/io.py +1782 -0
  108. cecli/linter.py +304 -0
  109. cecli/llm.py +101 -0
  110. cecli/main.py +1280 -0
  111. cecli/mcp/__init__.py +154 -0
  112. cecli/mcp/oauth.py +250 -0
  113. cecli/mcp/server.py +278 -0
  114. cecli/mdstream.py +243 -0
  115. cecli/models.py +1255 -0
  116. cecli/onboarding.py +301 -0
  117. cecli/prompts/__init__.py +0 -0
  118. cecli/prompts/agent.yml +71 -0
  119. cecli/prompts/architect.yml +35 -0
  120. cecli/prompts/ask.yml +31 -0
  121. cecli/prompts/base.yml +99 -0
  122. cecli/prompts/context.yml +60 -0
  123. cecli/prompts/copypaste.yml +5 -0
  124. cecli/prompts/editblock.yml +143 -0
  125. cecli/prompts/editblock_fenced.yml +106 -0
  126. cecli/prompts/editblock_func.yml +25 -0
  127. cecli/prompts/editor_diff_fenced.yml +115 -0
  128. cecli/prompts/editor_editblock.yml +121 -0
  129. cecli/prompts/editor_whole.yml +46 -0
  130. cecli/prompts/help.yml +37 -0
  131. cecli/prompts/patch.yml +110 -0
  132. cecli/prompts/single_wholefile_func.yml +24 -0
  133. cecli/prompts/udiff.yml +106 -0
  134. cecli/prompts/udiff_simple.yml +13 -0
  135. cecli/prompts/utils/__init__.py +0 -0
  136. cecli/prompts/utils/prompt_registry.py +167 -0
  137. cecli/prompts/utils/system.py +56 -0
  138. cecli/prompts/wholefile.yml +50 -0
  139. cecli/prompts/wholefile_func.yml +24 -0
  140. cecli/queries/tree-sitter-language-pack/README.md +7 -0
  141. cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  142. cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
  143. cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  144. cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
  145. cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
  146. cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
  147. cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
  148. cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  149. cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
  150. cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  151. cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
  152. cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
  153. cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  154. cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
  155. cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
  156. cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
  157. cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
  158. cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  159. cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  160. cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
  161. cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  162. cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  163. cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
  164. cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
  165. cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  166. cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
  167. cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
  168. cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  169. cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
  170. cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  171. cecli/queries/tree-sitter-languages/README.md +24 -0
  172. cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
  173. cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
  174. cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
  175. cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
  176. cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  177. cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
  178. cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
  179. cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
  180. cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
  181. cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
  182. cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  183. cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
  184. cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
  185. cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
  186. cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
  187. cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  188. cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  189. cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
  190. cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
  191. cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
  192. cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
  193. cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
  194. cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
  195. cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
  196. cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
  197. cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
  198. cecli/reasoning_tags.py +82 -0
  199. cecli/repo.py +626 -0
  200. cecli/repomap.py +1368 -0
  201. cecli/report.py +260 -0
  202. cecli/resources/__init__.py +3 -0
  203. cecli/resources/model-metadata.json +25751 -0
  204. cecli/resources/model-settings.yml +2394 -0
  205. cecli/resources/providers.json +67 -0
  206. cecli/run_cmd.py +143 -0
  207. cecli/scrape.py +295 -0
  208. cecli/sendchat.py +250 -0
  209. cecli/sessions.py +281 -0
  210. cecli/special.py +203 -0
  211. cecli/tools/__init__.py +72 -0
  212. cecli/tools/command.py +103 -0
  213. cecli/tools/command_interactive.py +113 -0
  214. cecli/tools/context_manager.py +175 -0
  215. cecli/tools/delete_block.py +154 -0
  216. cecli/tools/delete_line.py +120 -0
  217. cecli/tools/delete_lines.py +144 -0
  218. cecli/tools/extract_lines.py +281 -0
  219. cecli/tools/finished.py +35 -0
  220. cecli/tools/git_branch.py +132 -0
  221. cecli/tools/git_diff.py +49 -0
  222. cecli/tools/git_log.py +43 -0
  223. cecli/tools/git_remote.py +39 -0
  224. cecli/tools/git_show.py +37 -0
  225. cecli/tools/git_status.py +32 -0
  226. cecli/tools/grep.py +242 -0
  227. cecli/tools/indent_lines.py +195 -0
  228. cecli/tools/insert_block.py +263 -0
  229. cecli/tools/list_changes.py +71 -0
  230. cecli/tools/load_skill.py +51 -0
  231. cecli/tools/ls.py +77 -0
  232. cecli/tools/remove_skill.py +51 -0
  233. cecli/tools/replace_all.py +113 -0
  234. cecli/tools/replace_line.py +135 -0
  235. cecli/tools/replace_lines.py +180 -0
  236. cecli/tools/replace_text.py +186 -0
  237. cecli/tools/show_numbered_context.py +137 -0
  238. cecli/tools/thinking.py +52 -0
  239. cecli/tools/undo_change.py +82 -0
  240. cecli/tools/update_todo_list.py +148 -0
  241. cecli/tools/utils/base_tool.py +64 -0
  242. cecli/tools/utils/helpers.py +359 -0
  243. cecli/tools/utils/output.py +119 -0
  244. cecli/tools/utils/registry.py +145 -0
  245. cecli/tools/view_files_matching.py +138 -0
  246. cecli/tools/view_files_with_symbol.py +117 -0
  247. cecli/tui/__init__.py +83 -0
  248. cecli/tui/app.py +971 -0
  249. cecli/tui/io.py +566 -0
  250. cecli/tui/styles.tcss +117 -0
  251. cecli/tui/widgets/__init__.py +19 -0
  252. cecli/tui/widgets/completion_bar.py +331 -0
  253. cecli/tui/widgets/file_list.py +76 -0
  254. cecli/tui/widgets/footer.py +165 -0
  255. cecli/tui/widgets/input_area.py +320 -0
  256. cecli/tui/widgets/key_hints.py +16 -0
  257. cecli/tui/widgets/output.py +354 -0
  258. cecli/tui/widgets/status_bar.py +279 -0
  259. cecli/tui/worker.py +160 -0
  260. cecli/urls.py +16 -0
  261. cecli/utils.py +499 -0
  262. cecli/versioncheck.py +90 -0
  263. cecli/voice.py +90 -0
  264. cecli/waiting.py +38 -0
  265. cecli/watch.py +316 -0
  266. cecli/watch_prompts.py +12 -0
  267. cecli/website/Gemfile +8 -0
  268. cecli/website/_includes/blame.md +162 -0
  269. cecli/website/_includes/get-started.md +22 -0
  270. cecli/website/_includes/help-tip.md +5 -0
  271. cecli/website/_includes/help.md +24 -0
  272. cecli/website/_includes/install.md +5 -0
  273. cecli/website/_includes/keys.md +4 -0
  274. cecli/website/_includes/model-warnings.md +67 -0
  275. cecli/website/_includes/multi-line.md +22 -0
  276. cecli/website/_includes/python-m-aider.md +5 -0
  277. cecli/website/_includes/recording.css +228 -0
  278. cecli/website/_includes/recording.md +34 -0
  279. cecli/website/_includes/replit-pipx.md +9 -0
  280. cecli/website/_includes/works-best.md +1 -0
  281. cecli/website/_sass/custom/custom.scss +103 -0
  282. cecli/website/docs/config/adv-model-settings.md +2498 -0
  283. cecli/website/docs/config/agent-mode.md +320 -0
  284. cecli/website/docs/config/aider_conf.md +548 -0
  285. cecli/website/docs/config/api-keys.md +90 -0
  286. cecli/website/docs/config/custom-commands.md +187 -0
  287. cecli/website/docs/config/dotenv.md +493 -0
  288. cecli/website/docs/config/editor.md +127 -0
  289. cecli/website/docs/config/mcp.md +210 -0
  290. cecli/website/docs/config/model-aliases.md +173 -0
  291. cecli/website/docs/config/options.md +890 -0
  292. cecli/website/docs/config/reasoning.md +210 -0
  293. cecli/website/docs/config/skills.md +172 -0
  294. cecli/website/docs/config/tui.md +126 -0
  295. cecli/website/docs/config.md +44 -0
  296. cecli/website/docs/faq.md +379 -0
  297. cecli/website/docs/git.md +76 -0
  298. cecli/website/docs/index.md +47 -0
  299. cecli/website/docs/install/codespaces.md +39 -0
  300. cecli/website/docs/install/docker.md +48 -0
  301. cecli/website/docs/install/optional.md +100 -0
  302. cecli/website/docs/install/replit.md +8 -0
  303. cecli/website/docs/install.md +115 -0
  304. cecli/website/docs/languages.md +264 -0
  305. cecli/website/docs/legal/contributor-agreement.md +111 -0
  306. cecli/website/docs/legal/privacy.md +104 -0
  307. cecli/website/docs/llms/anthropic.md +77 -0
  308. cecli/website/docs/llms/azure.md +48 -0
  309. cecli/website/docs/llms/bedrock.md +132 -0
  310. cecli/website/docs/llms/cohere.md +34 -0
  311. cecli/website/docs/llms/deepseek.md +32 -0
  312. cecli/website/docs/llms/gemini.md +49 -0
  313. cecli/website/docs/llms/github.md +111 -0
  314. cecli/website/docs/llms/groq.md +36 -0
  315. cecli/website/docs/llms/lm-studio.md +39 -0
  316. cecli/website/docs/llms/ollama.md +75 -0
  317. cecli/website/docs/llms/openai-compat.md +39 -0
  318. cecli/website/docs/llms/openai.md +58 -0
  319. cecli/website/docs/llms/openrouter.md +78 -0
  320. cecli/website/docs/llms/other.md +117 -0
  321. cecli/website/docs/llms/vertex.md +50 -0
  322. cecli/website/docs/llms/warnings.md +10 -0
  323. cecli/website/docs/llms/xai.md +53 -0
  324. cecli/website/docs/llms.md +54 -0
  325. cecli/website/docs/more/analytics.md +127 -0
  326. cecli/website/docs/more/edit-formats.md +116 -0
  327. cecli/website/docs/more/infinite-output.md +192 -0
  328. cecli/website/docs/more-info.md +8 -0
  329. cecli/website/docs/recordings/auto-accept-architect.md +31 -0
  330. cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  331. cecli/website/docs/recordings/index.md +21 -0
  332. cecli/website/docs/recordings/model-accepts-settings.md +69 -0
  333. cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  334. cecli/website/docs/repomap.md +112 -0
  335. cecli/website/docs/scripting.md +100 -0
  336. cecli/website/docs/sessions.md +213 -0
  337. cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
  338. cecli/website/docs/troubleshooting/edit-errors.md +76 -0
  339. cecli/website/docs/troubleshooting/imports.md +62 -0
  340. cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
  341. cecli/website/docs/troubleshooting/support.md +79 -0
  342. cecli/website/docs/troubleshooting/token-limits.md +96 -0
  343. cecli/website/docs/troubleshooting/warnings.md +12 -0
  344. cecli/website/docs/troubleshooting.md +11 -0
  345. cecli/website/docs/usage/browser.md +57 -0
  346. cecli/website/docs/usage/caching.md +49 -0
  347. cecli/website/docs/usage/commands.md +133 -0
  348. cecli/website/docs/usage/conventions.md +119 -0
  349. cecli/website/docs/usage/copypaste.md +136 -0
  350. cecli/website/docs/usage/images-urls.md +48 -0
  351. cecli/website/docs/usage/lint-test.md +118 -0
  352. cecli/website/docs/usage/modes.md +211 -0
  353. cecli/website/docs/usage/not-code.md +179 -0
  354. cecli/website/docs/usage/notifications.md +87 -0
  355. cecli/website/docs/usage/tips.md +79 -0
  356. cecli/website/docs/usage/tutorials.md +30 -0
  357. cecli/website/docs/usage/voice.md +121 -0
  358. cecli/website/docs/usage/watch.md +294 -0
  359. cecli/website/docs/usage.md +102 -0
  360. cecli/website/share/index.md +101 -0
  361. cecli_dev-0.95.5.dist-info/METADATA +549 -0
  362. cecli_dev-0.95.5.dist-info/RECORD +366 -0
  363. cecli_dev-0.95.5.dist-info/WHEEL +5 -0
  364. cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
  365. cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
  366. cecli_dev-0.95.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,331 @@
1
+ """Completion bar widget for autocomplete suggestions."""
2
+
3
+ import os
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.message import Message
7
+ from textual.widget import Widget
8
+ from textual.widgets import Static
9
+
10
+
11
+ class CompletionBar(Widget, can_focus=False):
12
+ """Bar showing autocomplete suggestions above input (non-focusable)."""
13
+
14
+ MAX_SUGGESTIONS = 50
15
+ WINDOW_SIZE = 6
16
+
17
+ DEFAULT_CSS = """
18
+ CompletionBar {
19
+ height: 1;
20
+ background: $surface;
21
+ margin: 0 0;
22
+ padding: 0 0;
23
+ layout: horizontal;
24
+ }
25
+
26
+ CompletionBar .completion-prefix {
27
+ width: auto;
28
+ height: 1;
29
+ margin-right: 1;
30
+ color: $secondary;
31
+ background: $surface;
32
+ }
33
+
34
+ CompletionBar .completion-item {
35
+ width: auto;
36
+ height: 1;
37
+ margin-right: 2;
38
+ color: $secondary;
39
+ background: $surface;
40
+ }
41
+
42
+ CompletionBar .completion-item.selected {
43
+ color: $primary;
44
+ text-style: bold;
45
+ }
46
+
47
+ CompletionBar .completion-item.preselected {
48
+ color: $secondary;
49
+ }
50
+
51
+ CompletionBar .completion-more {
52
+ width: auto;
53
+ height: 1;
54
+ margin-right: 1;
55
+ color: $panel;
56
+ }
57
+
58
+ CompletionBar .completion-hint {
59
+ width: auto;
60
+ height: 1;
61
+ color: $panel;
62
+ dock: right;
63
+ }
64
+ """
65
+
66
+ class Selected(Message):
67
+ """Completion selected message."""
68
+
69
+ def __init__(self, value: str):
70
+ self.value = value
71
+ super().__init__()
72
+
73
+ class Dismissed(Message):
74
+ """Completion bar dismissed."""
75
+
76
+ pass
77
+
78
+ def __init__(self, suggestions: list[str] = None, prefix: str = "", **kwargs):
79
+ """Initialize completion bar.
80
+
81
+ Args:
82
+ suggestions: List of completion suggestions
83
+ prefix: Current input prefix to complete from
84
+ """
85
+ super().__init__(**kwargs)
86
+ self.suggestions = (suggestions or [])[: self.MAX_SUGGESTIONS]
87
+ self.prefix = prefix
88
+ self.selected_index = 0
89
+ self._has_cycled = False # Track if user has actively cycled through suggestions
90
+ self._item_widgets: list[Static] = []
91
+ self._prefix_widget: Static | None = None
92
+ self._left_more: Static | None = None
93
+ self._right_more: Static | None = None
94
+ self._hint: Static | None = None
95
+
96
+ # Compute common directory prefix and display names
97
+ self._common_prefix = ""
98
+ self._display_names: list[str] = []
99
+ self._compute_display_names()
100
+
101
+ @property
102
+ def current_selection(self) -> str | None:
103
+ """Get currently selected suggestion."""
104
+ if self.suggestions and 0 <= self.selected_index < len(self.suggestions):
105
+ return self.suggestions[self.selected_index]
106
+ return None
107
+
108
+ def _compute_display_names(self) -> None:
109
+ """Compute common directory prefix and short display names."""
110
+ if not self.suggestions:
111
+ self._common_prefix = ""
112
+ self._display_names = []
113
+ return
114
+
115
+ # Check if these look like file paths (contain /)
116
+ has_paths = any("/" in s for s in self.suggestions)
117
+
118
+ if not has_paths:
119
+ # Commands or non-path items - show as-is
120
+ self._common_prefix = ""
121
+ self._display_names = self.suggestions[:]
122
+ return
123
+
124
+ # Find common directory prefix
125
+ dirs = [os.path.dirname(s) for s in self.suggestions]
126
+ if dirs and all(d == dirs[0] for d in dirs) and dirs[0]:
127
+ # All in same directory
128
+ self._common_prefix = dirs[0] + "/"
129
+ self._display_names = [os.path.basename(s) for s in self.suggestions]
130
+ else:
131
+ # Find longest common path prefix
132
+ common = os.path.commonpath(self.suggestions) if self.suggestions else ""
133
+ if common and "/" in common:
134
+ # Use the directory part of common prefix
135
+ self._common_prefix = common.rsplit("/", 1)[0] + "/" if "/" in common else ""
136
+ if self._common_prefix:
137
+ self._display_names = [s[len(self._common_prefix) :] for s in self.suggestions]
138
+ else:
139
+ self._display_names = self.suggestions[:]
140
+ else:
141
+ self._common_prefix = ""
142
+ self._display_names = self.suggestions[:]
143
+
144
+ def compose(self) -> ComposeResult:
145
+ """Create the bar layout."""
146
+ # Directory prefix (shown once)
147
+ self._prefix_widget = Static(self._common_prefix, classes="completion-prefix")
148
+ self._prefix_widget.display = bool(self._common_prefix)
149
+ yield self._prefix_widget
150
+
151
+ self._left_more = Static("…", classes="completion-more")
152
+ self._left_more.display = False
153
+ yield self._left_more
154
+
155
+ self._item_widgets = []
156
+ for i in range(self.WINDOW_SIZE):
157
+ if i < len(self._display_names):
158
+ selected_class = "selected" if self._has_cycled else "preselected"
159
+ classes = f"completion-item {selected_class}" if i == 0 else "completion-item"
160
+ item = Static(self._display_names[i], classes=classes)
161
+ else:
162
+ item = Static("", classes="completion-item")
163
+ item.display = False
164
+ self._item_widgets.append(item)
165
+ yield item
166
+
167
+ # Show "+N more" instead of just ellipsis
168
+ remaining = len(self.suggestions) - self.WINDOW_SIZE
169
+ more_text = f"+{remaining}" if remaining > 0 else ""
170
+ self._right_more = Static(more_text, classes="completion-more")
171
+ self._right_more.display = remaining > 0
172
+ yield self._right_more
173
+
174
+ self._hint = Static("Tab ↹ Enter ⏎ Esc ✗", classes="completion-hint")
175
+ yield self._hint
176
+
177
+ def update_suggestions(self, suggestions: list[str], prefix: str = "") -> None:
178
+ """Update suggestions in place."""
179
+ self.suggestions = suggestions[: self.MAX_SUGGESTIONS]
180
+ self.prefix = prefix
181
+ self.selected_index = 0
182
+ self._has_cycled = False # Reset cycling flag when suggestions change
183
+
184
+ # Recompute display names
185
+ self._compute_display_names()
186
+
187
+ # Update prefix widget
188
+ if self._prefix_widget:
189
+ self._prefix_widget.update(self._common_prefix)
190
+ self._prefix_widget.display = bool(self._common_prefix)
191
+
192
+ self._refresh_items()
193
+ self._set_selection_classes()
194
+
195
+ def _refresh_items(self) -> None:
196
+ """Update visible items - selected item always shown first."""
197
+ # Ensure meta widgets exist
198
+ if self._left_more is None or self._left_more.parent is None:
199
+ self._left_more = Static("", classes="completion-more")
200
+ self.mount(
201
+ self._left_more, before=self._item_widgets[0] if self._item_widgets else None
202
+ )
203
+ if self._right_more is None or self._right_more.parent is None:
204
+ self._right_more = Static("", classes="completion-more")
205
+ self.mount(self._right_more, after=self._left_more if self._left_more else None)
206
+ if self._hint is None or self._hint.parent is None:
207
+ self._hint = Static("Tab ↹ Enter ⏎ Esc ✗", classes="completion-hint")
208
+ self.mount(self._hint)
209
+
210
+ # Grow the widget list to the window size
211
+ while len(self._item_widgets) < self.WINDOW_SIZE:
212
+ new_item = Static("", classes="completion-item")
213
+ self._item_widgets.append(new_item)
214
+ target = (
215
+ self._right_more if self._right_more and self._right_more.parent else self._hint
216
+ )
217
+ self.mount(new_item, before=target)
218
+
219
+ if not self._display_names:
220
+ for item in self._item_widgets:
221
+ item.display = False
222
+ if self._left_more:
223
+ self._left_more.display = False
224
+ if self._right_more:
225
+ self._right_more.display = False
226
+ return
227
+
228
+ # Build display order: selected item first, then others after it
229
+ total = len(self._display_names)
230
+ items_before = self.selected_index
231
+ # items_after = total - self.selected_index - 1
232
+
233
+ # Show indicator if there are items before selected
234
+ if self._left_more:
235
+ if items_before > 0:
236
+ self._left_more.update(f"{items_before}+")
237
+ self._left_more.display = True
238
+ else:
239
+ self._left_more.display = False
240
+
241
+ # Fill window: selected first, then subsequent items
242
+ window_size = min(self.WINDOW_SIZE, total)
243
+ visible_indices = []
244
+
245
+ # Always include selected
246
+ visible_indices.append(self.selected_index)
247
+
248
+ # Add items after selected
249
+ for i in range(1, window_size):
250
+ next_idx = self.selected_index + i
251
+ if next_idx < total:
252
+ visible_indices.append(next_idx)
253
+
254
+ # Update item widgets
255
+ for i, item in enumerate(self._item_widgets):
256
+ if i < len(visible_indices):
257
+ display_index = visible_indices[i]
258
+ item.update(self._display_names[display_index])
259
+ item.display = True
260
+ else:
261
+ item.display = False
262
+
263
+ # Show indicator for remaining items after visible window
264
+ remaining_after = total - (self.selected_index + len(visible_indices))
265
+ if self._right_more:
266
+ if remaining_after > 0:
267
+ self._right_more.update(f"+{remaining_after}")
268
+ self._right_more.display = True
269
+ else:
270
+ self._right_more.display = False
271
+
272
+ def _set_selection_classes(self) -> None:
273
+ """Apply selected class - first visible item is always selected."""
274
+ for i, item in enumerate(self._item_widgets):
275
+ if not item.display:
276
+ item.remove_class("selected")
277
+ item.remove_class("preselected")
278
+ continue
279
+ # First item is always the selected one
280
+ if i == 0:
281
+ # Use "preselected" style if we haven't cycled yet and are at index 0
282
+ if not self._has_cycled and self.selected_index == 0:
283
+ item.add_class("preselected")
284
+ item.remove_class("selected")
285
+ else:
286
+ item.add_class("selected")
287
+ item.remove_class("preselected")
288
+ else:
289
+ item.remove_class("selected")
290
+ item.remove_class("preselected")
291
+
292
+ def _update_selection(self) -> None:
293
+ """Update visual selection state."""
294
+ if not self.suggestions:
295
+ return
296
+ self._refresh_items()
297
+ self._set_selection_classes()
298
+
299
+ def cycle_next(self) -> None:
300
+ """Cycle to next suggestion."""
301
+ if self.suggestions:
302
+ if not self._has_cycled:
303
+ self._has_cycled = True # User has actively cycled
304
+ else:
305
+ self.selected_index = (self.selected_index + 1) % len(self.suggestions)
306
+
307
+ self._update_selection()
308
+
309
+ def cycle_previous(self) -> None:
310
+ """Cycle to previous suggestion."""
311
+ if self.suggestions:
312
+ if not self._has_cycled:
313
+ self._has_cycled = True # User has actively cycled
314
+ else:
315
+ if not self.selected_index:
316
+ self.selected_index = len(self.suggestions) - 1
317
+ else:
318
+ self.selected_index = (self.selected_index - 1) % len(self.suggestions)
319
+
320
+ self._update_selection()
321
+
322
+ def select_current(self) -> None:
323
+ """Select current suggestion and dismiss."""
324
+ if self.suggestions:
325
+ self.post_message(self.Selected(self.suggestions[self.selected_index]))
326
+ self.remove()
327
+
328
+ def dismiss(self) -> None:
329
+ """Dismiss without selecting."""
330
+ self.post_message(self.Dismissed())
331
+ self.remove()
@@ -0,0 +1,76 @@
1
+ from rich.columns import Columns
2
+ from rich.console import Group
3
+ from textual.widgets import Static
4
+
5
+
6
+ class FileList(Static):
7
+ """Widget to display the list of files in chat."""
8
+
9
+ def update_files(self, chat_files):
10
+ """Update the file list display."""
11
+ if not chat_files:
12
+ self.update("")
13
+ return
14
+
15
+ rel_fnames = chat_files.get("rel_fnames", [])
16
+ rel_read_only_fnames = chat_files.get("rel_read_only_fnames", [])
17
+ rel_read_only_stubs_fnames = chat_files.get("rel_read_only_stubs_fnames", [])
18
+
19
+ total_files = (
20
+ len(rel_fnames)
21
+ + len(rel_read_only_fnames or [])
22
+ + len(rel_read_only_stubs_fnames or [])
23
+ )
24
+
25
+ if total_files == 0:
26
+ self.add_class("empty")
27
+ self.update("")
28
+ return
29
+ else:
30
+ self.remove_class("empty")
31
+
32
+ # For very large numbers of files, use a summary display
33
+ if total_files > 20:
34
+ read_only_count = len(rel_read_only_fnames or [])
35
+ stub_file_count = len(rel_read_only_stubs_fnames or [])
36
+ editable_count = len([f for f in rel_fnames if f not in (rel_read_only_fnames or [])])
37
+
38
+ summary = f"{editable_count} editable file(s)"
39
+ if read_only_count > 0:
40
+ summary += f", {read_only_count} read-only file(s)"
41
+ if stub_file_count > 0:
42
+ summary += f", {stub_file_count} stub file(s)"
43
+ summary += " (use /ls to list all files)"
44
+ self.update(summary)
45
+ return
46
+
47
+ renderables = []
48
+
49
+ # Handle read-only files
50
+ if rel_read_only_fnames or rel_read_only_stubs_fnames:
51
+ ro_paths = []
52
+ # Regular read-only files
53
+ for rel_path in sorted(rel_read_only_fnames or []):
54
+ ro_paths.append(rel_path)
55
+ # Stub files with (stub) marker
56
+ for rel_path in sorted(rel_read_only_stubs_fnames or []):
57
+ ro_paths.append(f"{rel_path} (stub)")
58
+
59
+ if ro_paths:
60
+ files_with_label = ["Readonly:"] + ro_paths
61
+ renderables.append(Columns(files_with_label))
62
+
63
+ # Handle editable files
64
+ editable_files = [
65
+ f
66
+ for f in sorted(rel_fnames)
67
+ if f not in rel_read_only_fnames and f not in rel_read_only_stubs_fnames
68
+ ]
69
+ if editable_files:
70
+ files_with_label = editable_files
71
+ if rel_read_only_fnames or rel_read_only_stubs_fnames:
72
+ files_with_label = ["Editable:"] + editable_files
73
+
74
+ renderables.append(Columns(files_with_label))
75
+
76
+ self.update(Group(*renderables))
@@ -0,0 +1,165 @@
1
+ """Footer widget for cecli TUI."""
2
+
3
+ from rich.text import Text
4
+ from textual.reactive import reactive
5
+ from textual.widgets import Static
6
+
7
+
8
+ class MainFooter(Static):
9
+ """Footer showing mode, model, project, git, and cost."""
10
+
11
+ # Left side info
12
+ coder_mode = reactive("code")
13
+ model_name = reactive("")
14
+
15
+ # Right side info
16
+ project_name = reactive("")
17
+ git_branch = reactive("")
18
+ git_dirty = reactive(0)
19
+ cost = reactive(0.0)
20
+
21
+ # Spinner state
22
+ spinner_text = reactive("")
23
+ spinner_suffix = reactive("")
24
+ spinner_visible = reactive(False)
25
+ _spinner_frame = 0
26
+ _spinner_chars = "⠏⠛⠹⠼⠶⠧"
27
+
28
+ def __init__(
29
+ self,
30
+ model_name: str = "",
31
+ project_name: str = "",
32
+ git_branch: str = "",
33
+ coder_mode: str = "code",
34
+ **kwargs,
35
+ ):
36
+ """Initialize footer.
37
+
38
+ Args:
39
+ model_name: Name of the AI model
40
+ project_name: Name of the project folder
41
+ git_branch: Current git branch name
42
+ coder_mode: Current edit mode (code, agent, architect, etc.)
43
+ """
44
+ super().__init__(**kwargs)
45
+ self.model_name = model_name
46
+ self.project_name = project_name
47
+ self.git_branch = git_branch
48
+ self.coder_mode = coder_mode
49
+ self._spinner_interval = None
50
+
51
+ def on_mount(self):
52
+ """Start spinner animation interval."""
53
+ self._spinner_interval = self.set_interval(0.1, self._animate_spinner)
54
+
55
+ def _animate_spinner(self):
56
+ """Animate the spinner character."""
57
+ if self.spinner_visible:
58
+ self._spinner_frame = (self._spinner_frame + 1) % len(self._spinner_chars)
59
+ self.refresh()
60
+
61
+ def _get_display_model(self) -> str:
62
+ """Get shortened model name for display."""
63
+ if not self.model_name:
64
+ return ""
65
+ # Strip common prefixes like "openrouter/x-ai/"
66
+ name = self.app.worker.coder.main_model.name
67
+ if len(name) > 40:
68
+ if "/" in name:
69
+ name = name.split("/")[-1]
70
+
71
+ if len(name) > 35:
72
+ name = name[:35] + "..."
73
+
74
+ return name
75
+
76
+ def render(self) -> Text:
77
+ """Render the footer with left/right split."""
78
+
79
+ # Build left side: spinner/mode + model
80
+ left = Text()
81
+
82
+ if self.spinner_visible:
83
+ spinner_char = self._spinner_chars[self._spinner_frame]
84
+ left.append(f"{spinner_char} ")
85
+ if self.spinner_text:
86
+ left.append(self.spinner_text)
87
+
88
+ if self.spinner_suffix:
89
+ left.append(" • ")
90
+ left.append(self.spinner_suffix)
91
+ else:
92
+ left.append("cecli")
93
+ left.append(" • ")
94
+ left.append(self._get_display_model())
95
+
96
+ # Build right side: mode + model + project + git
97
+ right = Text()
98
+
99
+ if self.coder_mode:
100
+ right.append(f"{self.coder_mode}")
101
+ right.append(" • ")
102
+
103
+ # model_display = self._get_display_model()
104
+ # if model_display:
105
+ # right.append(f"{model_display}")
106
+ # right.append(" • ")
107
+
108
+ if self.project_name:
109
+ right.append(f"{self.project_name}")
110
+ right.append(" • ")
111
+
112
+ if self.git_branch:
113
+ right.append(self.git_branch)
114
+ if self.git_dirty:
115
+ right.append(f" +{self.git_dirty}")
116
+ # right.append(" ")
117
+
118
+ # Always show cost
119
+ # right.append(f"${self.cost:.2f}")
120
+
121
+ # Calculate padding to right-align
122
+ try:
123
+ total_width = self.size.width
124
+ except Exception:
125
+ total_width = 80
126
+
127
+ left_len = len(left.plain)
128
+ right_len = len(right.plain)
129
+ padding = max(1, total_width - left_len - right_len)
130
+
131
+ # Combine: left + padding + right
132
+ result = Text()
133
+ result.append_text(left)
134
+ result.append(" " * padding)
135
+ result.append_text(right)
136
+
137
+ return result
138
+
139
+ def update_cost(self, cost: float):
140
+ """Update the displayed cost."""
141
+ self.cost = cost
142
+ self.refresh()
143
+
144
+ def update_git(self, branch: str, dirty_count: int = 0):
145
+ """Update git status display."""
146
+ self.git_branch = branch
147
+ self.git_dirty = dirty_count
148
+ self.refresh()
149
+
150
+ def update_mode(self, mode: str):
151
+ """Update the chat mode display."""
152
+ self.coder_mode = mode
153
+ self.refresh()
154
+
155
+ def start_spinner(self, text: str = ""):
156
+ """Show spinner with optional text."""
157
+ self.spinner_text = text
158
+ self.spinner_visible = True
159
+ self.refresh()
160
+
161
+ def stop_spinner(self):
162
+ """Hide spinner."""
163
+ self.spinner_visible = False
164
+ self.spinner_text = ""
165
+ self.refresh()