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.
- cecli/__init__.py +20 -0
- cecli/__main__.py +4 -0
- cecli/_version.py +34 -0
- cecli/args.py +1092 -0
- cecli/args_formatter.py +228 -0
- cecli/change_tracker.py +133 -0
- cecli/coders/__init__.py +38 -0
- cecli/coders/agent_coder.py +1872 -0
- cecli/coders/architect_coder.py +63 -0
- cecli/coders/ask_coder.py +8 -0
- cecli/coders/base_coder.py +3993 -0
- cecli/coders/chat_chunks.py +116 -0
- cecli/coders/context_coder.py +52 -0
- cecli/coders/copypaste_coder.py +269 -0
- cecli/coders/editblock_coder.py +656 -0
- cecli/coders/editblock_fenced_coder.py +9 -0
- cecli/coders/editblock_func_coder.py +140 -0
- cecli/coders/editor_diff_fenced_coder.py +8 -0
- cecli/coders/editor_editblock_coder.py +8 -0
- cecli/coders/editor_whole_coder.py +8 -0
- cecli/coders/help_coder.py +15 -0
- cecli/coders/patch_coder.py +705 -0
- cecli/coders/search_replace.py +757 -0
- cecli/coders/shell.py +37 -0
- cecli/coders/single_wholefile_func_coder.py +101 -0
- cecli/coders/udiff_coder.py +428 -0
- cecli/coders/udiff_simple.py +12 -0
- cecli/coders/wholefile_coder.py +143 -0
- cecli/coders/wholefile_func_coder.py +133 -0
- cecli/commands/__init__.py +192 -0
- cecli/commands/add.py +226 -0
- cecli/commands/agent.py +51 -0
- cecli/commands/architect.py +46 -0
- cecli/commands/ask.py +44 -0
- cecli/commands/chat_mode.py +0 -0
- cecli/commands/clear.py +37 -0
- cecli/commands/code.py +46 -0
- cecli/commands/command_prefix.py +44 -0
- cecli/commands/commit.py +52 -0
- cecli/commands/context.py +47 -0
- cecli/commands/context_blocks.py +124 -0
- cecli/commands/context_management.py +51 -0
- cecli/commands/copy.py +62 -0
- cecli/commands/copy_context.py +81 -0
- cecli/commands/core.py +287 -0
- cecli/commands/diff.py +68 -0
- cecli/commands/drop.py +217 -0
- cecli/commands/editor.py +78 -0
- cecli/commands/exit.py +55 -0
- cecli/commands/git.py +57 -0
- cecli/commands/help.py +140 -0
- cecli/commands/history_search.py +40 -0
- cecli/commands/lint.py +109 -0
- cecli/commands/list_sessions.py +56 -0
- cecli/commands/load.py +85 -0
- cecli/commands/load_session.py +48 -0
- cecli/commands/load_skill.py +68 -0
- cecli/commands/ls.py +75 -0
- cecli/commands/map.py +37 -0
- cecli/commands/map_refresh.py +35 -0
- cecli/commands/model.py +118 -0
- cecli/commands/models.py +41 -0
- cecli/commands/multiline_mode.py +38 -0
- cecli/commands/paste.py +91 -0
- cecli/commands/quit.py +32 -0
- cecli/commands/read_only.py +267 -0
- cecli/commands/read_only_stub.py +270 -0
- cecli/commands/reasoning_effort.py +70 -0
- cecli/commands/remove_skill.py +68 -0
- cecli/commands/report.py +40 -0
- cecli/commands/reset.py +88 -0
- cecli/commands/run.py +99 -0
- cecli/commands/save.py +49 -0
- cecli/commands/save_session.py +43 -0
- cecli/commands/settings.py +69 -0
- cecli/commands/test.py +58 -0
- cecli/commands/think_tokens.py +74 -0
- cecli/commands/tokens.py +207 -0
- cecli/commands/undo.py +145 -0
- cecli/commands/utils/__init__.py +0 -0
- cecli/commands/utils/base_command.py +131 -0
- cecli/commands/utils/helpers.py +142 -0
- cecli/commands/utils/registry.py +53 -0
- cecli/commands/utils/save_load_manager.py +98 -0
- cecli/commands/voice.py +78 -0
- cecli/commands/weak_model.py +123 -0
- cecli/commands/web.py +87 -0
- cecli/deprecated_args.py +185 -0
- cecli/diffs.py +129 -0
- cecli/dump.py +29 -0
- cecli/editor.py +147 -0
- cecli/exceptions.py +115 -0
- cecli/format_settings.py +26 -0
- cecli/help.py +119 -0
- cecli/help_pats.py +19 -0
- cecli/helpers/__init__.py +9 -0
- cecli/helpers/copypaste.py +123 -0
- cecli/helpers/coroutines.py +8 -0
- cecli/helpers/file_searcher.py +142 -0
- cecli/helpers/model_providers.py +552 -0
- cecli/helpers/plugin_manager.py +81 -0
- cecli/helpers/profiler.py +162 -0
- cecli/helpers/requests.py +77 -0
- cecli/helpers/similarity.py +98 -0
- cecli/helpers/skills.py +577 -0
- cecli/history.py +186 -0
- cecli/io.py +1782 -0
- cecli/linter.py +304 -0
- cecli/llm.py +101 -0
- cecli/main.py +1280 -0
- cecli/mcp/__init__.py +154 -0
- cecli/mcp/oauth.py +250 -0
- cecli/mcp/server.py +278 -0
- cecli/mdstream.py +243 -0
- cecli/models.py +1255 -0
- cecli/onboarding.py +301 -0
- cecli/prompts/__init__.py +0 -0
- cecli/prompts/agent.yml +71 -0
- cecli/prompts/architect.yml +35 -0
- cecli/prompts/ask.yml +31 -0
- cecli/prompts/base.yml +99 -0
- cecli/prompts/context.yml +60 -0
- cecli/prompts/copypaste.yml +5 -0
- cecli/prompts/editblock.yml +143 -0
- cecli/prompts/editblock_fenced.yml +106 -0
- cecli/prompts/editblock_func.yml +25 -0
- cecli/prompts/editor_diff_fenced.yml +115 -0
- cecli/prompts/editor_editblock.yml +121 -0
- cecli/prompts/editor_whole.yml +46 -0
- cecli/prompts/help.yml +37 -0
- cecli/prompts/patch.yml +110 -0
- cecli/prompts/single_wholefile_func.yml +24 -0
- cecli/prompts/udiff.yml +106 -0
- cecli/prompts/udiff_simple.yml +13 -0
- cecli/prompts/utils/__init__.py +0 -0
- cecli/prompts/utils/prompt_registry.py +167 -0
- cecli/prompts/utils/system.py +56 -0
- cecli/prompts/wholefile.yml +50 -0
- cecli/prompts/wholefile_func.yml +24 -0
- cecli/queries/tree-sitter-language-pack/README.md +7 -0
- cecli/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/c-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- cecli/queries/tree-sitter-language-pack/clojure-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/commonlisp-tags.scm +127 -0
- cecli/queries/tree-sitter-language-pack/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-language-pack/csharp-tags.scm +32 -0
- cecli/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/dart-tags.scm +97 -0
- cecli/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-language-pack/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- cecli/queries/tree-sitter-language-pack/go-tags.scm +49 -0
- cecli/queries/tree-sitter-language-pack/java-tags.scm +26 -0
- cecli/queries/tree-sitter-language-pack/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-language-pack/lua-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +101 -0
- cecli/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- cecli/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- cecli/queries/tree-sitter-language-pack/python-tags.scm +24 -0
- cecli/queries/tree-sitter-language-pack/r-tags.scm +27 -0
- cecli/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- cecli/queries/tree-sitter-language-pack/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-language-pack/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- cecli/queries/tree-sitter-language-pack/swift-tags.scm +54 -0
- cecli/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- cecli/queries/tree-sitter-languages/README.md +24 -0
- cecli/queries/tree-sitter-languages/c-tags.scm +12 -0
- cecli/queries/tree-sitter-languages/c_sharp-tags.scm +52 -0
- cecli/queries/tree-sitter-languages/cpp-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/dart-tags.scm +92 -0
- cecli/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- cecli/queries/tree-sitter-languages/elixir-tags.scm +59 -0
- cecli/queries/tree-sitter-languages/elm-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/fortran-tags.scm +18 -0
- cecli/queries/tree-sitter-languages/go-tags.scm +36 -0
- cecli/queries/tree-sitter-languages/haskell-tags.scm +5 -0
- cecli/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- cecli/queries/tree-sitter-languages/java-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/javascript-tags.scm +96 -0
- cecli/queries/tree-sitter-languages/julia-tags.scm +60 -0
- cecli/queries/tree-sitter-languages/kotlin-tags.scm +30 -0
- cecli/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- cecli/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- cecli/queries/tree-sitter-languages/ocaml_interface-tags.scm +104 -0
- cecli/queries/tree-sitter-languages/php-tags.scm +32 -0
- cecli/queries/tree-sitter-languages/python-tags.scm +22 -0
- cecli/queries/tree-sitter-languages/ql-tags.scm +26 -0
- cecli/queries/tree-sitter-languages/ruby-tags.scm +69 -0
- cecli/queries/tree-sitter-languages/rust-tags.scm +63 -0
- cecli/queries/tree-sitter-languages/scala-tags.scm +64 -0
- cecli/queries/tree-sitter-languages/typescript-tags.scm +44 -0
- cecli/queries/tree-sitter-languages/zig-tags.scm +20 -0
- cecli/reasoning_tags.py +82 -0
- cecli/repo.py +626 -0
- cecli/repomap.py +1368 -0
- cecli/report.py +260 -0
- cecli/resources/__init__.py +3 -0
- cecli/resources/model-metadata.json +25751 -0
- cecli/resources/model-settings.yml +2394 -0
- cecli/resources/providers.json +67 -0
- cecli/run_cmd.py +143 -0
- cecli/scrape.py +295 -0
- cecli/sendchat.py +250 -0
- cecli/sessions.py +281 -0
- cecli/special.py +203 -0
- cecli/tools/__init__.py +72 -0
- cecli/tools/command.py +103 -0
- cecli/tools/command_interactive.py +113 -0
- cecli/tools/context_manager.py +175 -0
- cecli/tools/delete_block.py +154 -0
- cecli/tools/delete_line.py +120 -0
- cecli/tools/delete_lines.py +144 -0
- cecli/tools/extract_lines.py +281 -0
- cecli/tools/finished.py +35 -0
- cecli/tools/git_branch.py +132 -0
- cecli/tools/git_diff.py +49 -0
- cecli/tools/git_log.py +43 -0
- cecli/tools/git_remote.py +39 -0
- cecli/tools/git_show.py +37 -0
- cecli/tools/git_status.py +32 -0
- cecli/tools/grep.py +242 -0
- cecli/tools/indent_lines.py +195 -0
- cecli/tools/insert_block.py +263 -0
- cecli/tools/list_changes.py +71 -0
- cecli/tools/load_skill.py +51 -0
- cecli/tools/ls.py +77 -0
- cecli/tools/remove_skill.py +51 -0
- cecli/tools/replace_all.py +113 -0
- cecli/tools/replace_line.py +135 -0
- cecli/tools/replace_lines.py +180 -0
- cecli/tools/replace_text.py +186 -0
- cecli/tools/show_numbered_context.py +137 -0
- cecli/tools/thinking.py +52 -0
- cecli/tools/undo_change.py +82 -0
- cecli/tools/update_todo_list.py +148 -0
- cecli/tools/utils/base_tool.py +64 -0
- cecli/tools/utils/helpers.py +359 -0
- cecli/tools/utils/output.py +119 -0
- cecli/tools/utils/registry.py +145 -0
- cecli/tools/view_files_matching.py +138 -0
- cecli/tools/view_files_with_symbol.py +117 -0
- cecli/tui/__init__.py +83 -0
- cecli/tui/app.py +971 -0
- cecli/tui/io.py +566 -0
- cecli/tui/styles.tcss +117 -0
- cecli/tui/widgets/__init__.py +19 -0
- cecli/tui/widgets/completion_bar.py +331 -0
- cecli/tui/widgets/file_list.py +76 -0
- cecli/tui/widgets/footer.py +165 -0
- cecli/tui/widgets/input_area.py +320 -0
- cecli/tui/widgets/key_hints.py +16 -0
- cecli/tui/widgets/output.py +354 -0
- cecli/tui/widgets/status_bar.py +279 -0
- cecli/tui/worker.py +160 -0
- cecli/urls.py +16 -0
- cecli/utils.py +499 -0
- cecli/versioncheck.py +90 -0
- cecli/voice.py +90 -0
- cecli/waiting.py +38 -0
- cecli/watch.py +316 -0
- cecli/watch_prompts.py +12 -0
- cecli/website/Gemfile +8 -0
- cecli/website/_includes/blame.md +162 -0
- cecli/website/_includes/get-started.md +22 -0
- cecli/website/_includes/help-tip.md +5 -0
- cecli/website/_includes/help.md +24 -0
- cecli/website/_includes/install.md +5 -0
- cecli/website/_includes/keys.md +4 -0
- cecli/website/_includes/model-warnings.md +67 -0
- cecli/website/_includes/multi-line.md +22 -0
- cecli/website/_includes/python-m-aider.md +5 -0
- cecli/website/_includes/recording.css +228 -0
- cecli/website/_includes/recording.md +34 -0
- cecli/website/_includes/replit-pipx.md +9 -0
- cecli/website/_includes/works-best.md +1 -0
- cecli/website/_sass/custom/custom.scss +103 -0
- cecli/website/docs/config/adv-model-settings.md +2498 -0
- cecli/website/docs/config/agent-mode.md +320 -0
- cecli/website/docs/config/aider_conf.md +548 -0
- cecli/website/docs/config/api-keys.md +90 -0
- cecli/website/docs/config/custom-commands.md +187 -0
- cecli/website/docs/config/dotenv.md +493 -0
- cecli/website/docs/config/editor.md +127 -0
- cecli/website/docs/config/mcp.md +210 -0
- cecli/website/docs/config/model-aliases.md +173 -0
- cecli/website/docs/config/options.md +890 -0
- cecli/website/docs/config/reasoning.md +210 -0
- cecli/website/docs/config/skills.md +172 -0
- cecli/website/docs/config/tui.md +126 -0
- cecli/website/docs/config.md +44 -0
- cecli/website/docs/faq.md +379 -0
- cecli/website/docs/git.md +76 -0
- cecli/website/docs/index.md +47 -0
- cecli/website/docs/install/codespaces.md +39 -0
- cecli/website/docs/install/docker.md +48 -0
- cecli/website/docs/install/optional.md +100 -0
- cecli/website/docs/install/replit.md +8 -0
- cecli/website/docs/install.md +115 -0
- cecli/website/docs/languages.md +264 -0
- cecli/website/docs/legal/contributor-agreement.md +111 -0
- cecli/website/docs/legal/privacy.md +104 -0
- cecli/website/docs/llms/anthropic.md +77 -0
- cecli/website/docs/llms/azure.md +48 -0
- cecli/website/docs/llms/bedrock.md +132 -0
- cecli/website/docs/llms/cohere.md +34 -0
- cecli/website/docs/llms/deepseek.md +32 -0
- cecli/website/docs/llms/gemini.md +49 -0
- cecli/website/docs/llms/github.md +111 -0
- cecli/website/docs/llms/groq.md +36 -0
- cecli/website/docs/llms/lm-studio.md +39 -0
- cecli/website/docs/llms/ollama.md +75 -0
- cecli/website/docs/llms/openai-compat.md +39 -0
- cecli/website/docs/llms/openai.md +58 -0
- cecli/website/docs/llms/openrouter.md +78 -0
- cecli/website/docs/llms/other.md +117 -0
- cecli/website/docs/llms/vertex.md +50 -0
- cecli/website/docs/llms/warnings.md +10 -0
- cecli/website/docs/llms/xai.md +53 -0
- cecli/website/docs/llms.md +54 -0
- cecli/website/docs/more/analytics.md +127 -0
- cecli/website/docs/more/edit-formats.md +116 -0
- cecli/website/docs/more/infinite-output.md +192 -0
- cecli/website/docs/more-info.md +8 -0
- cecli/website/docs/recordings/auto-accept-architect.md +31 -0
- cecli/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- cecli/website/docs/recordings/index.md +21 -0
- cecli/website/docs/recordings/model-accepts-settings.md +69 -0
- cecli/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- cecli/website/docs/repomap.md +112 -0
- cecli/website/docs/scripting.md +100 -0
- cecli/website/docs/sessions.md +213 -0
- cecli/website/docs/troubleshooting/aider-not-found.md +24 -0
- cecli/website/docs/troubleshooting/edit-errors.md +76 -0
- cecli/website/docs/troubleshooting/imports.md +62 -0
- cecli/website/docs/troubleshooting/models-and-keys.md +54 -0
- cecli/website/docs/troubleshooting/support.md +79 -0
- cecli/website/docs/troubleshooting/token-limits.md +96 -0
- cecli/website/docs/troubleshooting/warnings.md +12 -0
- cecli/website/docs/troubleshooting.md +11 -0
- cecli/website/docs/usage/browser.md +57 -0
- cecli/website/docs/usage/caching.md +49 -0
- cecli/website/docs/usage/commands.md +133 -0
- cecli/website/docs/usage/conventions.md +119 -0
- cecli/website/docs/usage/copypaste.md +136 -0
- cecli/website/docs/usage/images-urls.md +48 -0
- cecli/website/docs/usage/lint-test.md +118 -0
- cecli/website/docs/usage/modes.md +211 -0
- cecli/website/docs/usage/not-code.md +179 -0
- cecli/website/docs/usage/notifications.md +87 -0
- cecli/website/docs/usage/tips.md +79 -0
- cecli/website/docs/usage/tutorials.md +30 -0
- cecli/website/docs/usage/voice.md +121 -0
- cecli/website/docs/usage/watch.md +294 -0
- cecli/website/docs/usage.md +102 -0
- cecli/website/share/index.md +101 -0
- cecli_dev-0.95.5.dist-info/METADATA +549 -0
- cecli_dev-0.95.5.dist-info/RECORD +366 -0
- cecli_dev-0.95.5.dist-info/WHEEL +5 -0
- cecli_dev-0.95.5.dist-info/entry_points.txt +4 -0
- cecli_dev-0.95.5.dist-info/licenses/LICENSE.txt +202 -0
- 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()
|