From fe417e90b3e3db1748c2d1ec690bb7a187114bd2 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Tue, 23 Apr 2024 17:38:17 -0700 Subject: [PATCH 01/82] Updated HISTORY --- HISTORY.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 6f861900c..b2a47558c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,13 @@ # Release history +### v0.30.0 + +- Added [Gemini 1.5 Pro](https://aider.chat/docs/llms.html#free-models) as a recommended free model. +- Allow repo map for "whole" edit format. +- Added `--models ` to search the available models. +- Added `--no-show-model-warnings` to silence model warnings. + ### v0.29.2 - Improved [model warnings](https://aider.chat/docs/llms.html#model-warnings) for unknown or unfamiliar models From b14ca861c1709cc3e53160560f09f6ebf16f2d66 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Tue, 23 Apr 2024 17:39:41 -0700 Subject: [PATCH 02/82] version bump to 0.30.0 --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index 62af5f6da..0bda2bd09 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1 +1 @@ -__version__ = "0.29.3-dev" +__version__ = "0.30.0" From c6d6171da00cebc63e5d70ceddb6fef1a3467a4d Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Tue, 23 Apr 2024 17:40:19 -0700 Subject: [PATCH 03/82] set version to 0.30.1-dev --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index 0bda2bd09..8fc5fdd9d 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1 +1 @@ -__version__ = "0.30.0" +__version__ = "0.30.1-dev" From 80b4ebe6f7ff00e8bb54ccfd4abe20eeb4101cac Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Wed, 24 Apr 2024 11:46:50 -0700 Subject: [PATCH 04/82] include google-generativeai; bump all dep versions --- requirements.txt | 77 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0ac4889d4..1cfc8cff2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,8 @@ backoff==2.2.1 # via -r requirements.in beautifulsoup4==4.12.3 # via -r requirements.in +cachetools==5.3.3 + # via google-auth certifi==2024.2.2 # via # httpcore @@ -56,14 +58,48 @@ gitdb==4.0.11 # via gitpython gitpython==3.1.43 # via -r requirements.in +google-ai-generativelanguage==0.6.2 + # via google-generativeai +google-api-core[grpc]==2.18.0 + # via + # google-ai-generativelanguage + # google-api-python-client + # google-generativeai +google-api-python-client==2.127.0 + # via google-generativeai +google-auth==2.29.0 + # via + # google-ai-generativelanguage + # google-api-core + # google-api-python-client + # google-auth-httplib2 + # google-generativeai +google-auth-httplib2==0.2.0 + # via google-api-python-client +google-generativeai==0.5.2 + # via -r requirements.in +googleapis-common-protos==1.63.0 + # via + # google-api-core + # grpcio-status greenlet==3.0.3 # via playwright grep-ast==0.2.4 # via -r requirements.in +grpcio==1.62.2 + # via + # google-api-core + # grpcio-status +grpcio-status==1.62.2 + # via google-api-core h11==0.14.0 # via httpcore httpcore==1.0.5 # via httpx +httplib2==0.22.0 + # via + # google-api-python-client + # google-auth-httplib2 httpx==0.27.0 # via openai huggingface-hub==0.22.2 @@ -82,7 +118,7 @@ jsonschema==4.21.1 # via -r requirements.in jsonschema-specifications==2023.12.1 # via jsonschema -litellm==1.35.18 +litellm==1.35.23 # via -r requirements.in markdown-it-py==3.0.0 # via rich @@ -100,7 +136,7 @@ numpy==1.26.4 # via # -r requirements.in # scipy -openai==1.23.2 +openai==1.23.3 # via # -r requirements.in # litellm @@ -118,11 +154,31 @@ playwright==1.43.0 # via -r requirements.in prompt-toolkit==3.0.43 # via -r requirements.in +proto-plus==1.23.0 + # via + # google-ai-generativelanguage + # google-api-core +protobuf==4.25.3 + # via + # google-ai-generativelanguage + # google-api-core + # google-generativeai + # googleapis-common-protos + # grpcio-status + # proto-plus +pyasn1==0.6.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth pycparser==2.22 # via cffi -pydantic==2.7.0 - # via openai -pydantic-core==2.18.1 +pydantic==2.7.1 + # via + # google-generativeai + # openai +pydantic-core==2.18.2 # via pydantic pyee==11.1.0 # via playwright @@ -130,13 +186,15 @@ pygments==2.17.2 # via rich pypandoc==1.13 # via -r requirements.in +pyparsing==3.1.2 + # via httplib2 python-dotenv==1.0.1 # via litellm pyyaml==6.0.1 # via # -r requirements.in # huggingface-hub -referencing==0.34.0 +referencing==0.35.0 # via # jsonschema # jsonschema-specifications @@ -144,6 +202,7 @@ regex==2024.4.16 # via tiktoken requests==2.31.0 # via + # google-api-core # huggingface-hub # litellm # tiktoken @@ -153,6 +212,8 @@ rpds-py==0.18.0 # via # jsonschema # referencing +rsa==4.9 + # via google-auth scipy==1.13.0 # via -r requirements.in smmap==5.0.1 @@ -176,6 +237,7 @@ tokenizers==0.19.1 # via litellm tqdm==4.66.2 # via + # google-generativeai # huggingface-hub # openai tree-sitter==0.21.3 @@ -184,11 +246,14 @@ tree-sitter-languages==1.10.2 # via grep-ast typing-extensions==4.11.0 # via + # google-generativeai # huggingface-hub # openai # pydantic # pydantic-core # pyee +uritemplate==4.1.1 + # via google-api-python-client urllib3==2.2.1 # via requests wcwidth==0.2.13 From fe6257b513b2fd8064a073156e474397566e6982 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Wed, 24 Apr 2024 14:13:21 -0700 Subject: [PATCH 05/82] copy --- docs/faq.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 5de6e8e2e..e7aeff8ae 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -117,8 +117,6 @@ User `aider --help` to see all the command line options, but these are useful fo You can also script aider from python: ```python -import os -import openai from aider.coders import Coder # This is a list of files to add to the chat From 60d92feaa8f543c1a60db4448aae879d9d048f52 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Wed, 24 Apr 2024 15:59:52 -0700 Subject: [PATCH 06/82] copy --- docs/faq.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index e7aeff8ae..2b3147563 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -118,11 +118,12 @@ You can also script aider from python: ```python from aider.coders import Coder +from aider.models import Model # This is a list of files to add to the chat -fnames = ["foo.py"] +fnames = ["greeting.py"] -model = models.Model("gpt-4-turbo", weak_model="gpt-3.5-turbo") +model = Model("gpt-4-turbo", weak_model="gpt-3.5-turbo") # Create a coder object coder = Coder.create(main_model=model, fnames=fnames) From d6bd563fc37f37937556fab33ff8cf4da30739cb Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Wed, 24 Apr 2024 17:20:13 -0700 Subject: [PATCH 07/82] version bump to 0.30.1 --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index 8fc5fdd9d..8925285f0 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1 +1 @@ -__version__ = "0.30.1-dev" +__version__ = "0.30.1" From 4b68376695a9351e5643439d0dd137ebb6fff8c4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Wed, 24 Apr 2024 17:20:51 -0700 Subject: [PATCH 08/82] set version to 0.30.2-dev --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index 8925285f0..223c95fc0 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1 +1 @@ -__version__ = "0.30.1" +__version__ = "0.30.2-dev" From ed392205da623835f4e762e86fa237f23da0e623 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Wed, 24 Apr 2024 17:21:46 -0700 Subject: [PATCH 09/82] Updated HISTORY --- HISTORY.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index b2a47558c..ed7cd9b35 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,10 @@ # Release history +### v0.30.1 + +- Adding missing `google-generativeai` dependency + ### v0.30.0 - Added [Gemini 1.5 Pro](https://aider.chat/docs/llms.html#free-models) as a recommended free model. From 6ebfd15a71844c815611ae40380e689923c6b0b0 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Thu, 25 Apr 2024 11:35:26 -0700 Subject: [PATCH 10/82] initial --- aider/gui.py | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100755 aider/gui.py diff --git a/aider/gui.py b/aider/gui.py new file mode 100755 index 000000000..57010e315 --- /dev/null +++ b/aider/gui.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python + +import os +import sys +from pathlib import Path +from collections import defaultdict + +from aider.dump import dump + +import streamlit as st + +from aider.dump import dump +from aider.coders import Coder +from aider.models import Model + +from streamlit_file_browser import st_file_browser +from streamlit_searchbox import st_searchbox +from st_row_buttons import st_row_buttons + + +if 'recent_msgs_num' not in st.session_state: + st.session_state.recent_msgs_num = 0 + +recent_msgs_label = "Recent chat messages" +def recent_msgs(): + msgs = [ + "write a python program that shows off some python features", + "write a tsx program that shows off some language features", + "refactor the Frobulator.simplify method to be a stand alone function", + "lorem ipsum dolor", + "lorem adipiscing adipiscing et dolore sit elit aliqua dolore ut incididunt", + "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed labore aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim aliqua enim tempor adipiscing sit et" + ] + msgs = 30 * msgs + + return st.selectbox( + recent_msgs_label, + msgs, + placeholder = "Recent chat messages", + label_visibility = "collapsed", + index = None, + key=f"recent_msgs_{st.session_state.recent_msgs_num}", + ) + + +def search(text=None): + results = [] + for root, _, files in os.walk("aider"): + for file in files: + path = os.path.join(root, file) + if not text or text in path: + results.append(path) + #dump(results) + + return results + +#selected_value = st_searchbox(search) + + +model = Model("gpt-3.5-turbo", weak_model="gpt-3.5-turbo") +fnames = ["greeting.py"] +coder = Coder.create(main_model=model, fnames=fnames, use_git=False) + +import random + +lorem_words = [ + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", + "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", + "incididunt", "ut", "labore", "et", "dolore", "magna", + "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", + "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", + "incididunt", "ut", "labore", "et", "dolore", "magna", + "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", + "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", + "incididunt", "ut", "labore", "et", "dolore", "magna", + "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" + "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", + "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", + "incididunt", "ut", "labore", "et", "dolore", "magna", + "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" + "\n\n", + "\n\n", + "\n\n", +] + +def generate_lorem_text(min_words=10, max_words=50): + num_words = random.randint(min_words, max_words) + words = random.sample(lorem_words, num_words) + return " ".join(words) + +with st.sidebar: + st.header("Aider") + + cmds_tab, settings_tab = st.tabs(["Commands", "Settings"]) + +with cmds_tab: + with st.expander("Add to the chat", expanded=True): + st.multiselect( + "Files for the LLM to edit", + search(), + default=["aider/main.py","aider/io.py"], + help="Only add the files that need to be *edited* for the task you are working in. Aider will pull in other code to provide relevant context to the LLM.", + ) + with st.popover("Add web page"): + st.markdown("www") + name = st.text_input("URL?") + with st.popover("Add image"): + st.markdown("Hello World 👋") + st.file_uploader("Image file") + with st.popover("Run shell command"): + st.markdown("## Run a shell command and share the output with aider") + name = st.text_input("Cmd") + st.selectbox("Recent commands", [ + "my_app.py --doit", + "my_app.py --cleanup", + ]) + + with st.popover("Run test command"): + st.markdown("Hello World 👋") + name = st.text_input("Test") + + with st.expander("Costs and context", expanded=True): + st.button("Show token usage") + st.button("Clear chat history") + st.metric("Per message context cost", "$0.0013", help="foo") + st.metric("Total cost this session", "$0.22") + + + with st.expander("Git", expanded=True): + st.button("Show last diff") + st.button("Undo last commit") + st.button("Commit pending changes") + with st.popover("Run git command"): + st.markdown("## Run git command") + name = st.text_input("git", value="git ") + st.button("Run") + st.selectbox("Recent git commands", [ + "git checkout -b experiment", + "git stash", + ]) + + + +#chat_tab, settings_tab = st.tabs(["Chat", "Settings"]) + + +messages = st.container() +messages.container(height = 1200, border=False) +recent_msgs_empty = st.empty() + +with recent_msgs_empty: + old_prompt = recent_msgs() + +if "messages" not in st.session_state: + st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you?"}] + + +for msg in st.session_state.messages: + messages.chat_message(msg["role"]).write(msg["content"]) + +_=''' +with st.expander("Commands"): + st.markdown("## Run git command") + name = st.text_input("gitx", value="git ") + st.button("Run it") + st.selectbox("Recent chat commands", [ + "git checkout -b experiment", + "git stash", + ]) +''' + + +prompt = st.chat_input("Say something") +dump(old_prompt, prompt) + +if old_prompt: + prompt = old_prompt + st.session_state.recent_msgs_num += 1 + with recent_msgs_empty: + old_prompt = recent_msgs() + +if prompt: + + st.session_state.messages.append({"role": "user", "content": prompt}) + with messages.chat_message("user"): + st.write(prompt) + + res = coder.run(prompt) + st.session_state.messages.append({"role": "assistant", "content": res}) + + with messages.chat_message("ai"): + st.write(res) From 7a6bce381e527e83d113f44105db3a339f140f5b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 06:11:56 -0700 Subject: [PATCH 11/82] better --- aider/coders/wholefile_prompts.py | 4 +- aider/gui.py | 332 ++++++++++++++++++++++-------- 2 files changed, 253 insertions(+), 83 deletions(-) diff --git a/aider/coders/wholefile_prompts.py b/aider/coders/wholefile_prompts.py index e410fdd9c..dda10fe3c 100644 --- a/aider/coders/wholefile_prompts.py +++ b/aider/coders/wholefile_prompts.py @@ -18,14 +18,14 @@ Once you understand the request you MUST: You MUST use this *file listing* format: path/to/filename.js -{fence[0]} +{fence[0]}javascript // entire file content ... // ... goes in between {fence[1]} Every *file listing* MUST use this format: - First line: the filename with any originally provided path -- Second line: opening {fence[0]} +- Second line: opening {fence[0]} including the code language - ... entire content of the file ... - Final line: closing {fence[1]} diff --git a/aider/gui.py b/aider/gui.py index 57010e315..e2f92daf0 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -1,27 +1,70 @@ #!/usr/bin/env python import os +import random import sys from pathlib import Path -from collections import defaultdict - -from aider.dump import dump import streamlit as st - -from aider.dump import dump -from aider.coders import Coder -from aider.models import Model - +from st_row_buttons import st_row_buttons from streamlit_file_browser import st_file_browser from streamlit_searchbox import st_searchbox -from st_row_buttons import st_row_buttons +from aider.coders import Coder +from aider.dump import dump +from aider.models import Model -if 'recent_msgs_num' not in st.session_state: +if "recent_msgs_num" not in st.session_state: st.session_state.recent_msgs_num = 0 -recent_msgs_label = "Recent chat messages" +chat_controls = None + +diff = Path("aider/tmp.diff").read_text() + + +def mock_tool_output(): + global chat_controls + + messages = """Commit `33a242c`: Added sample python that highlights language features \nApplied edit to new_program.py""" + messages = """Applied edit to new_program.py""" + # st.info(messages) + + if chat_controls: + chat_controls.empty() + chat_controls = st.empty() + with chat_controls: + container = st.container() + + with container: + # cols = st.columns([0.8,0.2]) + # with cols[0]: + + # with st.expander(messages): + # diff = Path("aider/tmp.diff").read_text() + # st.code(diff, language="diff") + with st.expander( + f"Commit `33a242c`: Added sample python that highlights language features \n{messages}" + ): + # st.info(messages) + st.code(diff, language="diff") + st.button( + "Undo commit `33a242c`", + key=random.random(), + help="wtf?", + ) + + if False: + st.button("Allow edits to `foobar.py`", key=random.random(), help="??") + st.button("Allow creation of new file `some/new/file.js`", key=random.random()) + st.button("Add `baz/foo.py` to the chat", key=random.random()) + + # st.toggle("Show diffs", key=random.random()) + # st.toggle("Undo this commit `33a242c`", key=random.random(), help="?") + + # cost = random.random() * 0.003 + 0.001 + # st.caption(f"${cost:0.4f} to send the next message", help="# header\n- list\n- list\n") + + def recent_msgs(): msgs = [ "write a python program that shows off some python features", @@ -29,16 +72,20 @@ def recent_msgs(): "refactor the Frobulator.simplify method to be a stand alone function", "lorem ipsum dolor", "lorem adipiscing adipiscing et dolore sit elit aliqua dolore ut incididunt", - "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed labore aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim aliqua enim tempor adipiscing sit et" + ( + "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed labore" + " aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim aliqua enim" + " tempor adipiscing sit et" + ), ] msgs = 30 * msgs return st.selectbox( - recent_msgs_label, + "N/A", msgs, - placeholder = "Recent chat messages", - label_visibility = "collapsed", - index = None, + placeholder="Resend recent chat message", + label_visibility="collapsed", + index=None, key=f"recent_msgs_{st.session_state.recent_msgs_num}", ) @@ -50,58 +97,156 @@ def search(text=None): path = os.path.join(root, file) if not text or text in path: results.append(path) - #dump(results) + # dump(results) return results -#selected_value = st_searchbox(search) + +# selected_value = st_searchbox(search) -model = Model("gpt-3.5-turbo", weak_model="gpt-3.5-turbo") +model = Model("claude-3-haiku-20240307") fnames = ["greeting.py"] -coder = Coder.create(main_model=model, fnames=fnames, use_git=False) +coder = Coder.create(main_model=model, fnames=fnames, use_git=False, pretty=False) import random lorem_words = [ - "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", - "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", - "incididunt", "ut", "labore", "et", "dolore", "magna", - "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" - "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", - "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", - "incididunt", "ut", "labore", "et", "dolore", "magna", - "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" - "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", - "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", - "incididunt", "ut", "labore", "et", "dolore", "magna", - "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" - "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", - "adipiscing", "elit", "sed", "do", "eiusmod", "tempor", - "incididunt", "ut", "labore", "et", "dolore", "magna", - "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud" - "\n\n", + "lorem", + "ipsum", + "dolor", + "sit", + "amet", + "consectetur", + "adipiscing", + "elit", + "sed", + "do", + "eiusmod", + "tempor", + "incididunt", + "ut", + "labore", + "et", + "dolore", + "magna", + "aliqua", + "enim", + "ad", + "minim", + "veniam", + "quis", + "nostrudlorem", + "ipsum", + "dolor", + "sit", + "amet", + "consectetur", + "adipiscing", + "elit", + "sed", + "do", + "eiusmod", + "tempor", + "incididunt", + "ut", + "labore", + "et", + "dolore", + "magna", + "aliqua", + "enim", + "ad", + "minim", + "veniam", + "quis", + "nostrudlorem", + "ipsum", + "dolor", + "sit", + "amet", + "consectetur", + "adipiscing", + "elit", + "sed", + "do", + "eiusmod", + "tempor", + "incididunt", + "ut", + "labore", + "et", + "dolore", + "magna", + "aliqua", + "enim", + "ad", + "minim", + "veniam", + "quis", + "nostrudlorem", + "ipsum", + "dolor", + "sit", + "amet", + "consectetur", + "adipiscing", + "elit", + "sed", + "do", + "eiusmod", + "tempor", + "incididunt", + "ut", + "labore", + "et", + "dolore", + "magna", + "aliqua", + "enim", + "ad", + "minim", + "veniam", + "quis", + "nostrud\n\n", "\n\n", "\n\n", ] + def generate_lorem_text(min_words=10, max_words=50): num_words = random.randint(min_words, max_words) words = random.sample(lorem_words, num_words) return " ".join(words) + with st.sidebar: - st.header("Aider") + st.title("Aider") cmds_tab, settings_tab = st.tabs(["Commands", "Settings"]) with cmds_tab: + with st.expander("Recommended actions", expanded=True): + with st.popover("Create a git repo to track changes"): + st.write( + "Aider works best when your code is stored in a git repo. \n[See the FAQ for more" + " info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" + ) + st.button("Create git repo", key=random.random(), help="?") + + with st.popover("Update your `.gitignore` file"): + st.write("It's best to keep aider's internal files out of your git repo.") + st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") + with st.expander("Add to the chat", expanded=True): st.multiselect( "Files for the LLM to edit", search(), - default=["aider/main.py","aider/io.py"], - help="Only add the files that need to be *edited* for the task you are working in. Aider will pull in other code to provide relevant context to the LLM.", + default=["aider/main.py", "aider/io.py"], + help=( + "Only add the files that need to be *edited* for the task you are working on. Aider" + " will pull in other code to provide relevant context to the LLM." + ), ) with st.popover("Add web page"): st.markdown("www") @@ -109,46 +254,62 @@ with cmds_tab: with st.popover("Add image"): st.markdown("Hello World 👋") st.file_uploader("Image file") - with st.popover("Run shell command"): - st.markdown("## Run a shell command and share the output with aider") - name = st.text_input("Cmd") - st.selectbox("Recent commands", [ - "my_app.py --doit", - "my_app.py --cleanup", - ]) + with st.popover("Run shell commands, tests, etc"): + st.markdown( + "Run a shell command and optionally share the output with the LLM. This is a great" + " way to run your program or run tests and have the LLM fix bugs." + ) + name = st.text_input("Command:") + st.radio( + "Share the command output with the LLM?", + [ + "Review the output and decide whether to share", + "Automatically share the output on non-zero exit code (ie, if any tests fail)", + ], + ) + st.selectbox( + "Recent commands", + [ + "my_app.py --doit", + "my_app.py --cleanup", + ], + ) - with st.popover("Run test command"): - st.markdown("Hello World 👋") - name = st.text_input("Test") - - with st.expander("Costs and context", expanded=True): - st.button("Show token usage") + with st.expander("Tokens and costs", expanded=True): + with st.popover("Show token usage"): + st.write("hi") st.button("Clear chat history") - st.metric("Per message context cost", "$0.0013", help="foo") + # st.metric("Cost of last message send & reply", "$0.0019", help="foo") + # st.metric("Cost to send next message", "$0.0013", help="foo") st.metric("Total cost this session", "$0.22") - - with st.expander("Git", expanded=True): - st.button("Show last diff") - st.button("Undo last commit") - st.button("Commit pending changes") + with st.expander("Git", expanded=False): + # st.button("Show last diff") + # st.button("Undo last commit") + st.button("Commit any pending changes") with st.popover("Run git command"): st.markdown("## Run git command") name = st.text_input("git", value="git ") st.button("Run") - st.selectbox("Recent git commands", [ - "git checkout -b experiment", - "git stash", - ]) + st.selectbox( + "Recent git commands", + [ + "git checkout -b experiment", + "git stash", + ], + ) + + recent_msgs_empty = st.empty() - -#chat_tab, settings_tab = st.tabs(["Chat", "Settings"]) +# chat_tab, settings_tab = st.tabs(["Chat", "Settings"]) messages = st.container() -messages.container(height = 1200, border=False) -recent_msgs_empty = st.empty() + +# stuff a bunch of vertical whitespace at the top +# to get all the chat text to the bottom +messages.container(height=1200, border=False) with recent_msgs_empty: old_prompt = recent_msgs() @@ -158,22 +319,24 @@ if "messages" not in st.session_state: for msg in st.session_state.messages: - messages.chat_message(msg["role"]).write(msg["content"]) + with messages.chat_message(msg["role"]): + st.write(msg["content"]) + cost = random.random() * 0.003 + 0.001 + st.caption(f"${cost:0.4f}") -_=''' -with st.expander("Commands"): - st.markdown("## Run git command") - name = st.text_input("gitx", value="git ") - st.button("Run it") - st.selectbox("Recent chat commands", [ - "git checkout -b experiment", - "git stash", - ]) -''' +with messages: + mock_tool_output() + +# inp_empty = st.empty() -prompt = st.chat_input("Say something") -dump(old_prompt, prompt) +def clear_controls(): + if chat_controls: + chat_controls.empty() + + +prompt = st.chat_input("Say something", on_submit=clear_controls) +# dump(old_prompt, prompt) if old_prompt: prompt = old_prompt @@ -182,6 +345,8 @@ if old_prompt: old_prompt = recent_msgs() if prompt: + if chat_controls: + chat_controls.empty() st.session_state.messages.append({"role": "user", "content": prompt}) with messages.chat_message("user"): @@ -190,5 +355,10 @@ if prompt: res = coder.run(prompt) st.session_state.messages.append({"role": "assistant", "content": res}) - with messages.chat_message("ai"): + with messages.chat_message("assistant"): st.write(res) + cost = random.random() * 0.003 + 0.001 + st.caption(f"${cost:0.4f}") + + with messages: + mock_tool_output() From e2d0a94d58d7d7b1c10cf37779279a694553562b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 06:12:07 -0700 Subject: [PATCH 12/82] better --- aider/gui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index e2f92daf0..68f1ca9f4 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -109,8 +109,6 @@ model = Model("claude-3-haiku-20240307") fnames = ["greeting.py"] coder = Coder.create(main_model=model, fnames=fnames, use_git=False, pretty=False) -import random - lorem_words = [ "lorem", "ipsum", From 0925c4b8b3f153204836d39f09eec3637f7bd180 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 06:13:40 -0700 Subject: [PATCH 13/82] better --- aider/gui.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 68f1ca9f4..ea8c77d09 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -2,16 +2,12 @@ import os import random -import sys from pathlib import Path import streamlit as st -from st_row_buttons import st_row_buttons -from streamlit_file_browser import st_file_browser -from streamlit_searchbox import st_searchbox from aider.coders import Coder -from aider.dump import dump +from aider.dump import dump # noqa: F401 from aider.models import Model if "recent_msgs_num" not in st.session_state: @@ -25,7 +21,6 @@ diff = Path("aider/tmp.diff").read_text() def mock_tool_output(): global chat_controls - messages = """Commit `33a242c`: Added sample python that highlights language features \nApplied edit to new_program.py""" messages = """Applied edit to new_program.py""" # st.info(messages) From d21de778ffd6ab44445baa838b17f97ed100d81b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 11:24:42 -0700 Subject: [PATCH 14/82] announcements --- aider/coders/base_coder.py | 90 +++++++++++++++----------- aider/gui.py | 127 +++++-------------------------------- aider/main.py | 28 ++++---- 3 files changed, 85 insertions(+), 160 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index daaa5fc7e..883ce8269 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -16,7 +16,7 @@ from jsonschema import Draft7Validator from rich.console import Console, Text from rich.markdown import Markdown -from aider import models, prompts, utils +from aider import __version__, models, prompts, utils from aider.commands import Commands from aider.history import ChatSummary from aider.io import InputOutput @@ -80,6 +80,54 @@ class Coder: else: raise ValueError(f"Unknown edit format {edit_format}") + def get_announcements(self): + lines = [] + lines.append(f"Aider v{__version__}") + + # Model + main_model = self.main_model + weak_model = main_model.weak_model + prefix = "Model:" + output = f" {main_model.name} with {self.edit_format} edit format" + if weak_model is not main_model: + prefix = "Models:" + output += f", weak model {weak_model.name}" + lines.append(prefix + output) + + # Repo + if self.repo: + rel_repo_dir = self.repo.get_rel_repo_dir() + num_files = len(self.repo.get_tracked_files()) + lines.append(f"Git repo: {rel_repo_dir} with {num_files:,} files") + if num_files > 1000: + lines.append( + "Warning: For large repos, consider using an .aiderignore file to ignore" + " irrelevant files/dirs." + ) + else: + lines.append("Git repo: none") + + # Repo-map + map_tokens = self.repo_map.max_map_tokens if self.repo_map else 0 + if map_tokens > 0 and self.repo_map: + lines.append(f"Repo-map: using {map_tokens} tokens") + max_map_tokens = 2048 + if map_tokens > max_map_tokens: + lines.append( + f"Warning: map-tokens > {max_map_tokens} is not recommended as too much" + " irrelevant code can confuse GPT." + ) + elif not map_tokens: + lines.append("Repo-map: disabled because map_tokens == 0") + else: + lines.append("Repo-map: disabled") + + # Files + for fname in self.get_inchat_relative_files(): + lines.append(f"Added {fname} to the chat.") + + return lines + def __init__( self, main_model, @@ -136,15 +184,6 @@ class Coder: self.main_model = main_model - weak_model = main_model.weak_model - prefix = "Model:" - output = f" {main_model.name} with {self.edit_format} edit format" - if weak_model is not main_model: - prefix = "Models:" - output += f", weak model {weak_model.name}" - - self.io.tool_output(prefix + output) - self.show_diffs = show_diffs self.commands = Commands(self.io, self, voice_language) @@ -181,17 +220,7 @@ class Coder: self.abs_fnames.add(fname) self.check_added_files() - if self.repo: - rel_repo_dir = self.repo.get_rel_repo_dir() - num_files = len(self.repo.get_tracked_files()) - self.io.tool_output(f"Git repo: {rel_repo_dir} with {num_files:,} files") - if num_files > 1000: - self.io.tool_error( - "Warning: For large repos, consider using an .aiderignore file to ignore" - " irrelevant files/dirs." - ) - else: - self.io.tool_output("Git repo: none") + if not self.repo: self.find_common_root() if main_model.use_repo_map and self.repo and self.gpt_prompts.repo_content_prefix: @@ -204,22 +233,6 @@ class Coder: self.verbose, ) - if map_tokens > 0 and self.repo_map: - self.io.tool_output(f"Repo-map: using {map_tokens} tokens") - max_map_tokens = 2048 - if map_tokens > max_map_tokens: - self.io.tool_error( - f"Warning: map-tokens > {max_map_tokens} is not recommended as too much" - " irrelevant code can confuse GPT." - ) - elif not map_tokens: - self.io.tool_output("Repo-map: disabled because map_tokens == 0") - else: - self.io.tool_output("Repo-map: disabled") - - for fname in self.get_inchat_relative_files(): - self.io.tool_output(f"Added {fname} to the chat.") - self.summarizer = ChatSummary( self.main_model.weak_model, self.main_model.max_chat_history_tokens, @@ -237,6 +250,9 @@ class Coder: self.io.tool_output("JSON Schema:") self.io.tool_output(json.dumps(self.functions, indent=4)) + for line in self.get_announcements(): + self.io.tool_output(line) + def find_common_root(self): if len(self.abs_fnames) == 1: self.root = os.path.dirname(list(self.abs_fnames)[0]) diff --git a/aider/gui.py b/aider/gui.py index ea8c77d09..632b2f795 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -8,7 +8,7 @@ import streamlit as st from aider.coders import Coder from aider.dump import dump # noqa: F401 -from aider.models import Model +from aider.main import main if "recent_msgs_num" not in st.session_state: st.session_state.recent_msgs_num = 0 @@ -100,122 +100,25 @@ def search(text=None): # selected_value = st_searchbox(search) -model = Model("claude-3-haiku-20240307") -fnames = ["greeting.py"] -coder = Coder.create(main_model=model, fnames=fnames, use_git=False, pretty=False) - -lorem_words = [ - "lorem", - "ipsum", - "dolor", - "sit", - "amet", - "consectetur", - "adipiscing", - "elit", - "sed", - "do", - "eiusmod", - "tempor", - "incididunt", - "ut", - "labore", - "et", - "dolore", - "magna", - "aliqua", - "enim", - "ad", - "minim", - "veniam", - "quis", - "nostrudlorem", - "ipsum", - "dolor", - "sit", - "amet", - "consectetur", - "adipiscing", - "elit", - "sed", - "do", - "eiusmod", - "tempor", - "incididunt", - "ut", - "labore", - "et", - "dolore", - "magna", - "aliqua", - "enim", - "ad", - "minim", - "veniam", - "quis", - "nostrudlorem", - "ipsum", - "dolor", - "sit", - "amet", - "consectetur", - "adipiscing", - "elit", - "sed", - "do", - "eiusmod", - "tempor", - "incididunt", - "ut", - "labore", - "et", - "dolore", - "magna", - "aliqua", - "enim", - "ad", - "minim", - "veniam", - "quis", - "nostrudlorem", - "ipsum", - "dolor", - "sit", - "amet", - "consectetur", - "adipiscing", - "elit", - "sed", - "do", - "eiusmod", - "tempor", - "incididunt", - "ut", - "labore", - "et", - "dolore", - "magna", - "aliqua", - "enim", - "ad", - "minim", - "veniam", - "quis", - "nostrud\n\n", - "\n\n", - "\n\n", -] +@st.cache_resource +def get_coder(): + coder = main(return_coder=True) + if isinstance(coder, Coder): + return coder + raise ValueError() -def generate_lorem_text(min_words=10, max_words=50): - num_words = random.randint(min_words, max_words) - words = random.sample(lorem_words, num_words) - return " ".join(words) +coder = get_coder() + + +def announce(coder): + lines = coder.get_announcements() + lines = " \n".join(lines) + st.info(lines) with st.sidebar: st.title("Aider") - cmds_tab, settings_tab = st.tabs(["Commands", "Settings"]) with cmds_tab: @@ -303,6 +206,8 @@ messages = st.container() # stuff a bunch of vertical whitespace at the top # to get all the chat text to the bottom messages.container(height=1200, border=False) +with messages: + announce(coder) with recent_msgs_empty: old_prompt = recent_msgs() diff --git a/aider/main.py b/aider/main.py index ffee5ee6e..ca778382b 100644 --- a/aider/main.py +++ b/aider/main.py @@ -122,7 +122,7 @@ def check_gitignore(git_root, io, ask=True): io.tool_output(f"Added {pat} to .gitignore") -def main(argv=None, input=None, output=None, force_git_root=None): +def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): if argv is None: argv = sys.argv[1:] @@ -554,20 +554,14 @@ def main(argv=None, input=None, output=None, force_git_root=None): if args.git and not force_git_root: right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname) if right_repo_root: - return main(argv, input, output, right_repo_root) - - io.tool_output(f"Aider v{__version__}") + return main(argv, input, output, right_repo_root, return_coder=return_coder) if not args.skip_check_update: check_version(io.tool_error) if args.check_update: update_available = check_version(lambda msg: None) - sys.exit(0 if not update_available else 1) - - if "VSCODE_GIT_IPC_HANDLE" in os.environ: - args.pretty = False - io.tool_output("VSCode terminal detected, pretty output has been disabled.") + return 0 if not update_available else 1 if args.models: matches = models.fuzzy_match_models(args.models) @@ -652,6 +646,9 @@ def main(argv=None, input=None, output=None, force_git_root=None): io.tool_error(str(err)) return 1 + if return_coder: + return coder + if args.commit: coder.commands.cmd_commit("") return @@ -670,6 +667,10 @@ def main(argv=None, input=None, output=None, force_git_root=None): coder.apply_updates() return + if "VSCODE_GIT_IPC_HANDLE" in os.environ: + args.pretty = False + io.tool_output("VSCode terminal detected, pretty output has been disabled.") + io.tool_output("Use /help to see in-chat commands, run with --help to see cmd line args") if git_root and Path.cwd().resolve() != Path(git_root).resolve(): @@ -685,7 +686,9 @@ def main(argv=None, input=None, output=None, force_git_root=None): io.add_to_input_history(args.message) io.tool_output() coder.run(with_message=args.message) - elif args.message_file: + return + + if args.message_file: try: message_from_file = io.read_text(args.message_file) io.tool_output() @@ -696,8 +699,9 @@ def main(argv=None, input=None, output=None, force_git_root=None): except IOError as e: io.tool_error(f"Error reading message file: {e}") return 1 - else: - coder.run() + return + + coder.run() if __name__ == "__main__": From 3f5cdb603c3a3cfbb3eeca3f18917ca144a8d0c4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 13:20:14 -0700 Subject: [PATCH 15/82] fixed announcements --- aider/coders/base_coder.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 883ce8269..3bb929216 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -108,17 +108,18 @@ class Coder: lines.append("Git repo: none") # Repo-map - map_tokens = self.repo_map.max_map_tokens if self.repo_map else 0 - if map_tokens > 0 and self.repo_map: - lines.append(f"Repo-map: using {map_tokens} tokens") - max_map_tokens = 2048 - if map_tokens > max_map_tokens: - lines.append( - f"Warning: map-tokens > {max_map_tokens} is not recommended as too much" - " irrelevant code can confuse GPT." - ) - elif not map_tokens: - lines.append("Repo-map: disabled because map_tokens == 0") + if self.repo_map: + map_tokens = self.repo_map.max_map_tokens + if map_tokens > 0: + lines.append(f"Repo-map: using {map_tokens} tokens") + max_map_tokens = 2048 + if map_tokens > max_map_tokens: + lines.append( + f"Warning: map-tokens > {max_map_tokens} is not recommended as too much" + " irrelevant code can confuse GPT." + ) + else: + lines.append("Repo-map: disabled because map_tokens == 0") else: lines.append("Repo-map: disabled") From bf5557381481068d1352a040eb34159e82047637 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 14:23:42 -0700 Subject: [PATCH 16/82] refac --- aider/gui.py | 381 ++++++++++++++++++++++++++------------------------- 1 file changed, 195 insertions(+), 186 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 632b2f795..9358a4830 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -2,64 +2,22 @@ import os import random +import sys from pathlib import Path import streamlit as st from aider.coders import Coder from aider.dump import dump # noqa: F401 -from aider.main import main +from aider.main import main as cli_main if "recent_msgs_num" not in st.session_state: st.session_state.recent_msgs_num = 0 -chat_controls = None diff = Path("aider/tmp.diff").read_text() -def mock_tool_output(): - global chat_controls - - messages = """Applied edit to new_program.py""" - # st.info(messages) - - if chat_controls: - chat_controls.empty() - chat_controls = st.empty() - with chat_controls: - container = st.container() - - with container: - # cols = st.columns([0.8,0.2]) - # with cols[0]: - - # with st.expander(messages): - # diff = Path("aider/tmp.diff").read_text() - # st.code(diff, language="diff") - with st.expander( - f"Commit `33a242c`: Added sample python that highlights language features \n{messages}" - ): - # st.info(messages) - st.code(diff, language="diff") - st.button( - "Undo commit `33a242c`", - key=random.random(), - help="wtf?", - ) - - if False: - st.button("Allow edits to `foobar.py`", key=random.random(), help="??") - st.button("Allow creation of new file `some/new/file.js`", key=random.random()) - st.button("Add `baz/foo.py` to the chat", key=random.random()) - - # st.toggle("Show diffs", key=random.random()) - # st.toggle("Undo this commit `33a242c`", key=random.random(), help="?") - - # cost = random.random() * 0.003 + 0.001 - # st.caption(f"${cost:0.4f} to send the next message", help="# header\n- list\n- list\n") - - def recent_msgs(): msgs = [ "write a python program that shows off some python features", @@ -97,166 +55,217 @@ def search(text=None): return results -# selected_value = st_searchbox(search) - - @st.cache_resource def get_coder(): - coder = main(return_coder=True) + coder = cli_main(return_coder=True) if isinstance(coder, Coder): return coder raise ValueError() -coder = get_coder() +class GUI: + def announce(self): + lines = self.coder.get_announcements() + lines = " \n".join(lines) + st.info(lines) + def do_sidebar(self): + with st.sidebar: + st.title("Aider") + self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"]) -def announce(coder): - lines = coder.get_announcements() - lines = " \n".join(lines) - st.info(lines) + def do_cmd_tab(self): + with st.expander("Recommended actions", expanded=True): + with st.popover("Create a git repo to track changes"): + st.write( + "Aider works best when your code is stored in a git repo. \n[See the FAQ for" + " more info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" + ) + st.button("Create git repo", key=random.random(), help="?") + with st.popover("Update your `.gitignore` file"): + st.write("It's best to keep aider's internal files out of your git repo.") + st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") -with st.sidebar: - st.title("Aider") - cmds_tab, settings_tab = st.tabs(["Commands", "Settings"]) - -with cmds_tab: - with st.expander("Recommended actions", expanded=True): - with st.popover("Create a git repo to track changes"): - st.write( - "Aider works best when your code is stored in a git repo. \n[See the FAQ for more" - " info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" + with st.expander("Add to the chat", expanded=True): + st.multiselect( + "Files for the LLM to edit", + search(), + default=["aider/main.py", "aider/io.py"], + help=( + "Only add the files that need to be *edited* for the task you are working on." + " Aider will pull in other code to provide relevant context to the LLM." + ), ) - st.button("Create git repo", key=random.random(), help="?") + with st.popover("Add web page"): + st.markdown("www") + st.text_input("URL?") + with st.popover("Add image"): + st.markdown("Hello World 👋") + st.file_uploader("Image file") + with st.popover("Run shell commands, tests, etc"): + st.markdown( + "Run a shell command and optionally share the output with the LLM. This is a" + " great way to run your program or run tests and have the LLM fix bugs." + ) + st.text_input("Command:") + st.radio( + "Share the command output with the LLM?", + [ + "Review the output and decide whether to share", + ( + "Automatically share the output on non-zero exit code (ie, if any tests" + " fail)" + ), + ], + ) + st.selectbox( + "Recent commands", + [ + "my_app.py --doit", + "my_app.py --cleanup", + ], + ) - with st.popover("Update your `.gitignore` file"): - st.write("It's best to keep aider's internal files out of your git repo.") - st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") + with st.expander("Tokens and costs", expanded=True): + with st.popover("Show token usage"): + st.write("hi") + st.button("Clear chat history") + # st.metric("Cost of last message send & reply", "$0.0019", help="foo") + # st.metric("Cost to send next message", "$0.0013", help="foo") + st.metric("Total cost this session", "$0.22") - with st.expander("Add to the chat", expanded=True): - st.multiselect( - "Files for the LLM to edit", - search(), - default=["aider/main.py", "aider/io.py"], - help=( - "Only add the files that need to be *edited* for the task you are working on. Aider" - " will pull in other code to provide relevant context to the LLM." - ), - ) - with st.popover("Add web page"): - st.markdown("www") - name = st.text_input("URL?") - with st.popover("Add image"): - st.markdown("Hello World 👋") - st.file_uploader("Image file") - with st.popover("Run shell commands, tests, etc"): - st.markdown( - "Run a shell command and optionally share the output with the LLM. This is a great" - " way to run your program or run tests and have the LLM fix bugs." - ) - name = st.text_input("Command:") - st.radio( - "Share the command output with the LLM?", - [ - "Review the output and decide whether to share", - "Automatically share the output on non-zero exit code (ie, if any tests fail)", - ], - ) - st.selectbox( - "Recent commands", - [ - "my_app.py --doit", - "my_app.py --cleanup", - ], - ) + with st.expander("Git", expanded=False): + # st.button("Show last diff") + # st.button("Undo last commit") + st.button("Commit any pending changes") + with st.popover("Run git command"): + st.markdown("## Run git command") + st.text_input("git", value="git ") + st.button("Run") + st.selectbox( + "Recent git commands", + [ + "git checkout -b experiment", + "git stash", + ], + ) - with st.expander("Tokens and costs", expanded=True): - with st.popover("Show token usage"): - st.write("hi") - st.button("Clear chat history") - # st.metric("Cost of last message send & reply", "$0.0019", help="foo") - # st.metric("Cost to send next message", "$0.0013", help="foo") - st.metric("Total cost this session", "$0.22") + self.recent_msgs_empty = st.empty() - with st.expander("Git", expanded=False): - # st.button("Show last diff") - # st.button("Undo last commit") - st.button("Commit any pending changes") - with st.popover("Run git command"): - st.markdown("## Run git command") - name = st.text_input("git", value="git ") - st.button("Run") - st.selectbox( - "Recent git commands", - [ - "git checkout -b experiment", - "git stash", - ], - ) + def do_messages_container(self): + self.messages = st.container() - recent_msgs_empty = st.empty() + # stuff a bunch of vertical whitespace at the top + # to get all the chat text to the bottom + self.messages.container(height=1200, border=False) + with self.messages: + self.announce() + + with self.recent_msgs_empty: + self.old_prompt = recent_msgs() + + for msg in st.session_state.messages: + with self.messages.chat_message(msg["role"]): + st.write(msg["content"]) + cost = random.random() * 0.003 + 0.001 + st.caption(f"${cost:0.4f}") + + def mock_tool_output(self): + messages = """Applied edit to new_program.py""" + # st.info(messages) + + if self.chat_controls: + self.chat_controls.empty() + + self.chat_controls = st.empty() + with self.chat_controls: + container = st.container() + + with container: + # cols = st.columns([0.8,0.2]) + # with cols[0]: + + # with st.expander(messages): + # diff = Path("aider/tmp.diff").read_text() + # st.code(diff, language="diff") + with st.expander( + "Commit `33a242c`: Added sample python that highlights language features " + f" \n{messages}" + ): + # st.info(messages) + st.code(diff, language="diff") + st.button( + "Undo commit `33a242c`", + key=random.random(), + help="wtf?", + ) + + if False: + st.button("Allow edits to `foobar.py`", key=random.random(), help="??") + st.button("Allow creation of new file `some/new/file.js`", key=random.random()) + st.button("Add `baz/foo.py` to the chat", key=random.random()) + + def clear_controls(self): + if self.chat_controls: + self.chat_controls.empty() + + def init_state(self): + if "messages" not in st.session_state: + st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you?"}] + + def __init__(self, coder): + self.coder = coder + self.chat_controls = None + + self.init_state() + + self.do_sidebar() + with self.cmds_tab: + self.do_cmd_tab() + + self.do_messages_container() + + with self.messages: + self.mock_tool_output() + + self.chat() + + def chat(self): + prompt = st.chat_input("Say something", on_submit=self.clear_controls) + # dump(old_prompt, prompt) + + if self.old_prompt: + prompt = self.old_prompt + st.session_state.recent_msgs_num += 1 + with self.recent_msgs_empty: + # do I need the assignment here? + self.old_prompt = recent_msgs() + + if prompt: + self.clear_controls() + + st.session_state.messages.append({"role": "user", "content": prompt}) + with self.messages.chat_message("user"): + st.write(prompt) + + res = self.coder.run(prompt) + st.session_state.messages.append({"role": "assistant", "content": res}) + + with self.messages.chat_message("assistant"): + st.write(res) + cost = random.random() * 0.003 + 0.001 + st.caption(f"${cost:0.4f}") + + with self.messages: + self.mock_tool_output() -# chat_tab, settings_tab = st.tabs(["Chat", "Settings"]) +def gui_main(): + coder = get_coder() + GUI(coder) -messages = st.container() - -# stuff a bunch of vertical whitespace at the top -# to get all the chat text to the bottom -messages.container(height=1200, border=False) -with messages: - announce(coder) - -with recent_msgs_empty: - old_prompt = recent_msgs() - -if "messages" not in st.session_state: - st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you?"}] - - -for msg in st.session_state.messages: - with messages.chat_message(msg["role"]): - st.write(msg["content"]) - cost = random.random() * 0.003 + 0.001 - st.caption(f"${cost:0.4f}") - -with messages: - mock_tool_output() - -# inp_empty = st.empty() - - -def clear_controls(): - if chat_controls: - chat_controls.empty() - - -prompt = st.chat_input("Say something", on_submit=clear_controls) -# dump(old_prompt, prompt) - -if old_prompt: - prompt = old_prompt - st.session_state.recent_msgs_num += 1 - with recent_msgs_empty: - old_prompt = recent_msgs() - -if prompt: - if chat_controls: - chat_controls.empty() - - st.session_state.messages.append({"role": "user", "content": prompt}) - with messages.chat_message("user"): - st.write(prompt) - - res = coder.run(prompt) - st.session_state.messages.append({"role": "assistant", "content": res}) - - with messages.chat_message("assistant"): - st.write(res) - cost = random.random() * 0.003 + 0.001 - st.caption(f"${cost:0.4f}") - - with messages: - mock_tool_output() +if __name__ == "__main__": + status = gui_main() + sys.exit(status) From 882e68c046fff4ccd80c5924b8bb3b9d1fd03157 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 14:32:45 -0700 Subject: [PATCH 17/82] cleanup --- aider/gui.py | 159 +++++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 9358a4830..8095ba9cb 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -15,7 +15,12 @@ if "recent_msgs_num" not in st.session_state: st.session_state.recent_msgs_num = 0 -diff = Path("aider/tmp.diff").read_text() +# st.cache_data +def get_diff(): + return Path("/Users/gauthier/Projects/aider/aider/tmp.diff").read_text() + + +diff = get_diff() def recent_msgs(): @@ -75,83 +80,84 @@ class GUI: self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"]) def do_cmd_tab(self): - with st.expander("Recommended actions", expanded=True): - with st.popover("Create a git repo to track changes"): - st.write( - "Aider works best when your code is stored in a git repo. \n[See the FAQ for" - " more info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" - ) - st.button("Create git repo", key=random.random(), help="?") + with self.cmds_tab: + with st.expander("Recommended actions", expanded=True): + with st.popover("Create a git repo to track changes"): + st.write( + "Aider works best when your code is stored in a git repo. \n[See the FAQ" + " for more info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" + ) + st.button("Create git repo", key=random.random(), help="?") - with st.popover("Update your `.gitignore` file"): - st.write("It's best to keep aider's internal files out of your git repo.") - st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") + with st.popover("Update your `.gitignore` file"): + st.write("It's best to keep aider's internal files out of your git repo.") + st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") - with st.expander("Add to the chat", expanded=True): - st.multiselect( - "Files for the LLM to edit", - search(), - default=["aider/main.py", "aider/io.py"], - help=( - "Only add the files that need to be *edited* for the task you are working on." - " Aider will pull in other code to provide relevant context to the LLM." - ), - ) - with st.popover("Add web page"): - st.markdown("www") - st.text_input("URL?") - with st.popover("Add image"): - st.markdown("Hello World 👋") - st.file_uploader("Image file") - with st.popover("Run shell commands, tests, etc"): - st.markdown( - "Run a shell command and optionally share the output with the LLM. This is a" - " great way to run your program or run tests and have the LLM fix bugs." - ) - st.text_input("Command:") - st.radio( - "Share the command output with the LLM?", - [ - "Review the output and decide whether to share", - ( - "Automatically share the output on non-zero exit code (ie, if any tests" - " fail)" - ), - ], - ) - st.selectbox( - "Recent commands", - [ - "my_app.py --doit", - "my_app.py --cleanup", - ], + with st.expander("Add to the chat", expanded=True): + st.multiselect( + "Files for the LLM to edit", + self.coder.get_all_relative_files(), + default=self.coder.get_inchat_relative_files(), + help=( + "Only add the files that need to be *edited* for the task you are working" + " on. Aider will pull in other code to provide relevant context to the LLM." + ), ) + with st.popover("Add web page"): + st.markdown("www") + st.text_input("URL?") + with st.popover("Add image"): + st.markdown("Hello World 👋") + st.file_uploader("Image file") + with st.popover("Run shell commands, tests, etc"): + st.markdown( + "Run a shell command and optionally share the output with the LLM. This is" + " a great way to run your program or run tests and have the LLM fix bugs." + ) + st.text_input("Command:") + st.radio( + "Share the command output with the LLM?", + [ + "Review the output and decide whether to share", + ( + "Automatically share the output on non-zero exit code (ie, if any" + " tests fail)" + ), + ], + ) + st.selectbox( + "Recent commands", + [ + "my_app.py --doit", + "my_app.py --cleanup", + ], + ) - with st.expander("Tokens and costs", expanded=True): - with st.popover("Show token usage"): - st.write("hi") - st.button("Clear chat history") - # st.metric("Cost of last message send & reply", "$0.0019", help="foo") - # st.metric("Cost to send next message", "$0.0013", help="foo") - st.metric("Total cost this session", "$0.22") + with st.expander("Tokens and costs", expanded=True): + with st.popover("Show token usage"): + st.write("hi") + st.button("Clear chat history") + # st.metric("Cost of last message send & reply", "$0.0019", help="foo") + # st.metric("Cost to send next message", "$0.0013", help="foo") + st.metric("Total cost this session", "$0.22") - with st.expander("Git", expanded=False): - # st.button("Show last diff") - # st.button("Undo last commit") - st.button("Commit any pending changes") - with st.popover("Run git command"): - st.markdown("## Run git command") - st.text_input("git", value="git ") - st.button("Run") - st.selectbox( - "Recent git commands", - [ - "git checkout -b experiment", - "git stash", - ], - ) + with st.expander("Git", expanded=False): + # st.button("Show last diff") + # st.button("Undo last commit") + st.button("Commit any pending changes") + with st.popover("Run git command"): + st.markdown("## Run git command") + st.text_input("git", value="git ") + st.button("Run") + st.selectbox( + "Recent git commands", + [ + "git checkout -b experiment", + "git stash", + ], + ) - self.recent_msgs_empty = st.empty() + self.recent_msgs_empty = st.empty() def do_messages_container(self): self.messages = st.container() @@ -221,19 +227,12 @@ class GUI: self.init_state() self.do_sidebar() - with self.cmds_tab: - self.do_cmd_tab() - + self.do_cmd_tab() self.do_messages_container() - - with self.messages: - self.mock_tool_output() - self.chat() def chat(self): prompt = st.chat_input("Say something", on_submit=self.clear_controls) - # dump(old_prompt, prompt) if self.old_prompt: prompt = self.old_prompt From c3f48317c026ebe6fe2bb69a26bf2e2e64a21605 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 14:41:00 -0700 Subject: [PATCH 18/82] cleanup --- aider/gui.py | 128 +++++++++++++++++++++++++++------------------------ 1 file changed, 68 insertions(+), 60 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 8095ba9cb..d59168213 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -11,9 +11,6 @@ from aider.coders import Coder from aider.dump import dump # noqa: F401 from aider.main import main as cli_main -if "recent_msgs_num" not in st.session_state: - st.session_state.recent_msgs_num = 0 - # st.cache_data def get_diff(): @@ -36,8 +33,9 @@ def recent_msgs(): " tempor adipiscing sit et" ), ] - msgs = 30 * msgs + # msgs = 30 * msgs + dump(st.session_state.recent_msgs_num) return st.selectbox( "N/A", msgs, @@ -74,6 +72,41 @@ class GUI: lines = " \n".join(lines) st.info(lines) + def mock_tool_output(self): + messages = """Applied edit to new_program.py""" + # st.info(messages) + + if self.chat_controls: + self.chat_controls.empty() + + self.chat_controls = st.empty() + with self.chat_controls: + container = st.container() + + with container: + # cols = st.columns([0.8,0.2]) + # with cols[0]: + + # with st.expander(messages): + # diff = Path("aider/tmp.diff").read_text() + # st.code(diff, language="diff") + with st.expander( + "Commit `33a242c`: Added sample python that highlights language features " + f" \n{messages}" + ): + # st.info(messages) + st.code(diff, language="diff") + st.button( + "Undo commit `33a242c`", + key=random.random(), + help="wtf?", + ) + + if False: + st.button("Allow edits to `foobar.py`", key=random.random(), help="??") + st.button("Allow creation of new file `some/new/file.js`", key=random.random()) + st.button("Add `baz/foo.py` to the chat", key=random.random()) + def do_sidebar(self): with st.sidebar: st.title("Aider") @@ -158,6 +191,12 @@ class GUI: ) self.recent_msgs_empty = st.empty() + self.reset_recent_msgs() + + def reset_recent_msgs(self): + self.recent_msgs_empty.empty() + with self.recent_msgs_empty: + self.old_prompt = recent_msgs() def do_messages_container(self): self.messages = st.container() @@ -168,50 +207,12 @@ class GUI: with self.messages: self.announce() - with self.recent_msgs_empty: - self.old_prompt = recent_msgs() - for msg in st.session_state.messages: with self.messages.chat_message(msg["role"]): st.write(msg["content"]) cost = random.random() * 0.003 + 0.001 st.caption(f"${cost:0.4f}") - def mock_tool_output(self): - messages = """Applied edit to new_program.py""" - # st.info(messages) - - if self.chat_controls: - self.chat_controls.empty() - - self.chat_controls = st.empty() - with self.chat_controls: - container = st.container() - - with container: - # cols = st.columns([0.8,0.2]) - # with cols[0]: - - # with st.expander(messages): - # diff = Path("aider/tmp.diff").read_text() - # st.code(diff, language="diff") - with st.expander( - "Commit `33a242c`: Added sample python that highlights language features " - f" \n{messages}" - ): - # st.info(messages) - st.code(diff, language="diff") - st.button( - "Undo commit `33a242c`", - key=random.random(), - help="wtf?", - ) - - if False: - st.button("Allow edits to `foobar.py`", key=random.random(), help="??") - st.button("Allow creation of new file `some/new/file.js`", key=random.random()) - st.button("Add `baz/foo.py` to the chat", key=random.random()) - def clear_controls(self): if self.chat_controls: self.chat_controls.empty() @@ -220,6 +221,9 @@ class GUI: if "messages" not in st.session_state: st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you?"}] + if "recent_msgs_num" not in st.session_state: + st.session_state.recent_msgs_num = 0 + def __init__(self, coder): self.coder = coder self.chat_controls = None @@ -229,35 +233,39 @@ class GUI: self.do_sidebar() self.do_cmd_tab() self.do_messages_container() + + self.prompt = st.chat_input("Say something", on_submit=self.clear_controls) self.chat() def chat(self): - prompt = st.chat_input("Say something", on_submit=self.clear_controls) - if self.old_prompt: prompt = self.old_prompt st.session_state.recent_msgs_num += 1 - with self.recent_msgs_empty: - # do I need the assignment here? - self.old_prompt = recent_msgs() + self.reset_recent_msgs() + elif self.prompt: + prompt = self.prompt + else: + prompt = None - if prompt: - self.clear_controls() + if not prompt: + return - st.session_state.messages.append({"role": "user", "content": prompt}) - with self.messages.chat_message("user"): - st.write(prompt) + self.clear_controls() - res = self.coder.run(prompt) - st.session_state.messages.append({"role": "assistant", "content": res}) + st.session_state.messages.append({"role": "user", "content": prompt}) + with self.messages.chat_message("user"): + st.write(prompt) - with self.messages.chat_message("assistant"): - st.write(res) - cost = random.random() * 0.003 + 0.001 - st.caption(f"${cost:0.4f}") + res = self.coder.run(prompt) + st.session_state.messages.append({"role": "assistant", "content": res}) - with self.messages: - self.mock_tool_output() + with self.messages.chat_message("assistant"): + st.write(res) + cost = random.random() * 0.003 + 0.001 + st.caption(f"${cost:0.4f}") + + with self.messages: + self.mock_tool_output() def gui_main(): From d549d5ecabb21c9bfdb8d55cf9b8dc3fa9f8098f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 14:43:27 -0700 Subject: [PATCH 19/82] cleanup --- aider/gui.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index d59168213..f0121f21a 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -235,21 +235,18 @@ class GUI: self.do_messages_container() self.prompt = st.chat_input("Say something", on_submit=self.clear_controls) - self.chat() - def chat(self): + if self.prompt: + self.chat(self.prompt) + return + if self.old_prompt: prompt = self.old_prompt st.session_state.recent_msgs_num += 1 self.reset_recent_msgs() - elif self.prompt: - prompt = self.prompt - else: - prompt = None - - if not prompt: - return + self.chat(prompt) + def chat(self, prompt): self.clear_controls() st.session_state.messages.append({"role": "user", "content": prompt}) From 98d299703503a641b811255f269e9cd725c521e4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 14:59:02 -0700 Subject: [PATCH 20/82] refac --- aider/gui.py | 167 ++++++++++++++++++++++++++++----------------------- 1 file changed, 93 insertions(+), 74 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index f0121f21a..4c91bcffe 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -35,7 +35,6 @@ def recent_msgs(): ] # msgs = 30 * msgs - dump(st.session_state.recent_msgs_num) return st.selectbox( "N/A", msgs, @@ -114,84 +113,95 @@ class GUI: def do_cmd_tab(self): with self.cmds_tab: - with st.expander("Recommended actions", expanded=True): - with st.popover("Create a git repo to track changes"): - st.write( - "Aider works best when your code is stored in a git repo. \n[See the FAQ" - " for more info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" - ) - st.button("Create git repo", key=random.random(), help="?") + self.do_recommended_actions() + self.do_add_to_chat() + self.do_tokens_and_cost() + self.do_git() + self.do_recent_msgs() - with st.popover("Update your `.gitignore` file"): - st.write("It's best to keep aider's internal files out of your git repo.") - st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") - - with st.expander("Add to the chat", expanded=True): - st.multiselect( - "Files for the LLM to edit", - self.coder.get_all_relative_files(), - default=self.coder.get_inchat_relative_files(), - help=( - "Only add the files that need to be *edited* for the task you are working" - " on. Aider will pull in other code to provide relevant context to the LLM." - ), + def do_recommended_actions(self): + with st.expander("Recommended actions", expanded=True): + with st.popover("Create a git repo to track changes"): + st.write( + "Aider works best when your code is stored in a git repo. \n[See the FAQ" + " for more info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" ) - with st.popover("Add web page"): - st.markdown("www") - st.text_input("URL?") - with st.popover("Add image"): - st.markdown("Hello World 👋") - st.file_uploader("Image file") - with st.popover("Run shell commands, tests, etc"): - st.markdown( - "Run a shell command and optionally share the output with the LLM. This is" - " a great way to run your program or run tests and have the LLM fix bugs." - ) - st.text_input("Command:") - st.radio( - "Share the command output with the LLM?", - [ - "Review the output and decide whether to share", - ( - "Automatically share the output on non-zero exit code (ie, if any" - " tests fail)" - ), - ], - ) - st.selectbox( - "Recent commands", - [ - "my_app.py --doit", - "my_app.py --cleanup", - ], - ) + st.button("Create git repo", key=random.random(), help="?") - with st.expander("Tokens and costs", expanded=True): - with st.popover("Show token usage"): - st.write("hi") - st.button("Clear chat history") - # st.metric("Cost of last message send & reply", "$0.0019", help="foo") - # st.metric("Cost to send next message", "$0.0013", help="foo") - st.metric("Total cost this session", "$0.22") + with st.popover("Update your `.gitignore` file"): + st.write("It's best to keep aider's internal files out of your git repo.") + st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") - with st.expander("Git", expanded=False): - # st.button("Show last diff") - # st.button("Undo last commit") - st.button("Commit any pending changes") - with st.popover("Run git command"): - st.markdown("## Run git command") - st.text_input("git", value="git ") - st.button("Run") - st.selectbox( - "Recent git commands", - [ - "git checkout -b experiment", - "git stash", - ], - ) + def do_add_to_chat(self): + with st.expander("Add to the chat", expanded=True): + st.multiselect( + "Files for the LLM to edit", + self.coder.get_all_relative_files(), + default=self.coder.get_inchat_relative_files(), + help=( + "Only add the files that need to be *edited* for the task you are working" + " on. Aider will pull in other code to provide relevant context to the LLM." + ), + ) + with st.popover("Add web page"): + st.markdown("www") + st.text_input("URL?") + with st.popover("Add image"): + st.markdown("Hello World 👋") + st.file_uploader("Image file") + with st.popover("Run shell commands, tests, etc"): + st.markdown( + "Run a shell command and optionally share the output with the LLM. This is" + " a great way to run your program or run tests and have the LLM fix bugs." + ) + st.text_input("Command:") + st.radio( + "Share the command output with the LLM?", + [ + "Review the output and decide whether to share", + ( + "Automatically share the output on non-zero exit code (ie, if any" + " tests fail)" + ), + ], + ) + st.selectbox( + "Recent commands", + [ + "my_app.py --doit", + "my_app.py --cleanup", + ], + ) - self.recent_msgs_empty = st.empty() - self.reset_recent_msgs() + def do_tokens_and_cost(self): + with st.expander("Tokens and costs", expanded=True): + with st.popover("Show token usage"): + st.write("hi") + st.button("Clear chat history") + # st.metric("Cost of last message send & reply", "$0.0019", help="foo") + # st.metric("Cost to send next message", "$0.0013", help="foo") + st.metric("Total cost this session", "$0.22") + + def do_git(self): + with st.expander("Git", expanded=False): + # st.button("Show last diff") + # st.button("Undo last commit") + st.button("Commit any pending changes") + with st.popover("Run git command"): + st.markdown("## Run git command") + st.text_input("git", value="git ") + st.button("Run") + st.selectbox( + "Recent git commands", + [ + "git checkout -b experiment", + "git stash", + ], + ) + + def do_recent_msgs(self): + self.recent_msgs_empty = st.empty() + self.reset_recent_msgs() def reset_recent_msgs(self): self.recent_msgs_empty.empty() @@ -226,6 +236,13 @@ class GUI: def __init__(self, coder): self.coder = coder + + # Force the coder to cooperate, regardless of cmd line args + self.coder.yield_stream = True + self.coder.stream = True + self.coder.io.yes = True + self.coder.pretty = False + self.chat_controls = None self.init_state() @@ -254,6 +271,8 @@ class GUI: st.write(prompt) res = self.coder.run(prompt) + # self.coder.io.user_input(with_message) + st.session_state.messages.append({"role": "assistant", "content": res}) with self.messages.chat_message("assistant"): From 15e6651e0953ef0664bbea4cdfc428abe82e5d71 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 15:41:47 -0700 Subject: [PATCH 21/82] stream to gui --- aider/coders/base_coder.py | 42 +++++++++++++++++++++++++++++--------- aider/gui.py | 9 +++----- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 3bb929216..4b7d0faf4 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -37,6 +37,11 @@ class ExhaustedContextWindow(Exception): pass +class ReflectMessage(Exception): + def __init__(self, message): + super().__init__(message) + + def wrap_fence(name): return f"<{name}>", f"" @@ -54,6 +59,7 @@ class Coder: last_keyboard_interrupt = None max_apply_update_errors = 3 edit_format = None + yield_stream = False @classmethod def create( @@ -405,6 +411,9 @@ class Coder: return {"role": "user", "content": image_messages} def run(self, with_message=None): + list(self.run_stream(with_message)) + + def run_stream(self, with_message=None): while True: try: if with_message: @@ -414,7 +423,12 @@ class Coder: new_user_message = self.run_loop() while new_user_message: - new_user_message = self.send_new_user_message(new_user_message) + try: + for chunk in self.send_new_user_message(new_user_message): + yield chunk + new_user_message = None + except ReflectMessage as msg: + new_user_message = str(msg) if with_message: return self.partial_response_content @@ -495,7 +509,10 @@ class Coder: self.check_for_file_mentions(inp) - return self.send_new_user_message(inp) + try: + list(self.send_new_user_message(inp)) + except ReflectMessage as msg: + return str(msg) def fmt_system_prompt(self, prompt): prompt = prompt.format(fence=self.fence) @@ -550,7 +567,10 @@ class Coder: exhausted = False interrupted = False try: - interrupted = self.send(messages, functions=self.functions) + for chunk in self.send(messages, functions=self.functions): + yield chunk + except KeyboardInterrupt: + interrupted = True except ExhaustedContextWindow: exhausted = True except openai.BadRequestError as err: @@ -579,18 +599,17 @@ class Coder: else: content = "" + self.io.tool_output() + if interrupted: content += "\n^C KeyboardInterrupt" - - self.io.tool_output() - if interrupted: self.cur_messages += [dict(role="assistant", content=content)] return edited, edit_error = self.apply_updates() if edit_error: self.update_cur_messages(set()) - return edit_error + raise ReflectMessage(edit_error) self.update_cur_messages(edited) @@ -606,7 +625,7 @@ class Coder: add_rel_files_message = self.check_for_file_mentions(content) if add_rel_files_message: - return add_rel_files_message + raise ReflectMessage(add_rel_files_message) def update_cur_messages(self, edited): if self.partial_response_content: @@ -674,7 +693,8 @@ class Coder: self.chat_completion_call_hashes.append(hash_object.hexdigest()) if self.stream: - self.show_send_output_stream(completion) + for chunk in self.show_send_output_stream(completion): + yield chunk else: self.show_send_output(completion) except KeyboardInterrupt: @@ -689,7 +709,8 @@ class Coder: if args: self.io.ai_output(json.dumps(args, indent=4)) - return interrupted + if interrupted: + raise KeyboardInterrupt def show_send_output(self, completion): if self.verbose: @@ -790,6 +811,7 @@ class Coder: elif text: sys.stdout.write(text) sys.stdout.flush() + yield text finally: if mdstream: self.live_incremental_response(mdstream, True) diff --git a/aider/gui.py b/aider/gui.py index 4c91bcffe..788805acb 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -270,16 +270,13 @@ class GUI: with self.messages.chat_message("user"): st.write(prompt) - res = self.coder.run(prompt) - # self.coder.io.user_input(with_message) - - st.session_state.messages.append({"role": "assistant", "content": res}) - with self.messages.chat_message("assistant"): - st.write(res) + res = st.write(self.coder.run_stream(prompt)) cost = random.random() * 0.003 + 0.001 st.caption(f"${cost:0.4f}") + st.session_state.messages.append({"role": "assistant", "content": res}) + with self.messages: self.mock_tool_output() From 86d770516616f06341a1c99caf44ede05c4166ae Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 16:02:45 -0700 Subject: [PATCH 22/82] restructured reflected messages --- aider/coders/base_coder.py | 33 ++++++++++++--------------------- aider/gui.py | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 4b7d0faf4..61ae76734 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -37,11 +37,6 @@ class ExhaustedContextWindow(Exception): pass -class ReflectMessage(Exception): - def __init__(self, message): - super().__init__(message) - - def wrap_fence(name): return f"<{name}>", f"" @@ -410,10 +405,13 @@ class Coder: return {"role": "user", "content": image_messages} - def run(self, with_message=None): - list(self.run_stream(with_message)) + def run_stream(self, user_message): + self.io.user_input(user_message) + self.reflected_message = None + for chunk in self.send_new_user_message(user_message): + yield chunk - def run_stream(self, with_message=None): + def run(self, with_message=None): while True: try: if with_message: @@ -423,12 +421,9 @@ class Coder: new_user_message = self.run_loop() while new_user_message: - try: - for chunk in self.send_new_user_message(new_user_message): - yield chunk - new_user_message = None - except ReflectMessage as msg: - new_user_message = str(msg) + self.reflected_message = None + list(self.send_new_user_message(new_user_message)) + new_user_message = self.reflected_message if with_message: return self.partial_response_content @@ -508,11 +503,7 @@ class Coder: return self.commands.run(inp) self.check_for_file_mentions(inp) - - try: - list(self.send_new_user_message(inp)) - except ReflectMessage as msg: - return str(msg) + return inp def fmt_system_prompt(self, prompt): prompt = prompt.format(fence=self.fence) @@ -609,7 +600,7 @@ class Coder: edited, edit_error = self.apply_updates() if edit_error: self.update_cur_messages(set()) - raise ReflectMessage(edit_error) + self.reflected_message = edit_error self.update_cur_messages(edited) @@ -625,7 +616,7 @@ class Coder: add_rel_files_message = self.check_for_file_mentions(content) if add_rel_files_message: - raise ReflectMessage(add_rel_files_message) + self.reflected_message = add_rel_files_message def update_cur_messages(self, edited): if self.partial_response_content: diff --git a/aider/gui.py b/aider/gui.py index 788805acb..7807a0dff 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -263,6 +263,10 @@ class GUI: self.reset_recent_msgs() self.chat(prompt) + def cost(self): + cost = random.random() * 0.003 + 0.001 + st.caption(f"${cost:0.4f}") + def chat(self, prompt): self.clear_controls() @@ -270,10 +274,14 @@ class GUI: with self.messages.chat_message("user"): st.write(prompt) - with self.messages.chat_message("assistant"): - res = st.write(self.coder.run_stream(prompt)) - cost = random.random() * 0.003 + 0.001 - st.caption(f"${cost:0.4f}") + while prompt: + with self.messages.chat_message("assistant"): + res = st.write(self.coder.run_stream(prompt)) + self.cost() + dump(self.coder.reflected_message) + if self.coder.reflected_message: + self.messages.info(self.coder.reflected_message) + prompt = self.coder.reflected_message st.session_state.messages.append({"role": "assistant", "content": res}) From 89a721b38605ff0beb855c0f5483489b0af6ccf4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 26 Apr 2024 16:16:02 -0700 Subject: [PATCH 23/82] noop --- aider/gui.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 7807a0dff..d801015e9 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -75,8 +75,7 @@ class GUI: messages = """Applied edit to new_program.py""" # st.info(messages) - if self.chat_controls: - self.chat_controls.empty() + self.chat_controls.empty() self.chat_controls = st.empty() with self.chat_controls: @@ -223,9 +222,10 @@ class GUI: cost = random.random() * 0.003 + 0.001 st.caption(f"${cost:0.4f}") + self.chat_controls = st.empty() + def clear_controls(self): - if self.chat_controls: - self.chat_controls.empty() + self.chat_controls.empty() def init_state(self): if "messages" not in st.session_state: @@ -243,8 +243,6 @@ class GUI: self.coder.io.yes = True self.coder.pretty = False - self.chat_controls = None - self.init_state() self.do_sidebar() @@ -278,7 +276,6 @@ class GUI: with self.messages.chat_message("assistant"): res = st.write(self.coder.run_stream(prompt)) self.cost() - dump(self.coder.reflected_message) if self.coder.reflected_message: self.messages.info(self.coder.reflected_message) prompt = self.coder.reflected_message From 4c8d32601c219c058793e2529ec7ff57bbffb029 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 06:37:57 -0700 Subject: [PATCH 24/82] wired up fnames --- aider/gui.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index d801015e9..6e97ae6d8 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -133,15 +133,21 @@ class GUI: def do_add_to_chat(self): with st.expander("Add to the chat", expanded=True): - st.multiselect( + fnames = st.multiselect( "Files for the LLM to edit", - self.coder.get_all_relative_files(), - default=self.coder.get_inchat_relative_files(), + sorted(self.coder.get_all_relative_files()), + default=sorted(self.coder.get_inchat_relative_files()), + placeholder="Files to edit", help=( "Only add the files that need to be *edited* for the task you are working" " on. Aider will pull in other code to provide relevant context to the LLM." ), ) + + for fname in fnames: + if fname not in self.coder.get_inchat_relative_files(): + self.coder.add_rel_fname(fname) + with st.popover("Add web page"): st.markdown("www") st.text_input("URL?") @@ -219,8 +225,7 @@ class GUI: for msg in st.session_state.messages: with self.messages.chat_message(msg["role"]): st.write(msg["content"]) - cost = random.random() * 0.003 + 0.001 - st.caption(f"${cost:0.4f}") + # self.cost() self.chat_controls = st.empty() @@ -274,14 +279,14 @@ class GUI: while prompt: with self.messages.chat_message("assistant"): - res = st.write(self.coder.run_stream(prompt)) - self.cost() + res = st.write_stream(self.coder.run_stream(prompt)) + st.session_state.messages.append({"role": "assistant", "content": res}) + # self.cost() if self.coder.reflected_message: self.messages.info(self.coder.reflected_message) prompt = self.coder.reflected_message - st.session_state.messages.append({"role": "assistant", "content": res}) - + dump(st.session_state.messages) with self.messages: self.mock_tool_output() From 701103a9cca9866463e00c86a1ecd22dcb5c31f1 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 08:09:13 -0700 Subject: [PATCH 25/82] chat works, with commits and reflected messages --- aider/coders/base_coder.py | 5 ++ aider/gui.py | 107 +++++++++++++++++++++---------------- aider/main.py | 2 +- 3 files changed, 67 insertions(+), 47 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 61ae76734..3c10898de 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -45,6 +45,7 @@ class Coder: abs_fnames = None repo = None last_aider_commit_hash = None + aider_edited_files = None last_asked_for_commit_time = 0 repo_map = None functions = None @@ -546,6 +547,8 @@ class Coder: return messages def send_new_user_message(self, inp): + self.aider_edited_files = None + self.cur_messages += [ dict(role="user", content=inp), ] @@ -605,6 +608,7 @@ class Coder: self.update_cur_messages(edited) if edited: + self.aider_edited_files = edited if self.repo and self.auto_commits and not self.dry_run: saved_message = self.auto_commit(edited) elif hasattr(self.gpt_prompts, "files_content_gpt_edits_no_repo"): @@ -1055,6 +1059,7 @@ class Coder: if res: commit_hash, commit_message = res self.last_aider_commit_hash = commit_hash + self.last_aider_commit_message = commit_message return self.gpt_prompts.files_content_gpt_edits.format( hash=commit_hash, diff --git a/aider/gui.py b/aider/gui.py index 6e97ae6d8..7846fbc3e 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -60,9 +60,11 @@ def search(text=None): @st.cache_resource def get_coder(): coder = cli_main(return_coder=True) - if isinstance(coder, Coder): - return coder - raise ValueError() + if not isinstance(coder, Coder): + raise ValueError(coder) + if not coder.repo: + raise ValueError("GUI can currently only be used inside a git repo") + return coder class GUI: @@ -71,39 +73,34 @@ class GUI: lines = " \n".join(lines) st.info(lines) - def mock_tool_output(self): - messages = """Applied edit to new_program.py""" - # st.info(messages) + def show_edit_info(self, edit): + commit_hash = edit.get("commit_hash") + commit_message = edit.get("commit_message") + fnames = edit.get("fnames") + if fnames: + fnames = sorted(fnames) - self.chat_controls.empty() + if not commit_hash and not fnames: + return - self.chat_controls = st.empty() - with self.chat_controls: - container = st.container() + show_undo = False + res = "" + if commit_hash: + res += f"Commit `{commit_hash}`: {commit_message} \n" + if commit_hash == self.coder.last_aider_commit_hash: + show_undo = True - with container: - # cols = st.columns([0.8,0.2]) - # with cols[0]: + if len(fnames) == 1: + res += f"Applied edits to `{fnames[0]}`" + elif len(fnames) > 1: + res += "Applied edits to: \n" + for fname in fnames: + res += f"- `{fname}` \n" - # with st.expander(messages): - # diff = Path("aider/tmp.diff").read_text() - # st.code(diff, language="diff") - with st.expander( - "Commit `33a242c`: Added sample python that highlights language features " - f" \n{messages}" - ): - # st.info(messages) - st.code(diff, language="diff") - st.button( - "Undo commit `33a242c`", - key=random.random(), - help="wtf?", - ) - - if False: - st.button("Allow edits to `foobar.py`", key=random.random(), help="??") - st.button("Allow creation of new file `some/new/file.js`", key=random.random()) - st.button("Add `baz/foo.py` to the chat", key=random.random()) + with st.container(border=True): + st.write(res) + if show_undo: + st.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}") def do_sidebar(self): with st.sidebar: @@ -112,7 +109,7 @@ class GUI: def do_cmd_tab(self): with self.cmds_tab: - self.do_recommended_actions() + # self.do_recommended_actions() self.do_add_to_chat() self.do_tokens_and_cost() self.do_git() @@ -185,7 +182,7 @@ class GUI: st.button("Clear chat history") # st.metric("Cost of last message send & reply", "$0.0019", help="foo") # st.metric("Cost to send next message", "$0.0013", help="foo") - st.metric("Total cost this session", "$0.22") + # st.metric("Total cost this session", "$0.22") def do_git(self): with st.expander("Git", expanded=False): @@ -222,15 +219,21 @@ class GUI: with self.messages: self.announce() - for msg in st.session_state.messages: - with self.messages.chat_message(msg["role"]): - st.write(msg["content"]) - # self.cost() + for msg in st.session_state.messages: + dump(msg) - self.chat_controls = st.empty() + role = msg["role"] - def clear_controls(self): - self.chat_controls.empty() + if role == "edit": + self.show_edit_info(msg) + elif role == "info": + st.info(msg["message"]) + elif role in ("user", "assistant"): + with st.chat_message(role): + st.write(msg["content"]) + # self.cost() + else: + st.dict(msg) def init_state(self): if "messages" not in st.session_state: @@ -239,6 +242,9 @@ class GUI: if "recent_msgs_num" not in st.session_state: st.session_state.recent_msgs_num = 0 + if "last_aider_commit_hash" not in st.session_state: + st.session_state.last_aider_commit_hash = self.coder.last_aider_commit_hash + def __init__(self, coder): self.coder = coder @@ -254,7 +260,7 @@ class GUI: self.do_cmd_tab() self.do_messages_container() - self.prompt = st.chat_input("Say something", on_submit=self.clear_controls) + self.prompt = st.chat_input("Say something") if self.prompt: self.chat(self.prompt) @@ -271,8 +277,6 @@ class GUI: st.caption(f"${cost:0.4f}") def chat(self, prompt): - self.clear_controls() - st.session_state.messages.append({"role": "user", "content": prompt}) with self.messages.chat_message("user"): st.write(prompt) @@ -283,12 +287,23 @@ class GUI: st.session_state.messages.append({"role": "assistant", "content": res}) # self.cost() if self.coder.reflected_message: + info = dict(role="info", message=self.coder.reflected_message) + st.session_state.messages.append(info) self.messages.info(self.coder.reflected_message) prompt = self.coder.reflected_message - dump(st.session_state.messages) with self.messages: - self.mock_tool_output() + edit = dict( + role="edit", + fnames=self.coder.aider_edited_files, + ) + if st.session_state.last_aider_commit_hash != self.coder.last_aider_commit_hash: + edit["commit_hash"] = self.coder.last_aider_commit_hash + edit["commit_message"] = self.coder.last_aider_commit_message + st.session_state.last_aider_commit_hash = self.coder.last_aider_commit_hash + + st.session_state.messages.append(edit) + self.show_edit_info(edit) def gui_main(): diff --git a/aider/main.py b/aider/main.py index ca778382b..eee9eee8f 100644 --- a/aider/main.py +++ b/aider/main.py @@ -513,7 +513,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F io = InputOutput( args.pretty, - args.yes, + args.yes or return_coder, # Force --yes if return_coder args.input_history_file, args.chat_history_file, input=input, From 1b6f0385908ddc5bd589d96127826cf4349d2d63 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 08:13:50 -0700 Subject: [PATCH 26/82] prettier edit message --- aider/gui.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 7846fbc3e..37d89d4b3 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -86,16 +86,17 @@ class GUI: show_undo = False res = "" if commit_hash: + prefix = "aider: " + if commit_message.startswith(prefix): + commit_message = commit_message[len(prefix) :] res += f"Commit `{commit_hash}`: {commit_message} \n" if commit_hash == self.coder.last_aider_commit_hash: show_undo = True - if len(fnames) == 1: - res += f"Applied edits to `{fnames[0]}`" - elif len(fnames) > 1: - res += "Applied edits to: \n" - for fname in fnames: - res += f"- `{fname}` \n" + if fnames: + fnames = [f"`{fname}`" for fname in fnames] + fnames = ", ".join(fnames) + res += f"Applied edits to {fnames}." with st.container(border=True): st.write(res) From c32666b1c59819dc4e88c0d352aa1b2dd15d426a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 11:27:16 -0700 Subject: [PATCH 27/82] hide previous undo button during streaming --- aider/gui.py | 58 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 37d89d4b3..00f6aaa37 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -3,7 +3,6 @@ import os import random import sys -from pathlib import Path import streamlit as st @@ -12,12 +11,10 @@ from aider.dump import dump # noqa: F401 from aider.main import main as cli_main -# st.cache_data -def get_diff(): - return Path("/Users/gauthier/Projects/aider/aider/tmp.diff").read_text() - - -diff = get_diff() +def init_state(key, val): + if key in st.session_state: + return + setattr(st.session_state, key, val) def recent_msgs(): @@ -76,6 +73,7 @@ class GUI: def show_edit_info(self, edit): commit_hash = edit.get("commit_hash") commit_message = edit.get("commit_message") + diff = edit.get("diff") fnames = edit.get("fnames") if fnames: fnames = sorted(fnames) @@ -98,10 +96,24 @@ class GUI: fnames = ", ".join(fnames) res += f"Applied edits to {fnames}." - with st.container(border=True): - st.write(res) - if show_undo: - st.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}") + if diff: + with st.expander(res): + st.code(diff, language="diff") + if show_undo: + self.add_undo(commit_hash) + else: + with st.container(border=True): + st.write(res) + if show_undo: + self.add_undo(commit_hash) + + def add_undo(self, commit_hash): + if self.last_undo_button: + self.last_undo_button.empty() + + self.last_undo_button = st.empty() + with self.last_undo_button: + st.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}") def do_sidebar(self): with st.sidebar: @@ -236,18 +248,15 @@ class GUI: else: st.dict(msg) - def init_state(self): - if "messages" not in st.session_state: - st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you?"}] - - if "recent_msgs_num" not in st.session_state: - st.session_state.recent_msgs_num = 0 - - if "last_aider_commit_hash" not in st.session_state: - st.session_state.last_aider_commit_hash = self.coder.last_aider_commit_hash + def initialize_state(self): + messages = [{"role": "assistant", "content": "How can I help you?"}] + init_state("messages", messages) + init_state("recent_msgs_num", 0) + init_state("last_aider_commit_hash", self.coder.last_aider_commit_hash) def __init__(self, coder): self.coder = coder + self.last_undo_button = None # Force the coder to cooperate, regardless of cmd line args self.coder.yield_stream = True @@ -255,7 +264,7 @@ class GUI: self.coder.io.yes = True self.coder.pretty = False - self.init_state() + self.initialize_state() self.do_sidebar() self.do_cmd_tab() @@ -301,6 +310,13 @@ class GUI: if st.session_state.last_aider_commit_hash != self.coder.last_aider_commit_hash: edit["commit_hash"] = self.coder.last_aider_commit_hash edit["commit_message"] = self.coder.last_aider_commit_message + commits = f"{self.coder.last_aider_commit_hash}~1" + diff = self.coder.repo.diff_commits( + self.coder.pretty, + commits, + self.coder.last_aider_commit_hash, + ) + edit["diff"] = diff st.session_state.last_aider_commit_hash = self.coder.last_aider_commit_hash st.session_state.messages.append(edit) From 296a336d22213dbd0b052ec4fb17521c4e48aa42 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 11:47:18 -0700 Subject: [PATCH 28/82] yield from --- aider/coders/base_coder.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 3c10898de..6a3e18670 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -409,8 +409,7 @@ class Coder: def run_stream(self, user_message): self.io.user_input(user_message) self.reflected_message = None - for chunk in self.send_new_user_message(user_message): - yield chunk + yield from self.send_new_user_message(user_message) def run(self, with_message=None): while True: @@ -561,8 +560,7 @@ class Coder: exhausted = False interrupted = False try: - for chunk in self.send(messages, functions=self.functions): - yield chunk + yield from self.send(messages, functions=self.functions) except KeyboardInterrupt: interrupted = True except ExhaustedContextWindow: @@ -688,8 +686,7 @@ class Coder: self.chat_completion_call_hashes.append(hash_object.hexdigest()) if self.stream: - for chunk in self.show_send_output_stream(completion): - yield chunk + yield from self.show_send_output_stream(completion) else: self.show_send_output(completion) except KeyboardInterrupt: From 1c43722221e15e6031b31136f78fcb5e3600e15d Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 13:23:40 -0700 Subject: [PATCH 29/82] disable buttons when a prompt is pending --- aider/gui.py | 58 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 00f6aaa37..cef9ed8f9 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -11,7 +11,7 @@ from aider.dump import dump # noqa: F401 from aider.main import main as cli_main -def init_state(key, val): +def init_state(key, val=None): if key in st.session_state: return setattr(st.session_state, key, val) @@ -54,8 +54,16 @@ def search(text=None): return results +@st.cache_data +def get_nominal_cached_data(): + print("get_nominal_cached_data") + + @st.cache_resource def get_coder(): + print("get_coder") + get_nominal_cached_data() + coder = cli_main(return_coder=True) if not isinstance(coder, Coder): raise ValueError(coder) @@ -113,7 +121,7 @@ class GUI: self.last_undo_button = st.empty() with self.last_undo_button: - st.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}") + self.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}") def do_sidebar(self): with st.sidebar: @@ -135,11 +143,11 @@ class GUI: "Aider works best when your code is stored in a git repo. \n[See the FAQ" " for more info](https://aider.chat/docs/faq.html#how-does-aider-use-git)" ) - st.button("Create git repo", key=random.random(), help="?") + self.button("Create git repo", key=random.random(), help="?") with st.popover("Update your `.gitignore` file"): st.write("It's best to keep aider's internal files out of your git repo.") - st.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") + self.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") def do_add_to_chat(self): with st.expander("Add to the chat", expanded=True): @@ -148,6 +156,7 @@ class GUI: sorted(self.coder.get_all_relative_files()), default=sorted(self.coder.get_inchat_relative_files()), placeholder="Files to edit", + disabled=self.prompt_pending(), help=( "Only add the files that need to be *edited* for the task you are working" " on. Aider will pull in other code to provide relevant context to the LLM." @@ -163,7 +172,7 @@ class GUI: st.text_input("URL?") with st.popover("Add image"): st.markdown("Hello World 👋") - st.file_uploader("Image file") + st.file_uploader("Image file", disabled=self.prompt_pending()) with st.popover("Run shell commands, tests, etc"): st.markdown( "Run a shell command and optionally share the output with the LLM. This is" @@ -186,13 +195,14 @@ class GUI: "my_app.py --doit", "my_app.py --cleanup", ], + disabled=self.prompt_pending(), ) def do_tokens_and_cost(self): with st.expander("Tokens and costs", expanded=True): with st.popover("Show token usage"): st.write("hi") - st.button("Clear chat history") + self.button("Clear chat history") # st.metric("Cost of last message send & reply", "$0.0019", help="foo") # st.metric("Cost to send next message", "$0.0013", help="foo") # st.metric("Total cost this session", "$0.22") @@ -201,17 +211,18 @@ class GUI: with st.expander("Git", expanded=False): # st.button("Show last diff") # st.button("Undo last commit") - st.button("Commit any pending changes") + self.button("Commit any pending changes") with st.popover("Run git command"): st.markdown("## Run git command") st.text_input("git", value="git ") - st.button("Run") + self.button("Run") st.selectbox( "Recent git commands", [ "git checkout -b experiment", "git stash", ], + disabled=self.prompt_pending(), ) def do_recent_msgs(self): @@ -253,6 +264,12 @@ class GUI: init_state("messages", messages) init_state("recent_msgs_num", 0) init_state("last_aider_commit_hash", self.coder.last_aider_commit_hash) + init_state("prompt") + + def button(self, args, **kwargs): + "Create a button, disabled if prompt pending" + kwargs["disabled"] = self.prompt_pending() + return st.button(args, **kwargs) def __init__(self, coder): self.coder = coder @@ -270,10 +287,13 @@ class GUI: self.do_cmd_tab() self.do_messages_container() - self.prompt = st.chat_input("Say something") + prompt = st.chat_input("Say something") - if self.prompt: - self.chat(self.prompt) + if self.prompt_pending(): + self.process_chat() + + if prompt: + self.chat(prompt) return if self.old_prompt: @@ -281,16 +301,29 @@ class GUI: st.session_state.recent_msgs_num += 1 self.reset_recent_msgs() self.chat(prompt) + return + + def prompt_pending(self): + return st.session_state.prompt is not None def cost(self): cost = random.random() * 0.003 + 0.001 st.caption(f"${cost:0.4f}") def chat(self, prompt): + st.session_state.prompt = prompt + st.session_state.messages.append({"role": "user", "content": prompt}) with self.messages.chat_message("user"): st.write(prompt) + # re-render the UI for the prompt_pending state + st.experimental_rerun() + + def process_chat(self): + prompt = st.session_state.prompt + st.session_state.prompt = None + while prompt: with self.messages.chat_message("assistant"): res = st.write_stream(self.coder.run_stream(prompt)) @@ -322,6 +355,9 @@ class GUI: st.session_state.messages.append(edit) self.show_edit_info(edit) + # re-render the UI for the non-prompt_pending state + st.experimental_rerun() + def gui_main(): coder = get_coder() From b4d1a0a2a270439d427e9497e4be6a89bc12f698 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 13:40:00 -0700 Subject: [PATCH 30/82] state as a resource --- aider/gui.py | 118 +++++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index cef9ed8f9..bb862ffba 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -11,37 +11,6 @@ from aider.dump import dump # noqa: F401 from aider.main import main as cli_main -def init_state(key, val=None): - if key in st.session_state: - return - setattr(st.session_state, key, val) - - -def recent_msgs(): - msgs = [ - "write a python program that shows off some python features", - "write a tsx program that shows off some language features", - "refactor the Frobulator.simplify method to be a stand alone function", - "lorem ipsum dolor", - "lorem adipiscing adipiscing et dolore sit elit aliqua dolore ut incididunt", - ( - "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed labore" - " aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim aliqua enim" - " tempor adipiscing sit et" - ), - ] - # msgs = 30 * msgs - - return st.selectbox( - "N/A", - msgs, - placeholder="Resend recent chat message", - label_visibility="collapsed", - index=None, - key=f"recent_msgs_{st.session_state.recent_msgs_num}", - ) - - def search(text=None): results = [] for root, _, files in os.walk("aider"): @@ -54,16 +23,24 @@ def search(text=None): return results -@st.cache_data -def get_nominal_cached_data(): - print("get_nominal_cached_data") +# Keep state as a resource, which survives browser reloads (since Coder does too) +class State: + keys = set() + + def init(self, key, val=None): + if key in self.keys: + return + self.keys.add(key) + setattr(self, key, val) + + +@st.cache_resource +def get_state(): + return State() @st.cache_resource def get_coder(): - print("get_coder") - get_nominal_cached_data() - coder = cli_main(return_coder=True) if not isinstance(coder, Coder): raise ValueError(coder) @@ -232,7 +209,7 @@ class GUI: def reset_recent_msgs(self): self.recent_msgs_empty.empty() with self.recent_msgs_empty: - self.old_prompt = recent_msgs() + self.old_prompt = self.recent_msgs() def do_messages_container(self): self.messages = st.container() @@ -243,7 +220,7 @@ class GUI: with self.messages: self.announce() - for msg in st.session_state.messages: + for msg in self.state.messages: dump(msg) role = msg["role"] @@ -261,18 +238,22 @@ class GUI: def initialize_state(self): messages = [{"role": "assistant", "content": "How can I help you?"}] - init_state("messages", messages) - init_state("recent_msgs_num", 0) - init_state("last_aider_commit_hash", self.coder.last_aider_commit_hash) - init_state("prompt") + self.state.init("messages", messages) + self.state.init("recent_msgs_num", 0) + self.state.init("last_aider_commit_hash", self.coder.last_aider_commit_hash) + self.state.init("prompt") + + dump(self.state.messages) def button(self, args, **kwargs): "Create a button, disabled if prompt pending" kwargs["disabled"] = self.prompt_pending() return st.button(args, **kwargs) - def __init__(self, coder): + def __init__(self, coder, state): self.coder = coder + self.state = state + self.last_undo_button = None # Force the coder to cooperate, regardless of cmd line args @@ -298,22 +279,22 @@ class GUI: if self.old_prompt: prompt = self.old_prompt - st.session_state.recent_msgs_num += 1 + self.state.recent_msgs_num += 1 self.reset_recent_msgs() self.chat(prompt) return def prompt_pending(self): - return st.session_state.prompt is not None + return self.state.prompt is not None def cost(self): cost = random.random() * 0.003 + 0.001 st.caption(f"${cost:0.4f}") def chat(self, prompt): - st.session_state.prompt = prompt + self.state.prompt = prompt - st.session_state.messages.append({"role": "user", "content": prompt}) + self.state.messages.append({"role": "user", "content": prompt}) with self.messages.chat_message("user"): st.write(prompt) @@ -321,17 +302,17 @@ class GUI: st.experimental_rerun() def process_chat(self): - prompt = st.session_state.prompt - st.session_state.prompt = None + prompt = self.state.prompt + self.state.prompt = None while prompt: with self.messages.chat_message("assistant"): res = st.write_stream(self.coder.run_stream(prompt)) - st.session_state.messages.append({"role": "assistant", "content": res}) + self.state.messages.append({"role": "assistant", "content": res}) # self.cost() if self.coder.reflected_message: info = dict(role="info", message=self.coder.reflected_message) - st.session_state.messages.append(info) + self.state.messages.append(info) self.messages.info(self.coder.reflected_message) prompt = self.coder.reflected_message @@ -340,7 +321,7 @@ class GUI: role="edit", fnames=self.coder.aider_edited_files, ) - if st.session_state.last_aider_commit_hash != self.coder.last_aider_commit_hash: + if self.state.last_aider_commit_hash != self.coder.last_aider_commit_hash: edit["commit_hash"] = self.coder.last_aider_commit_hash edit["commit_message"] = self.coder.last_aider_commit_message commits = f"{self.coder.last_aider_commit_hash}~1" @@ -350,18 +331,43 @@ class GUI: self.coder.last_aider_commit_hash, ) edit["diff"] = diff - st.session_state.last_aider_commit_hash = self.coder.last_aider_commit_hash + self.state.last_aider_commit_hash = self.coder.last_aider_commit_hash - st.session_state.messages.append(edit) + self.state.messages.append(edit) self.show_edit_info(edit) # re-render the UI for the non-prompt_pending state st.experimental_rerun() + def recent_msgs(self): + msgs = [ + "write a python program that shows off some python features", + "write a tsx program that shows off some language features", + "refactor the Frobulator.simplify method to be a stand alone function", + "lorem ipsum dolor", + "lorem adipiscing adipiscing et dolore sit elit aliqua dolore ut incididunt", + ( + "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed" + " labore aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim" + " aliqua enim tempor adipiscing sit et" + ), + ] + # msgs = 30 * msgs + + return st.selectbox( + "N/A", + msgs, + placeholder="Resend recent chat message", + label_visibility="collapsed", + index=None, + key=f"recent_msgs_{self.state.recent_msgs_num}", + ) + def gui_main(): coder = get_coder() - GUI(coder) + state = get_state() + GUI(coder, state) if __name__ == "__main__": From 0eb69716bcb0861b9d7269c985f9ed143de675b3 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 13:53:12 -0700 Subject: [PATCH 31/82] simplify recent_msgs logic --- aider/gui.py | 86 ++++++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index bb862ffba..1b908c65a 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -10,6 +10,19 @@ from aider.coders import Coder from aider.dump import dump # noqa: F401 from aider.main import main as cli_main +MESSAGES = [ + "write a python program that shows off some python features", + "write a tsx program that shows off some language features", + "refactor the Frobulator.simplify method to be a stand alone function", + "lorem ipsum dolor", + "lorem adipiscing adipiscing et dolore sit elit aliqua dolore ut incididunt", + ( + "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed" + " labore aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim" + " aliqua enim tempor adipiscing sit et" + ), +] + def search(text=None): results = [] @@ -203,13 +216,23 @@ class GUI: ) def do_recent_msgs(self): - self.recent_msgs_empty = st.empty() - self.reset_recent_msgs() + if not self.recent_msgs_empty: + self.recent_msgs_empty = st.empty() + + if self.prompt_pending(): + self.recent_msgs_empty.empty() + self.state.recent_msgs_num += 1 - def reset_recent_msgs(self): - self.recent_msgs_empty.empty() with self.recent_msgs_empty: - self.old_prompt = self.recent_msgs() + self.old_prompt = st.selectbox( + "Resend recent chat message", + MESSAGES, + placeholder="Choose a recent chat message", + # label_visibility="collapsed", + index=None, + key=f"recent_msgs_{self.state.recent_msgs_num}", + disabled=self.prompt_pending(), + ) def do_messages_container(self): self.messages = st.container() @@ -239,8 +262,8 @@ class GUI: def initialize_state(self): messages = [{"role": "assistant", "content": "How can I help you?"}] self.state.init("messages", messages) - self.state.init("recent_msgs_num", 0) self.state.init("last_aider_commit_hash", self.coder.last_aider_commit_hash) + self.state.init("recent_msgs_num", 0) self.state.init("prompt") dump(self.state.messages) @@ -255,6 +278,7 @@ class GUI: self.state = state self.last_undo_button = None + self.recent_msgs_empty = None # Force the coder to cooperate, regardless of cmd line args self.coder.yield_stream = True @@ -273,25 +297,10 @@ class GUI: if self.prompt_pending(): self.process_chat() - if prompt: - self.chat(prompt) + prompt = prompt if prompt else self.old_prompt + if not prompt: return - if self.old_prompt: - prompt = self.old_prompt - self.state.recent_msgs_num += 1 - self.reset_recent_msgs() - self.chat(prompt) - return - - def prompt_pending(self): - return self.state.prompt is not None - - def cost(self): - cost = random.random() * 0.003 + 0.001 - st.caption(f"${cost:0.4f}") - - def chat(self, prompt): self.state.prompt = prompt self.state.messages.append({"role": "user", "content": prompt}) @@ -301,6 +310,13 @@ class GUI: # re-render the UI for the prompt_pending state st.experimental_rerun() + def prompt_pending(self): + return self.state.prompt is not None + + def cost(self): + cost = random.random() * 0.003 + 0.001 + st.caption(f"${cost:0.4f}") + def process_chat(self): prompt = self.state.prompt self.state.prompt = None @@ -339,30 +355,6 @@ class GUI: # re-render the UI for the non-prompt_pending state st.experimental_rerun() - def recent_msgs(self): - msgs = [ - "write a python program that shows off some python features", - "write a tsx program that shows off some language features", - "refactor the Frobulator.simplify method to be a stand alone function", - "lorem ipsum dolor", - "lorem adipiscing adipiscing et dolore sit elit aliqua dolore ut incididunt", - ( - "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed" - " labore aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim" - " aliqua enim tempor adipiscing sit et" - ), - ] - # msgs = 30 * msgs - - return st.selectbox( - "N/A", - msgs, - placeholder="Resend recent chat message", - label_visibility="collapsed", - index=None, - key=f"recent_msgs_{self.state.recent_msgs_num}", - ) - def gui_main(): coder = get_coder() From e71cf03f3602356d8007eb3854a3d9e506f91fad Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 13:55:44 -0700 Subject: [PATCH 32/82] self.coder.io.add_to_input_history --- aider/gui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aider/gui.py b/aider/gui.py index 1b908c65a..ac16231d7 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -303,6 +303,8 @@ class GUI: self.state.prompt = prompt + self.coder.io.add_to_input_history(prompt) + self.state.messages.append({"role": "user", "content": prompt}) with self.messages.chat_message("user"): st.write(prompt) From 594a6bd93448b027ec874d40ddc6b9fb61bea5c5 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 14:08:21 -0700 Subject: [PATCH 33/82] updated mocks of Coder.send() to fix tests --- tests/test_coder.py | 24 ++++++++++++++++-------- tests/test_editblock.py | 6 ++++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/test_coder.py b/tests/test_coder.py index 35cf0093c..2cd3bb9ba 100644 --- a/tests/test_coder.py +++ b/tests/test_coder.py @@ -225,8 +225,9 @@ class TestCoder(unittest.TestCase): def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() + return [] - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") @@ -251,8 +252,9 @@ class TestCoder(unittest.TestCase): def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() + return [] - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") @@ -281,8 +283,9 @@ class TestCoder(unittest.TestCase): def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() + return [] - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") @@ -310,8 +313,9 @@ class TestCoder(unittest.TestCase): def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() + return [] - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") @@ -373,8 +377,9 @@ new """ coder.partial_response_function_call = dict() + return [] - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send coder.repo.get_commit_message = MagicMock() coder.repo.get_commit_message.return_value = "commit message" @@ -424,13 +429,14 @@ TWO """ coder.partial_response_function_call = dict() + return [] def mock_get_commit_message(diffs, context): self.assertNotIn("one", diffs) self.assertNotIn("ONE", diffs) return "commit message" - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message) coder.run(with_message="hi") @@ -476,6 +482,7 @@ three """ coder.partial_response_function_call = dict() + return [] saved_diffs = [] @@ -484,7 +491,7 @@ three return "commit message" coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message) - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send coder.run(with_message="hi") @@ -553,6 +560,7 @@ two """ coder.partial_response_function_call = dict() + return [] saved_diffs = [] @@ -561,7 +569,7 @@ two return "commit message" coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message) - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send coder.run(with_message="hi") diff --git a/tests/test_editblock.py b/tests/test_editblock.py index a0c36f072..0c1143232 100644 --- a/tests/test_editblock.py +++ b/tests/test_editblock.py @@ -312,8 +312,9 @@ new """ coder.partial_response_function_call = dict() + return [] - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") @@ -354,8 +355,9 @@ new """ coder.partial_response_function_call = dict() + return [] - coder.send = MagicMock(side_effect=mock_send) + coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") From ddb83606082d2e775fd68a728cc8e17ae07b2f6d Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 14:13:23 -0700 Subject: [PATCH 34/82] Improved add/drop files --- aider/gui.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index ac16231d7..42e6dfa36 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -156,6 +156,12 @@ class GUI: for fname in fnames: if fname not in self.coder.get_inchat_relative_files(): self.coder.add_rel_fname(fname) + self.messages.info(f"Added {fname} to the chat") + for fname in self.coder.get_inchat_relative_files(): + if fname not in fnames: + abs_fname = self.coder.abs_root_path(fname) + self.coder.abs_fnames.remove(abs_fname) + self.messages.info(f"Removed {fname} from the chat") with st.popover("Add web page"): st.markdown("www") @@ -239,7 +245,7 @@ class GUI: # stuff a bunch of vertical whitespace at the top # to get all the chat text to the bottom - self.messages.container(height=1200, border=False) + self.messages.container(height=300, border=False) with self.messages: self.announce() @@ -288,9 +294,9 @@ class GUI: self.initialize_state() + self.do_messages_container() self.do_sidebar() self.do_cmd_tab() - self.do_messages_container() prompt = st.chat_input("Say something") From fb8e604fbe8d77a877ed469c3d3d1b37e5f4022c Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 14:29:00 -0700 Subject: [PATCH 35/82] Clear chat history --- aider/gui.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 42e6dfa36..f099641ec 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -43,8 +43,10 @@ class State: def init(self, key, val=None): if key in self.keys: return + self.keys.add(key) setattr(self, key, val) + return True @st.cache_resource @@ -198,7 +200,11 @@ class GUI: with st.expander("Tokens and costs", expanded=True): with st.popover("Show token usage"): st.write("hi") - self.button("Clear chat history") + if self.button("Clear chat history"): + self.coder.done_messages = [] + self.coder.cur_messages = [] + self.info("Cleared chat history") + # st.metric("Cost of last message send & reply", "$0.0019", help="foo") # st.metric("Cost to send next message", "$0.0013", help="foo") # st.metric("Total cost this session", "$0.22") @@ -335,9 +341,7 @@ class GUI: self.state.messages.append({"role": "assistant", "content": res}) # self.cost() if self.coder.reflected_message: - info = dict(role="info", message=self.coder.reflected_message) - self.state.messages.append(info) - self.messages.info(self.coder.reflected_message) + self.info(self.coder.reflected_message) prompt = self.coder.reflected_message with self.messages: @@ -363,6 +367,11 @@ class GUI: # re-render the UI for the non-prompt_pending state st.experimental_rerun() + def info(self, message): + info = dict(role="info", message=message) + self.state.messages.append(info) + self.messages.info(message) + def gui_main(): coder = get_coder() From 0b853204e4c3af061421fda74e898776e8685a96 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 14:36:47 -0700 Subject: [PATCH 36/82] Improved chat history handling by clearing history, storing input history, and appending new prompts to the input history. --- aider/gui.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index f099641ec..40856e24e 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -203,7 +203,7 @@ class GUI: if self.button("Clear chat history"): self.coder.done_messages = [] self.coder.cur_messages = [] - self.info("Cleared chat history") + self.info("Cleared chat history. Now the LLM can't see anything before this line.") # st.metric("Cost of last message send & reply", "$0.0019", help="foo") # st.metric("Cost to send next message", "$0.0013", help="foo") @@ -238,7 +238,7 @@ class GUI: with self.recent_msgs_empty: self.old_prompt = st.selectbox( "Resend recent chat message", - MESSAGES, + self.state.input_history, placeholder="Choose a recent chat message", # label_visibility="collapsed", index=None, @@ -278,7 +278,9 @@ class GUI: self.state.init("recent_msgs_num", 0) self.state.init("prompt") - dump(self.state.messages) + if "input_history" not in self.state.keys: + self.state.input_history = list(self.coder.io.get_input_history()) + # TODO: make self.state.input_history entries unique, but keep their order def button(self, args, **kwargs): "Create a button, disabled if prompt pending" @@ -316,6 +318,7 @@ class GUI: self.state.prompt = prompt self.coder.io.add_to_input_history(prompt) + self.state.input_history.append(prompt) self.state.messages.append({"role": "user", "content": prompt}) with self.messages.chat_message("user"): From 640d321b3e2307fd9479ada072bd14e945ac55d0 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 14:36:49 -0700 Subject: [PATCH 37/82] aider: Removed duplicates from the input history while preserving their order. # Aider chat conversation: USER: do the todo ASSISTANT: To address the TODO comment in the code about making the `self.state.input_history` entries unique while keeping their order, we can modify the `initialize_state()` method in `aider/gui.py`. Here are the changes: aider/gui.py ```python <<<<<<< SEARCH if "input_history" not in self.state.keys: self.state.input_history = list(self.coder.io.get_input_history()) # TODO: make self.state.input_history entries unique, but keep their order ======= if "input_history" not in self.state.keys: input_history = list(self.coder.io.get_input_history()) seen = set() self.state.input_history = [x for x in input_history if not (x in seen or seen.add(x))] >>>>>>> REPLACE ``` Explanation: 1. We first store the input history from `self.coder.io.get_input_history()` in a variable called `input_history`. 2. We create an empty set called `seen` to keep track of entries we've already processed. 3. We use a list comprehension to build `self.state.input_history`. For each item `x` in `input_history`: - If `x` is not in `seen`, it's a new entry so we add it to `self.state.input_history` and add it to `seen`. - If `x` is already in `seen`, it's a duplicate so we skip it. This preserves the original order of the input history while removing any duplicate entries. --- aider/gui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 40856e24e..148495e75 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -279,8 +279,9 @@ class GUI: self.state.init("prompt") if "input_history" not in self.state.keys: - self.state.input_history = list(self.coder.io.get_input_history()) - # TODO: make self.state.input_history entries unique, but keep their order + input_history = list(self.coder.io.get_input_history()) + seen = set() + self.state.input_history = [x for x in input_history if not (x in seen or seen.add(x))] def button(self, args, **kwargs): "Create a button, disabled if prompt pending" From 6e5e195ea857d922c3979aec923242a4541b6b32 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 14:40:44 -0700 Subject: [PATCH 38/82] resend recent msg --- aider/gui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aider/gui.py b/aider/gui.py index 148495e75..3701ce3c7 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -281,7 +281,8 @@ class GUI: if "input_history" not in self.state.keys: input_history = list(self.coder.io.get_input_history()) seen = set() - self.state.input_history = [x for x in input_history if not (x in seen or seen.add(x))] + input_history = [x for x in input_history if not (x in seen or seen.add(x))] + self.state.input_history = input_history def button(self, args, **kwargs): "Create a button, disabled if prompt pending" From dcb6100ce9f85be918a14932313bc15938a7cb95 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 15:28:08 -0700 Subject: [PATCH 39/82] Add web page --- aider/commands.py | 4 ++- aider/gui.py | 64 +++++++++++++++++++++++++++++++++++------------ aider/scrape.py | 4 +-- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index 36e5abaea..03b9fe4a1 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -42,7 +42,9 @@ class Commands: if content: self.io.tool_output(content) - self.scraper.show_playwright_instructions() + instructions = self.scraper.get_playwright_instructions() + if instructions: + self.io.tool_error(instructions) content = f"{url}:\n\n" + content diff --git a/aider/gui.py b/aider/gui.py index 3701ce3c7..f2f08225f 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -9,19 +9,7 @@ import streamlit as st from aider.coders import Coder from aider.dump import dump # noqa: F401 from aider.main import main as cli_main - -MESSAGES = [ - "write a python program that shows off some python features", - "write a tsx program that shows off some language features", - "refactor the Frobulator.simplify method to be a stand alone function", - "lorem ipsum dolor", - "lorem adipiscing adipiscing et dolore sit elit aliqua dolore ut incididunt", - ( - "sed magna consectetur et quis do magna labore ad elit et elit ad eiusmod sed" - " labore aliqua eiusmod enim ad nostrud\n\namet consectetur magna tempor do enim" - " aliqua enim tempor adipiscing sit et" - ), -] +from aider.scrape import Scraper def search(text=None): @@ -166,8 +154,8 @@ class GUI: self.messages.info(f"Removed {fname} from the chat") with st.popover("Add web page"): - st.markdown("www") - st.text_input("URL?") + self.do_web() + with st.popover("Add image"): st.markdown("Hello World 👋") st.file_uploader("Image file", disabled=self.prompt_pending()) @@ -276,7 +264,9 @@ class GUI: self.state.init("messages", messages) self.state.init("last_aider_commit_hash", self.coder.last_aider_commit_hash) self.state.init("recent_msgs_num", 0) + self.state.init("web_content_num", 0) self.state.init("prompt") + self.state.init("scraper") if "input_history" not in self.state.keys: input_history = list(self.coder.io.get_input_history()) @@ -294,7 +284,9 @@ class GUI: self.state = state self.last_undo_button = None + self.recent_msgs_empty = None + self.web_content_empty = None # Force the coder to cooperate, regardless of cmd line args self.coder.yield_stream = True @@ -313,7 +305,10 @@ class GUI: if self.prompt_pending(): self.process_chat() - prompt = prompt if prompt else self.old_prompt + for prompt in [prompt, self.old_prompt, self.web_content]: + if prompt: + break + if not prompt: return @@ -377,6 +372,43 @@ class GUI: self.state.messages.append(info) self.messages.info(message) + def do_web(self): + st.markdown("Add the text content of a web page to the chat") + + if not self.web_content_empty: + self.web_content_empty = st.empty() + + if self.prompt_pending(): + self.web_content_empty.empty() + self.state.web_content_num += 1 + + with self.web_content_empty: + self.web_content = st.text_input( + "URL", + placeholder="https://...", + key=f"web_content_{self.state.web_content_num}", + ) + + if not self.web_content: + return + + url = self.web_content + + if not self.state.scraper: + self.scraper = Scraper(print_error=self.info) + + instructions = self.scraper.get_playwright_instructions() + if instructions: + self.info(instructions) + + content = self.scraper.scrape(url) or "" + if content.strip(): + content = f"{url}:\n\n" + content + self.web_content = content + else: + self.info(f"No web content found for `{url}`.") + self.web_content = None + def gui_main(): coder = get_coder() diff --git a/aider/scrape.py b/aider/scrape.py index 64e557f94..21c888df2 100755 --- a/aider/scrape.py +++ b/aider/scrape.py @@ -66,14 +66,14 @@ class Scraper: except Exception: self.playwright_available = False - def show_playwright_instructions(self): + def get_playwright_instructions(self): if self.playwright_available in (True, None): return if self.playwright_instructions_shown: return self.playwright_instructions_shown = True - self.print_error(PLAYWRIGHT_INFO) + return PLAYWRIGHT_INFO def scrape_with_httpx(self, url): headers = {"User-Agent": f"Mozilla./5.0 ({aider_user_agent})"} From 355e072de8d1e1232eefd5894268f443ec952573 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 15:44:10 -0700 Subject: [PATCH 40/82] refactored self.prompt --- aider/gui.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index f2f08225f..2d59fdea5 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -53,6 +53,11 @@ def get_coder(): class GUI: + prompt = None + last_undo_button = None + recent_msgs_empty = None + web_content_empty = None + def announce(self): lines = self.coder.get_announcements() lines = " \n".join(lines) @@ -233,6 +238,8 @@ class GUI: key=f"recent_msgs_{self.state.recent_msgs_num}", disabled=self.prompt_pending(), ) + if self.old_prompt: + self.prompt = self.old_prompt def do_messages_container(self): self.messages = st.container() @@ -252,6 +259,10 @@ class GUI: self.show_edit_info(msg) elif role == "info": st.info(msg["message"]) + # elif role == "text": + # text = msg["message"] + # with st.expander(text[0]): + # st.text(text) elif role in ("user", "assistant"): with st.chat_message(role): st.write(msg["content"]) @@ -283,11 +294,6 @@ class GUI: self.coder = coder self.state = state - self.last_undo_button = None - - self.recent_msgs_empty = None - self.web_content_empty = None - # Force the coder to cooperate, regardless of cmd line args self.coder.yield_stream = True self.coder.stream = True @@ -300,26 +306,24 @@ class GUI: self.do_sidebar() self.do_cmd_tab() - prompt = st.chat_input("Say something") + user_inp = st.chat_input("Say something") + if user_inp: + self.prompt = user_inp if self.prompt_pending(): self.process_chat() - for prompt in [prompt, self.old_prompt, self.web_content]: - if prompt: - break - - if not prompt: + if not self.prompt: return - self.state.prompt = prompt + self.state.prompt = self.prompt - self.coder.io.add_to_input_history(prompt) - self.state.input_history.append(prompt) + self.coder.io.add_to_input_history(self.prompt) + self.state.input_history.append(self.prompt) - self.state.messages.append({"role": "user", "content": prompt}) + self.state.messages.append({"role": "user", "content": self.prompt}) with self.messages.chat_message("user"): - st.write(prompt) + st.write(self.prompt) # re-render the UI for the prompt_pending state st.experimental_rerun() @@ -404,11 +408,14 @@ class GUI: content = self.scraper.scrape(url) or "" if content.strip(): content = f"{url}:\n\n" + content - self.web_content = content + self.prompt = content else: self.info(f"No web content found for `{url}`.") self.web_content = None + # with self.messages.expander(content.splitlines()[0]): + # st.text(content) + def gui_main(): coder = get_coder() From f822fd055451401e60cd3ae49ef1767b90771cd5 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 15:52:14 -0700 Subject: [PATCH 41/82] put web content into a text box --- aider/gui.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 2d59fdea5..b7b877d9e 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -54,6 +54,7 @@ def get_coder(): class GUI: prompt = None + prompt_as = "user" last_undo_button = None recent_msgs_empty = None web_content_empty = None @@ -259,10 +260,10 @@ class GUI: self.show_edit_info(msg) elif role == "info": st.info(msg["message"]) - # elif role == "text": - # text = msg["message"] - # with st.expander(text[0]): - # st.text(text) + elif role == "text": + text = msg["content"] + with self.messages.expander(text.splitlines()[0]): + st.text(text) elif role in ("user", "assistant"): with st.chat_message(role): st.write(msg["content"]) @@ -321,9 +322,13 @@ class GUI: self.coder.io.add_to_input_history(self.prompt) self.state.input_history.append(self.prompt) - self.state.messages.append({"role": "user", "content": self.prompt}) - with self.messages.chat_message("user"): - st.write(self.prompt) + self.state.messages.append({"role": self.prompt_as, "content": self.prompt}) + if self.prompt_as == "user": + with self.messages.chat_message("user"): + st.write(self.prompt) + elif self.prompt_as == "text": + with self.messages.expander(self.prompt.splitlines()[0]): + st.text(self.prompt) # re-render the UI for the prompt_pending state st.experimental_rerun() @@ -407,17 +412,17 @@ class GUI: content = self.scraper.scrape(url) or "" if content.strip(): - content = f"{url}:\n\n" + content + content = f"{url}\n\n" + content self.prompt = content + self.prompt_as = "text" else: self.info(f"No web content found for `{url}`.") self.web_content = None - # with self.messages.expander(content.splitlines()[0]): - # st.text(content) - def gui_main(): + st.set_page_config(layout="wide") + coder = get_coder() state = get_state() GUI(coder, state) From d0d53262287c5840394a2daf649c42bb4a125ce8 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 16:00:10 -0700 Subject: [PATCH 42/82] roughed in undo --- aider/gui.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index b7b877d9e..2ccf14347 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -107,7 +107,8 @@ class GUI: self.last_undo_button = st.empty() with self.last_undo_button: - self.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}") + if self.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}"): + self.do_undo(commit_hash) def do_sidebar(self): with st.sidebar: @@ -162,9 +163,10 @@ class GUI: with st.popover("Add web page"): self.do_web() - with st.popover("Add image"): - st.markdown("Hello World 👋") - st.file_uploader("Image file", disabled=self.prompt_pending()) + # with st.popover("Add image"): + # st.markdown("Hello World 👋") + # st.file_uploader("Image file", disabled=self.prompt_pending()) + with st.popover("Run shell commands, tests, etc"): st.markdown( "Run a shell command and optionally share the output with the LLM. This is" @@ -252,8 +254,6 @@ class GUI: self.announce() for msg in self.state.messages: - dump(msg) - role = msg["role"] if role == "edit": @@ -322,7 +322,8 @@ class GUI: self.coder.io.add_to_input_history(self.prompt) self.state.input_history.append(self.prompt) - self.state.messages.append({"role": self.prompt_as, "content": self.prompt}) + if self.prompt_as: + self.state.messages.append({"role": self.prompt_as, "content": self.prompt}) if self.prompt_as == "user": with self.messages.chat_message("user"): st.write(self.prompt) @@ -419,6 +420,9 @@ class GUI: self.info(f"No web content found for `{url}`.") self.web_content = None + def do_undo(self, commit_hash): + pass + def gui_main(): st.set_page_config(layout="wide") From ce5abf8c79984a8a033e4f9ccdafedea909f5fbb Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 27 Apr 2024 16:47:07 -0700 Subject: [PATCH 43/82] working undo --- aider/commands.py | 4 +-- aider/gui.py | 90 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index 03b9fe4a1..40581b0f9 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -271,9 +271,7 @@ class Commands: self.coder.repo.repo.git.reset("--soft", "HEAD~1") self.io.tool_output( - f"{last_commit.message.strip()}\n" - f"The above commit {self.coder.last_aider_commit_hash} " - "was reset and removed from git.\n" + f"Commit `{self.coder.last_aider_commit_hash}` was reset and removed from git.\n" ) if self.coder.main_model.send_undo_reply: diff --git a/aider/gui.py b/aider/gui.py index 2ccf14347..3e967d14f 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -8,10 +8,26 @@ import streamlit as st from aider.coders import Coder from aider.dump import dump # noqa: F401 +from aider.io import InputOutput from aider.main import main as cli_main from aider.scrape import Scraper +class CaptureIO(InputOutput): + lines = [] + + def tool_output(self, msg): + self.lines.append(msg) + + def tool_error(self, msg): + self.lines.append(msg) + + def get_captured_lines(self): + lines = self.lines + self.lines = [] + return lines + + def search(text=None): results = [] for root, _, files in os.walk("aider"): @@ -49,13 +65,23 @@ def get_coder(): raise ValueError(coder) if not coder.repo: raise ValueError("GUI can currently only be used inside a git repo") + + io = CaptureIO( + pretty=False, + yes=True, + dry_run=coder.io.dry_run, + encoding=coder.io.encoding, + ) + # coder.io = io # this breaks the input_history + coder.commands.io = io + return coder class GUI: prompt = None prompt_as = "user" - last_undo_button = None + last_undo_empty = None recent_msgs_empty = None web_content_empty = None @@ -102,13 +128,15 @@ class GUI: self.add_undo(commit_hash) def add_undo(self, commit_hash): - if self.last_undo_button: - self.last_undo_button.empty() + if self.last_undo_empty: + self.last_undo_empty.empty() - self.last_undo_button = st.empty() - with self.last_undo_button: - if self.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}"): - self.do_undo(commit_hash) + self.last_undo_empty = st.empty() + undone = self.state.last_undone_commit_hash == commit_hash + if not undone: + with self.last_undo_empty: + if self.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}"): + self.do_undo(commit_hash) def do_sidebar(self): with st.sidebar: @@ -275,6 +303,7 @@ class GUI: messages = [{"role": "assistant", "content": "How can I help you?"}] self.state.init("messages", messages) self.state.init("last_aider_commit_hash", self.coder.last_aider_commit_hash) + self.state.init("last_undone_commit_hash") self.state.init("recent_msgs_num", 0) self.state.init("web_content_num", 0) self.state.init("prompt") @@ -285,10 +314,15 @@ class GUI: seen = set() input_history = [x for x in input_history if not (x in seen or seen.add(x))] self.state.input_history = input_history + self.state.keys.add("input_history") def button(self, args, **kwargs): "Create a button, disabled if prompt pending" - kwargs["disabled"] = self.prompt_pending() + + # Force everything to be disabled if there is a prompt pending + if self.prompt_pending(): + kwargs["disabled"] = True + return st.button(args, **kwargs) def __init__(self, coder, state): @@ -298,7 +332,6 @@ class GUI: # Force the coder to cooperate, regardless of cmd line args self.coder.yield_stream = True self.coder.stream = True - self.coder.io.yes = True self.coder.pretty = False self.initialize_state() @@ -319,7 +352,9 @@ class GUI: self.state.prompt = self.prompt - self.coder.io.add_to_input_history(self.prompt) + if self.prompt_as == "user": + self.coder.io.add_to_input_history(self.prompt) + self.state.input_history.append(self.prompt) if self.prompt_as: @@ -332,7 +367,7 @@ class GUI: st.text(self.prompt) # re-render the UI for the prompt_pending state - st.experimental_rerun() + st.rerun() def prompt_pending(self): return self.state.prompt is not None @@ -375,12 +410,15 @@ class GUI: self.show_edit_info(edit) # re-render the UI for the non-prompt_pending state - st.experimental_rerun() + st.rerun() - def info(self, message): + def info(self, message, echo=True): info = dict(role="info", message=message) self.state.messages.append(info) - self.messages.info(message) + + # We will render the tail of the messages array after this call + if echo: + self.messages.info(message) def do_web(self): st.markdown("Add the text content of a web page to the chat") @@ -421,7 +459,29 @@ class GUI: self.web_content = None def do_undo(self, commit_hash): - pass + self.last_undo_empty.empty() + + if ( + self.state.last_aider_commit_hash != commit_hash + or self.coder.last_aider_commit_hash != commit_hash + ): + self.info(f"Commit `{commit_hash}` is not the latest commit.") + return + + self.coder.commands.io.get_captured_lines() + reply = self.coder.commands.cmd_undo(None) + lines = self.coder.commands.io.get_captured_lines() + + lines = "\n".join(lines) + lines = lines.splitlines() + lines = " \n".join(lines) + self.info(lines, echo=False) + + self.state.last_undone_commit_hash = commit_hash + + if reply: + self.prompt_as = None + self.prompt = reply def gui_main(): From bfd9398f286d19ec88ae80a91931b01f6e58096b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 07:12:46 -0700 Subject: [PATCH 44/82] persist file add/drop messages --- aider/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 3e967d14f..409a28866 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -181,12 +181,12 @@ class GUI: for fname in fnames: if fname not in self.coder.get_inchat_relative_files(): self.coder.add_rel_fname(fname) - self.messages.info(f"Added {fname} to the chat") + self.info(f"Added {fname} to the chat") for fname in self.coder.get_inchat_relative_files(): if fname not in fnames: abs_fname = self.coder.abs_root_path(fname) self.coder.abs_fnames.remove(abs_fname) - self.messages.info(f"Removed {fname} from the chat") + self.info(f"Removed {fname} from the chat") with st.popover("Add web page"): self.do_web() From 0f8efa37145caab1c41407a5324109b48bc50240 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 07:15:48 -0700 Subject: [PATCH 45/82] Coder.drop_rel_fname --- aider/coders/base_coder.py | 6 ++++++ aider/gui.py | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 6a3e18670..988516b2c 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -270,6 +270,12 @@ class Coder: self.abs_fnames.add(self.abs_root_path(rel_fname)) self.check_added_files() + def drop_rel_fname(self, fname): + abs_fname = self.abs_root_path(fname) + if abs_fname in self.abs_fnames: + self.abs_fnames.remove(abs_fname) + return True + def abs_root_path(self, path): res = Path(self.root) / path return utils.safe_abs_path(res) diff --git a/aider/gui.py b/aider/gui.py index 409a28866..77c114ff4 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -184,8 +184,7 @@ class GUI: self.info(f"Added {fname} to the chat") for fname in self.coder.get_inchat_relative_files(): if fname not in fnames: - abs_fname = self.coder.abs_root_path(fname) - self.coder.abs_fnames.remove(abs_fname) + self.coder.drop_rel_fname(fname) self.info(f"Removed {fname} from the chat") with st.popover("Add web page"): From c0fec1d6ac593286acb1b256af43cc7d7e6e649f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 07:24:46 -0700 Subject: [PATCH 46/82] persist the announce lines --- aider/gui.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 77c114ff4..f350b81ab 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -88,7 +88,7 @@ class GUI: def announce(self): lines = self.coder.get_announcements() lines = " \n".join(lines) - st.info(lines) + return lines def show_edit_info(self, edit): commit_hash = edit.get("commit_hash") @@ -278,8 +278,6 @@ class GUI: # to get all the chat text to the bottom self.messages.container(height=300, border=False) with self.messages: - self.announce() - for msg in self.state.messages: role = msg["role"] @@ -289,7 +287,8 @@ class GUI: st.info(msg["message"]) elif role == "text": text = msg["content"] - with self.messages.expander(text.splitlines()[0]): + line = text.splitlines()[0] + with self.messages.expander(line): st.text(text) elif role in ("user", "assistant"): with st.chat_message(role): @@ -299,7 +298,11 @@ class GUI: st.dict(msg) def initialize_state(self): - messages = [{"role": "assistant", "content": "How can I help you?"}] + messages = [ + dict(role="text", content=self.announce()), + dict(role="assistant", content="How can I help you?"), + ] + self.state.init("messages", messages) self.state.init("last_aider_commit_hash", self.coder.last_aider_commit_hash) self.state.init("last_undone_commit_hash") @@ -362,7 +365,9 @@ class GUI: with self.messages.chat_message("user"): st.write(self.prompt) elif self.prompt_as == "text": - with self.messages.expander(self.prompt.splitlines()[0]): + line = self.prompt.splitlines()[0] + line += "??" + with self.messages.expander(line): st.text(self.prompt) # re-render the UI for the prompt_pending state From 81b0415a90800cca9e3cd6c01b756d06fc530e69 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 07:27:38 -0700 Subject: [PATCH 47/82] info dict messages->content --- aider/gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index f350b81ab..e0265101d 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -284,7 +284,7 @@ class GUI: if role == "edit": self.show_edit_info(msg) elif role == "info": - st.info(msg["message"]) + st.info(msg["content"]) elif role == "text": text = msg["content"] line = text.splitlines()[0] @@ -299,7 +299,7 @@ class GUI: def initialize_state(self): messages = [ - dict(role="text", content=self.announce()), + dict(role="info", content=self.announce()), dict(role="assistant", content="How can I help you?"), ] @@ -417,7 +417,7 @@ class GUI: st.rerun() def info(self, message, echo=True): - info = dict(role="info", message=message) + info = dict(role="info", content=message) self.state.messages.append(info) # We will render the tail of the messages array after this call From 4efa97201fa8e5c7b05838f57c0497456177add5 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 11:52:38 -0700 Subject: [PATCH 48/82] copy --- docs/llms.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/llms.md b/docs/llms.md index dad55f427..b85dc3ede 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -21,6 +21,10 @@ Cohere also offers free API access to their [Command-R+ model](#cohere), which works with aider as a *very basic* coding assistant. +## Local models + +Aider can work also with local models, for example using [Ollama](#ollama). + ## Other models Aider supports connecting to almost any LLM, @@ -39,6 +43,7 @@ So you should expect that models which are less capable than GPT-3.5 may struggl - [Azure](#azure) - [OpenRouter](#openrouter) - [OpenAI compatible APIs](#openai-compatible-apis) +- [Ollama](#ollama) - [Other LLMs](#other-llms) - [Model warnings](#model-warnings) - [Editing format](#editing-format) @@ -214,6 +219,26 @@ See the [model warnings](#model-warnings) section for information on warnings which will occur when working with models that aider is not familiar with. +## Ollama + +Aider can connect to local Ollama models. + +``` +# Start your ollama server +ollama serve + +# In another terminal window: +export OLLAMA_API_BASE=http://127.0.0.1:11434 +aider --model ollama/ +``` + +The Llama3 70B model works well with aider. +Give aider the `--edit-format diff` switch if you're working with it. + +Also see the [model warnings](#model-warnings) +section for information on warnings which will occur +when working with models that aider is not familiar with. + ## Other LLMs Aider uses the [litellm](https://docs.litellm.ai/docs/providers) package From ca6349069c90709163c50d625740599892cbf2a9 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 12:38:25 -0700 Subject: [PATCH 49/82] noop --- aider/coders/base_coder.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 988516b2c..ae68d0a50 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -439,6 +439,23 @@ class Coder: except EOFError: return + def run_loop(self): + inp = self.io.get_input( + self.root, + self.get_inchat_relative_files(), + self.get_addable_relative_files(), + self.commands, + ) + + if not inp: + return + + if self.commands.is_command(inp): + return self.commands.run(inp) + + self.check_for_file_mentions(inp) + return inp + def keyboard_interrupt(self): now = time.time() @@ -494,23 +511,6 @@ class Coder: ] self.cur_messages = [] - def run_loop(self): - inp = self.io.get_input( - self.root, - self.get_inchat_relative_files(), - self.get_addable_relative_files(), - self.commands, - ) - - if not inp: - return - - if self.commands.is_command(inp): - return self.commands.run(inp) - - self.check_for_file_mentions(inp) - return inp - def fmt_system_prompt(self, prompt): prompt = prompt.format(fence=self.fence) return prompt From 221ddbb9ea6fd43eeb3849a6f6601c148066920d Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 12:43:22 -0700 Subject: [PATCH 50/82] cleanup unimplemented UI --- aider/gui.py | 137 +++++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index e0265101d..1344b8736 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -141,14 +141,12 @@ class GUI: def do_sidebar(self): with st.sidebar: st.title("Aider") - self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"]) + # self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"]) def do_cmd_tab(self): - with self.cmds_tab: + with st.sidebar: # self.do_recommended_actions() self.do_add_to_chat() - self.do_tokens_and_cost() - self.do_git() self.do_recent_msgs() def do_recommended_actions(self): @@ -166,71 +164,82 @@ class GUI: def do_add_to_chat(self): with st.expander("Add to the chat", expanded=True): - fnames = st.multiselect( - "Files for the LLM to edit", - sorted(self.coder.get_all_relative_files()), - default=sorted(self.coder.get_inchat_relative_files()), - placeholder="Files to edit", - disabled=self.prompt_pending(), - help=( - "Only add the files that need to be *edited* for the task you are working" - " on. Aider will pull in other code to provide relevant context to the LLM." - ), + self.do_add_files() + self.do_add_web_page() + self.do_clear_chat_history() + + def do_add_files(self): + fnames = st.multiselect( + "Files for the LLM to edit", + sorted(self.coder.get_all_relative_files()), + default=sorted(self.coder.get_inchat_relative_files()), + placeholder="Files to edit", + disabled=self.prompt_pending(), + help=( + "Only add the files that need to be *edited* for the task you are working" + " on. Aider will pull in other code to provide relevant context to the LLM." + ), + ) + + for fname in fnames: + if fname not in self.coder.get_inchat_relative_files(): + self.coder.add_rel_fname(fname) + self.info(f"Added {fname} to the chat") + for fname in self.coder.get_inchat_relative_files(): + if fname not in fnames: + self.coder.drop_rel_fname(fname) + self.info(f"Removed {fname} from the chat") + + def do_add_web_page(self): + with st.popover("Add web page"): + self.do_web() + + def do_add_image(self): + with st.popover("Add image"): + st.markdown("Hello World 👋") + st.file_uploader("Image file", disabled=self.prompt_pending()) + + def do_run_shell(self): + with st.popover("Run shell commands, tests, etc"): + st.markdown( + "Run a shell command and optionally share the output with the LLM. This is" + " a great way to run your program or run tests and have the LLM fix bugs." + ) + st.text_input("Command:") + st.radio( + "Share the command output with the LLM?", + [ + "Review the output and decide whether to share", + "Automatically share the output on non-zero exit code (ie, if any tests fail)", + ], + ) + st.selectbox( + "Recent commands", + [ + "my_app.py --doit", + "my_app.py --cleanup", + ], + disabled=self.prompt_pending(), ) - - for fname in fnames: - if fname not in self.coder.get_inchat_relative_files(): - self.coder.add_rel_fname(fname) - self.info(f"Added {fname} to the chat") - for fname in self.coder.get_inchat_relative_files(): - if fname not in fnames: - self.coder.drop_rel_fname(fname) - self.info(f"Removed {fname} from the chat") - - with st.popover("Add web page"): - self.do_web() - - # with st.popover("Add image"): - # st.markdown("Hello World 👋") - # st.file_uploader("Image file", disabled=self.prompt_pending()) - - with st.popover("Run shell commands, tests, etc"): - st.markdown( - "Run a shell command and optionally share the output with the LLM. This is" - " a great way to run your program or run tests and have the LLM fix bugs." - ) - st.text_input("Command:") - st.radio( - "Share the command output with the LLM?", - [ - "Review the output and decide whether to share", - ( - "Automatically share the output on non-zero exit code (ie, if any" - " tests fail)" - ), - ], - ) - st.selectbox( - "Recent commands", - [ - "my_app.py --doit", - "my_app.py --cleanup", - ], - disabled=self.prompt_pending(), - ) def do_tokens_and_cost(self): with st.expander("Tokens and costs", expanded=True): - with st.popover("Show token usage"): - st.write("hi") - if self.button("Clear chat history"): - self.coder.done_messages = [] - self.coder.cur_messages = [] - self.info("Cleared chat history. Now the LLM can't see anything before this line.") + pass - # st.metric("Cost of last message send & reply", "$0.0019", help="foo") - # st.metric("Cost to send next message", "$0.0013", help="foo") - # st.metric("Total cost this session", "$0.22") + def do_show_token_usage(self): + with st.popover("Show token usage"): + st.write("hi") + + def do_clear_chat_history(self): + if self.button("Clear chat history"): + self.coder.done_messages = [] + self.coder.cur_messages = [] + self.info("Cleared chat history. Now the LLM can't see anything before this line.") + + def do_show_metrics(self): + st.metric("Cost of last message send & reply", "$0.0019", help="foo") + st.metric("Cost to send next message", "$0.0013", help="foo") + st.metric("Total cost this session", "$0.22") def do_git(self): with st.expander("Git", expanded=False): From 5a01ba70a689215ee298b2989917b18463f26287 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 13:15:14 -0700 Subject: [PATCH 51/82] refactored parser, args --- aider/gui.py | 13 ++++---- aider/main.py | 82 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 1344b8736..07a2eb483 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -143,6 +143,9 @@ class GUI: st.title("Aider") # self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"]) + def do_settings_tab(self): + pass + def do_cmd_tab(self): with st.sidebar: # self.do_recommended_actions() @@ -336,9 +339,9 @@ class GUI: return st.button(args, **kwargs) - def __init__(self, coder, state): - self.coder = coder - self.state = state + def __init__(self): + self.coder = get_coder() + self.state = get_state() # Force the coder to cooperate, regardless of cmd line args self.coder.yield_stream = True @@ -500,9 +503,7 @@ class GUI: def gui_main(): st.set_page_config(layout="wide") - coder = get_coder() - state = get_state() - GUI(coder, state) + GUI() if __name__ == "__main__": diff --git a/aider/main.py b/aider/main.py index eee9eee8f..b4e314169 100644 --- a/aider/main.py +++ b/aider/main.py @@ -122,25 +122,7 @@ def check_gitignore(git_root, io, ask=True): io.tool_output(f"Added {pat} to .gitignore") -def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): - if argv is None: - argv = sys.argv[1:] - - if force_git_root: - git_root = force_git_root - else: - git_root = get_git_root() - - conf_fname = Path(".aider.conf.yml") - - default_config_files = [conf_fname.resolve()] # CWD - if git_root: - git_conf = Path(git_root) / conf_fname # git root - if git_conf not in default_config_files: - default_config_files.append(git_conf) - default_config_files.append(Path.home() / conf_fname) # homedir - default_config_files = list(map(str, default_config_files)) - +def get_parser(default_config_files, git_root): parser = configargparse.ArgumentParser( description="aider is GPT powered coding in your terminal", add_config_file_help=True, @@ -497,6 +479,49 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F ), ) + return parser + + +def format_settings(parser, args): + show = scrub_sensitive_info(args, parser.format_values()) + show += "\n" + show += "Option settings:\n" + for arg, val in sorted(vars(args).items()): + if val: + val = scrub_sensitive_info(args, str(val)) + show += f" - {arg}: {val}\n" + return show + + +def scrub_sensitive_info(args, text): + # Replace sensitive information with placeholder + if text and args.openai_api_key: + text = text.replace(args.openai_api_key, "***") + if text and args.anthropic_api_key: + text = text.replace(args.anthropic_api_key, "***") + return text + + +def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): + if argv is None: + argv = sys.argv[1:] + + if force_git_root: + git_root = force_git_root + else: + git_root = get_git_root() + + conf_fname = Path(".aider.conf.yml") + + default_config_files = [conf_fname.resolve()] # CWD + if git_root: + git_conf = Path(git_root) / conf_fname # git root + if git_conf not in default_config_files: + default_config_files.append(git_conf) + default_config_files.append(Path.home() / conf_fname) # homedir + default_config_files = list(map(str, default_config_files)) + + parser = get_parser(default_config_files, git_root) args = parser.parse_args(argv) if args.dark_mode: @@ -582,24 +607,13 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if args.gitignore: check_gitignore(git_root, io) - def scrub_sensitive_info(text): - # Replace sensitive information with placeholder - if text and args.openai_api_key: - text = text.replace(args.openai_api_key, "***") - if text and args.anthropic_api_key: - text = text.replace(args.anthropic_api_key, "***") - return text - if args.verbose: - show = scrub_sensitive_info(parser.format_values()) + show = format_settings(parser, args) io.tool_output(show) - io.tool_output("Option settings:") - for arg, val in sorted(vars(args).items()): - if val: - val = scrub_sensitive_info(str(val)) - io.tool_output(f" - {arg}: {val}") - io.tool_output(*map(scrub_sensitive_info, sys.argv), log_only=True) + cmd_line = " ".join(sys.argv) + cmd_line = scrub_sensitive_info(args, cmd_line) + io.tool_output(cmd_line, log_only=True) if args.anthropic_api_key: os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key From 15a50a6afc093308d86c2f5e12754c0cf919db01 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 13:19:48 -0700 Subject: [PATCH 52/82] more refactor --- aider/main.py | 365 +------------------------------------------------- 1 file changed, 2 insertions(+), 363 deletions(-) diff --git a/aider/main.py b/aider/main.py index b4e314169..6dda7a1cf 100644 --- a/aider/main.py +++ b/aider/main.py @@ -1,14 +1,13 @@ -import argparse import configparser import os import sys from pathlib import Path -import configargparse import git import litellm -from aider import __version__, models +from aider import models +from aider.args import get_parser from aider.coders import Coder from aider.io import InputOutput from aider.repo import GitRepo @@ -122,366 +121,6 @@ def check_gitignore(git_root, io, ask=True): io.tool_output(f"Added {pat} to .gitignore") -def get_parser(default_config_files, git_root): - parser = configargparse.ArgumentParser( - description="aider is GPT powered coding in your terminal", - add_config_file_help=True, - default_config_files=default_config_files, - config_file_parser_class=configargparse.YAMLConfigFileParser, - auto_env_var_prefix="AIDER_", - ) - - ########## - core_group = parser.add_argument_group("Main") - core_group.add_argument( - "files", - metavar="FILE", - nargs="*", - help="the directory of a git repo, or a list of files to edit with GPT (optional)", - ) - core_group.add_argument( - "--openai-api-key", - metavar="OPENAI_API_KEY", - env_var="OPENAI_API_KEY", - help="Specify the OpenAI API key", - ) - core_group.add_argument( - "--anthropic-api-key", - metavar="ANTHROPIC_API_KEY", - env_var="ANTHROPIC_API_KEY", - help="Specify the OpenAI API key", - ) - default_model = models.DEFAULT_MODEL_NAME - core_group.add_argument( - "--model", - metavar="MODEL", - default=default_model, - help=f"Specify the model to use for the main chat (default: {default_model})", - ) - core_group.add_argument( - "--models", - metavar="MODEL", - help="List known models which match the (partial) MODEL name", - ) - opus_model = "claude-3-opus-20240229" - core_group.add_argument( - "--opus", - action="store_const", - dest="model", - const=opus_model, - help=f"Use {opus_model} model for the main chat", - ) - sonnet_model = "claude-3-sonnet-20240229" - core_group.add_argument( - "--sonnet", - action="store_const", - dest="model", - const=sonnet_model, - help=f"Use {sonnet_model} model for the main chat", - ) - gpt_4_model = "gpt-4-0613" - core_group.add_argument( - "--4", - "-4", - action="store_const", - dest="model", - const=gpt_4_model, - help=f"Use {gpt_4_model} model for the main chat", - ) - gpt_4_turbo_model = "gpt-4-turbo" - core_group.add_argument( - "--4-turbo-vision", - action="store_const", - dest="model", - const=gpt_4_turbo_model, - help=f"Use {gpt_4_turbo_model} model for the main chat", - ) - gpt_3_model_name = "gpt-3.5-turbo" - core_group.add_argument( - "--35turbo", - "--35-turbo", - "--3", - "-3", - action="store_const", - dest="model", - const=gpt_3_model_name, - help=f"Use {gpt_3_model_name} model for the main chat", - ) - core_group.add_argument( - "--voice-language", - metavar="VOICE_LANGUAGE", - default="en", - help="Specify the language for voice using ISO 639-1 code (default: auto)", - ) - - ########## - model_group = parser.add_argument_group("Advanced Model Settings") - model_group.add_argument( - "--openai-api-base", - metavar="OPENAI_API_BASE", - env_var="OPENAI_API_BASE", - help="Specify the api base url", - ) - model_group.add_argument( - "--openai-api-type", - metavar="OPENAI_API_TYPE", - env_var="OPENAI_API_TYPE", - help="Specify the api_type", - ) - model_group.add_argument( - "--openai-api-version", - metavar="OPENAI_API_VERSION", - env_var="OPENAI_API_VERSION", - help="Specify the api_version", - ) - model_group.add_argument( - "--openai-api-deployment-id", - metavar="OPENAI_API_DEPLOYMENT_ID", - env_var="OPENAI_API_DEPLOYMENT_ID", - help="Specify the deployment_id", - ) - model_group.add_argument( - "--openai-organization-id", - metavar="OPENAI_ORGANIZATION_ID", - env_var="OPENAI_ORGANIZATION_ID", - help="Specify the OpenAI organization ID", - ) - model_group.add_argument( - "--edit-format", - metavar="EDIT_FORMAT", - default=None, - help="Specify what edit format GPT should use (default depends on model)", - ) - core_group.add_argument( - "--weak-model", - metavar="WEAK_MODEL", - default=None, - help=( - "Specify the model to use for commit messages and chat history summarization (default" - " depends on --model)" - ), - ) - model_group.add_argument( - "--show-model-warnings", - action=argparse.BooleanOptionalAction, - default=True, - help="Only work with models that have meta-data available (default: True)", - ) - model_group.add_argument( - "--map-tokens", - type=int, - default=1024, - help="Max number of tokens to use for repo map, use 0 to disable (default: 1024)", - ) - - ########## - history_group = parser.add_argument_group("History Files") - default_input_history_file = ( - os.path.join(git_root, ".aider.input.history") if git_root else ".aider.input.history" - ) - default_chat_history_file = ( - os.path.join(git_root, ".aider.chat.history.md") if git_root else ".aider.chat.history.md" - ) - history_group.add_argument( - "--input-history-file", - metavar="INPUT_HISTORY_FILE", - default=default_input_history_file, - help=f"Specify the chat input history file (default: {default_input_history_file})", - ) - history_group.add_argument( - "--chat-history-file", - metavar="CHAT_HISTORY_FILE", - default=default_chat_history_file, - help=f"Specify the chat history file (default: {default_chat_history_file})", - ) - - ########## - output_group = parser.add_argument_group("Output Settings") - output_group.add_argument( - "--dark-mode", - action="store_true", - help="Use colors suitable for a dark terminal background (default: False)", - default=False, - ) - output_group.add_argument( - "--light-mode", - action="store_true", - help="Use colors suitable for a light terminal background (default: False)", - default=False, - ) - output_group.add_argument( - "--pretty", - action=argparse.BooleanOptionalAction, - default=True, - help="Enable/disable pretty, colorized output (default: True)", - ) - output_group.add_argument( - "--stream", - action=argparse.BooleanOptionalAction, - default=True, - help="Enable/disable streaming responses (default: True)", - ) - output_group.add_argument( - "--user-input-color", - default="#00cc00", - help="Set the color for user input (default: #00cc00)", - ) - output_group.add_argument( - "--tool-output-color", - default=None, - help="Set the color for tool output (default: None)", - ) - output_group.add_argument( - "--tool-error-color", - default="#FF2222", - help="Set the color for tool error messages (default: red)", - ) - output_group.add_argument( - "--assistant-output-color", - default="#0088ff", - help="Set the color for assistant output (default: #0088ff)", - ) - output_group.add_argument( - "--code-theme", - default="default", - help=( - "Set the markdown code theme (default: default, other options include monokai," - " solarized-dark, solarized-light)" - ), - ) - output_group.add_argument( - "--show-diffs", - action="store_true", - help="Show diffs when committing changes (default: False)", - default=False, - ) - - ########## - git_group = parser.add_argument_group("Git Settings") - git_group.add_argument( - "--git", - action=argparse.BooleanOptionalAction, - default=True, - help="Enable/disable looking for a git repo (default: True)", - ) - git_group.add_argument( - "--gitignore", - action=argparse.BooleanOptionalAction, - default=True, - help="Enable/disable adding .aider* to .gitignore (default: True)", - ) - default_aiderignore_file = ( - os.path.join(git_root, ".aiderignore") if git_root else ".aiderignore" - ) - git_group.add_argument( - "--aiderignore", - metavar="AIDERIGNORE", - default=default_aiderignore_file, - help="Specify the aider ignore file (default: .aiderignore in git root)", - ) - git_group.add_argument( - "--auto-commits", - action=argparse.BooleanOptionalAction, - default=True, - help="Enable/disable auto commit of GPT changes (default: True)", - ) - git_group.add_argument( - "--dirty-commits", - action=argparse.BooleanOptionalAction, - default=True, - help="Enable/disable commits when repo is found dirty (default: True)", - ) - git_group.add_argument( - "--dry-run", - action=argparse.BooleanOptionalAction, - default=False, - help="Perform a dry run without modifying files (default: False)", - ) - git_group.add_argument( - "--commit", - action="store_true", - help="Commit all pending changes with a suitable commit message, then exit", - default=False, - ) - - ########## - other_group = parser.add_argument_group("Other Settings") - other_group.add_argument( - "--version", - action="version", - version=f"%(prog)s {__version__}", - help="Show the version number and exit", - ) - other_group.add_argument( - "--check-update", - action="store_true", - help="Check for updates and return status in the exit code", - default=False, - ) - other_group.add_argument( - "--skip-check-update", - action="store_true", - help="Skips checking for the update when the program runs", - ) - other_group.add_argument( - "--apply", - metavar="FILE", - help="Apply the changes from the given file instead of running the chat (debug)", - ) - other_group.add_argument( - "--yes", - action="store_true", - help="Always say yes to every confirmation", - default=None, - ) - other_group.add_argument( - "-v", - "--verbose", - action="store_true", - help="Enable verbose output", - default=False, - ) - other_group.add_argument( - "--show-repo-map", - action="store_true", - help="Print the repo map and exit (debug)", - default=False, - ) - other_group.add_argument( - "--message", - "--msg", - "-m", - metavar="COMMAND", - help="Specify a single message to send GPT, process reply then exit (disables chat mode)", - ) - other_group.add_argument( - "--message-file", - "-f", - metavar="MESSAGE_FILE", - help=( - "Specify a file containing the message to send GPT, process reply, then exit (disables" - " chat mode)" - ), - ) - other_group.add_argument( - "--encoding", - default="utf-8", - help="Specify the encoding for input and output (default: utf-8)", - ) - other_group.add_argument( - "-c", - "--config", - is_config_file=True, - metavar="CONFIG_FILE", - help=( - "Specify the config file (default: search for .aider.conf.yml in git root, cwd" - " or home directory)" - ), - ) - - return parser - - def format_settings(parser, args): show = scrub_sensitive_info(args, parser.format_values()) show += "\n" From d889dfc87923f1129690b78a83a521619484a8a3 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 13:25:31 -0700 Subject: [PATCH 53/82] added args.py --- aider/args.py | 368 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 aider/args.py diff --git a/aider/args.py b/aider/args.py new file mode 100644 index 000000000..41d4dea97 --- /dev/null +++ b/aider/args.py @@ -0,0 +1,368 @@ +import argparse +import os + +import configargparse + +from aider import __version__, models + + +def get_parser(default_config_files, git_root): + parser = configargparse.ArgumentParser( + description="aider is GPT powered coding in your terminal", + add_config_file_help=True, + default_config_files=default_config_files, + config_file_parser_class=configargparse.YAMLConfigFileParser, + auto_env_var_prefix="AIDER_", + ) + + ########## + group = parser.add_argument_group("Main") + group.add_argument( + "files", + metavar="FILE", + nargs="*", + help="files to edit with an LLM (optional)", + ) + group.add_argument( + "--openai-api-key", + metavar="OPENAI_API_KEY", + env_var="OPENAI_API_KEY", + help="Specify the OpenAI API key", + ) + group.add_argument( + "--anthropic-api-key", + metavar="ANTHROPIC_API_KEY", + env_var="ANTHROPIC_API_KEY", + help="Specify the OpenAI API key", + ) + default_model = models.DEFAULT_MODEL_NAME + group.add_argument( + "--model", + metavar="MODEL", + default=default_model, + help=f"Specify the model to use for the main chat (default: {default_model})", + ) + opus_model = "claude-3-opus-20240229" + group.add_argument( + "--opus", + action="store_const", + dest="model", + const=opus_model, + help=f"Use {opus_model} model for the main chat", + ) + sonnet_model = "claude-3-sonnet-20240229" + group.add_argument( + "--sonnet", + action="store_const", + dest="model", + const=sonnet_model, + help=f"Use {sonnet_model} model for the main chat", + ) + gpt_4_model = "gpt-4-0613" + group.add_argument( + "--4", + "-4", + action="store_const", + dest="model", + const=gpt_4_model, + help=f"Use {gpt_4_model} model for the main chat", + ) + gpt_4_turbo_model = "gpt-4-turbo" + group.add_argument( + "--4-turbo-vision", + action="store_const", + dest="model", + const=gpt_4_turbo_model, + help=f"Use {gpt_4_turbo_model} model for the main chat", + ) + gpt_3_model_name = "gpt-3.5-turbo" + group.add_argument( + "--35turbo", + "--35-turbo", + "--3", + "-3", + action="store_const", + dest="model", + const=gpt_3_model_name, + help=f"Use {gpt_3_model_name} model for the main chat", + ) + + ########## + group = parser.add_argument_group("Model Settings") + group.add_argument( + "--models", + metavar="MODEL", + help="List known models which match the (partial) MODEL name", + ) + group.add_argument( + "--openai-api-base", + metavar="OPENAI_API_BASE", + env_var="OPENAI_API_BASE", + help="Specify the api base url", + ) + group.add_argument( + "--openai-api-type", + metavar="OPENAI_API_TYPE", + env_var="OPENAI_API_TYPE", + help="Specify the api_type", + ) + group.add_argument( + "--openai-api-version", + metavar="OPENAI_API_VERSION", + env_var="OPENAI_API_VERSION", + help="Specify the api_version", + ) + group.add_argument( + "--openai-api-deployment-id", + metavar="OPENAI_API_DEPLOYMENT_ID", + env_var="OPENAI_API_DEPLOYMENT_ID", + help="Specify the deployment_id", + ) + group.add_argument( + "--openai-organization-id", + metavar="OPENAI_ORGANIZATION_ID", + env_var="OPENAI_ORGANIZATION_ID", + help="Specify the OpenAI organization ID", + ) + group.add_argument( + "--edit-format", + metavar="EDIT_FORMAT", + default=None, + help="Specify what edit format the LLM should use (default depends on model)", + ) + group.add_argument( + "--weak-model", + metavar="WEAK_MODEL", + default=None, + help=( + "Specify the model to use for commit messages and chat history summarization (default" + " depends on --model)" + ), + ) + group.add_argument( + "--show-model-warnings", + action=argparse.BooleanOptionalAction, + default=True, + help="Only work with models that have meta-data available (default: True)", + ) + group.add_argument( + "--map-tokens", + type=int, + default=1024, + help="Max number of tokens to use for repo map, use 0 to disable (default: 1024)", + ) + + ########## + group = parser.add_argument_group("History Files") + default_input_history_file = ( + os.path.join(git_root, ".aider.input.history") if git_root else ".aider.input.history" + ) + default_chat_history_file = ( + os.path.join(git_root, ".aider.chat.history.md") if git_root else ".aider.chat.history.md" + ) + group.add_argument( + "--input-history-file", + metavar="INPUT_HISTORY_FILE", + default=default_input_history_file, + help=f"Specify the chat input history file (default: {default_input_history_file})", + ) + group.add_argument( + "--chat-history-file", + metavar="CHAT_HISTORY_FILE", + default=default_chat_history_file, + help=f"Specify the chat history file (default: {default_chat_history_file})", + ) + + ########## + group = parser.add_argument_group("Output Settings") + group.add_argument( + "--dark-mode", + action="store_true", + help="Use colors suitable for a dark terminal background (default: False)", + default=False, + ) + group.add_argument( + "--light-mode", + action="store_true", + help="Use colors suitable for a light terminal background (default: False)", + default=False, + ) + group.add_argument( + "--pretty", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable pretty, colorized output (default: True)", + ) + group.add_argument( + "--stream", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable streaming responses (default: True)", + ) + group.add_argument( + "--user-input-color", + default="#00cc00", + help="Set the color for user input (default: #00cc00)", + ) + group.add_argument( + "--tool-output-color", + default=None, + help="Set the color for tool output (default: None)", + ) + group.add_argument( + "--tool-error-color", + default="#FF2222", + help="Set the color for tool error messages (default: red)", + ) + group.add_argument( + "--assistant-output-color", + default="#0088ff", + help="Set the color for assistant output (default: #0088ff)", + ) + group.add_argument( + "--code-theme", + default="default", + help=( + "Set the markdown code theme (default: default, other options include monokai," + " solarized-dark, solarized-light)" + ), + ) + group.add_argument( + "--show-diffs", + action="store_true", + help="Show diffs when committing changes (default: False)", + default=False, + ) + + ########## + group = parser.add_argument_group("Git Settings") + group.add_argument( + "--git", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable looking for a git repo (default: True)", + ) + group.add_argument( + "--gitignore", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable adding .aider* to .gitignore (default: True)", + ) + default_aiderignore_file = ( + os.path.join(git_root, ".aiderignore") if git_root else ".aiderignore" + ) + group.add_argument( + "--aiderignore", + metavar="AIDERIGNORE", + default=default_aiderignore_file, + help="Specify the aider ignore file (default: .aiderignore in git root)", + ) + group.add_argument( + "--auto-commits", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable auto commit of LLM changes (default: True)", + ) + group.add_argument( + "--dirty-commits", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable commits when repo is found dirty (default: True)", + ) + group.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=False, + help="Perform a dry run without modifying files (default: False)", + ) + group.add_argument( + "--commit", + action="store_true", + help="Commit all pending changes with a suitable commit message, then exit", + default=False, + ) + + ########## + group = parser.add_argument_group("Other Settings") + group.add_argument( + "--voice-language", + metavar="VOICE_LANGUAGE", + default="en", + help="Specify the language for voice using ISO 639-1 code (default: auto)", + ) + group.add_argument( + "--version", + action="version", + version=f"%(prog)s {__version__}", + help="Show the version number and exit", + ) + group.add_argument( + "--check-update", + action="store_true", + help="Check for updates and return status in the exit code", + default=False, + ) + group.add_argument( + "--skip-check-update", + action="store_true", + help="Skips checking for the update when the program runs", + ) + group.add_argument( + "--apply", + metavar="FILE", + help="Apply the changes from the given file instead of running the chat (debug)", + ) + group.add_argument( + "--yes", + action="store_true", + help="Always say yes to every confirmation", + default=None, + ) + group.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + default=False, + ) + group.add_argument( + "--show-repo-map", + action="store_true", + help="Print the repo map and exit (debug)", + default=False, + ) + group.add_argument( + "--message", + "--msg", + "-m", + metavar="COMMAND", + help=( + "Specify a single message to send the LLM, process reply then exit (disables chat mode)" + ), + ) + group.add_argument( + "--message-file", + "-f", + metavar="MESSAGE_FILE", + help=( + "Specify a file containing the message to send the LLM, process reply, then exit" + " (disables chat mode)" + ), + ) + group.add_argument( + "--encoding", + default="utf-8", + help="Specify the encoding for input and output (default: utf-8)", + ) + group.add_argument( + "-c", + "--config", + is_config_file=True, + metavar="CONFIG_FILE", + help=( + "Specify the config file (default: search for .aider.conf.yml in git root, cwd" + " or home directory)" + ), + ) + + return parser From 9bc8ed92278c8f424b6b02555943e50994415ee8 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 13:26:29 -0700 Subject: [PATCH 54/82] fixed test to work with yield --- tests/test_wholefile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_wholefile.py b/tests/test_wholefile.py index 575444105..9cbef9a99 100644 --- a/tests/test_wholefile.py +++ b/tests/test_wholefile.py @@ -304,6 +304,7 @@ Do this: """ coder.partial_response_function_call = dict() + return [] coder.send = MagicMock(side_effect=mock_send) From d60b3432749aa33c87cfc08d8d8759fd79ff6722 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 13:44:04 -0700 Subject: [PATCH 55/82] layout --- aider/gui.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index 07a2eb483..f2149d8b1 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -151,6 +151,7 @@ class GUI: # self.do_recommended_actions() self.do_add_to_chat() self.do_recent_msgs() + self.do_clear_chat_history() def do_recommended_actions(self): with st.expander("Recommended actions", expanded=True): @@ -166,21 +167,20 @@ class GUI: self.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?") def do_add_to_chat(self): - with st.expander("Add to the chat", expanded=True): - self.do_add_files() - self.do_add_web_page() - self.do_clear_chat_history() + # with st.expander("Add to the chat", expanded=True): + self.do_add_files() + self.do_add_web_page() def do_add_files(self): fnames = st.multiselect( - "Files for the LLM to edit", + "Add files to the chat", sorted(self.coder.get_all_relative_files()), default=sorted(self.coder.get_inchat_relative_files()), placeholder="Files to edit", disabled=self.prompt_pending(), help=( "Only add the files that need to be *edited* for the task you are working" - " on. Aider will pull in other code to provide relevant context to the LLM." + " on. Aider will pull in other relevant code to provide context to the LLM." ), ) @@ -194,7 +194,7 @@ class GUI: self.info(f"Removed {fname} from the chat") def do_add_web_page(self): - with st.popover("Add web page"): + with st.popover("Add a web page to the chat"): self.do_web() def do_add_image(self): @@ -234,7 +234,8 @@ class GUI: st.write("hi") def do_clear_chat_history(self): - if self.button("Clear chat history"): + text = "Saves tokens, reduces confusion" + if self.button("Clear chat history", help=text): self.coder.done_messages = [] self.coder.cur_messages = [] self.info("Cleared chat history. Now the LLM can't see anything before this line.") @@ -272,7 +273,7 @@ class GUI: with self.recent_msgs_empty: self.old_prompt = st.selectbox( - "Resend recent chat message", + "Resend a recent chat message", self.state.input_history, placeholder="Choose a recent chat message", # label_visibility="collapsed", @@ -501,7 +502,15 @@ class GUI: def gui_main(): - st.set_page_config(layout="wide") + st.set_page_config( + layout="wide", + page_title="Aider", + menu_items={ + "Get Help": "https://aider.chat/docs/faq.html", + "Report a bug": "https://github.com/paul-gauthier/aider/issues", + "About": "# Aider\nAI pair programming in your browser.", + }, + ) GUI() From 542c091cf395a932374ab378fdda3ec77e4371b4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 14:08:25 -0700 Subject: [PATCH 56/82] Added --gui functionality --- aider/args.py | 7 +++++++ aider/main.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/aider/args.py b/aider/args.py index 41d4dea97..e01646539 100644 --- a/aider/args.py +++ b/aider/args.py @@ -364,5 +364,12 @@ def get_parser(default_config_files, git_root): " or home directory)" ), ) + group.add_argument( + "--gui", + "--browser", + action="store_true", + help="Run aider in your browser", + default=False, + ) return parser diff --git a/aider/main.py b/aider/main.py index 6dda7a1cf..99805c0e3 100644 --- a/aider/main.py +++ b/aider/main.py @@ -5,6 +5,8 @@ from pathlib import Path import git import litellm +from click.testing import CliRunner +from streamlit.web import cli from aider import models from aider.args import get_parser @@ -141,6 +143,26 @@ def scrub_sensitive_info(args, text): return text +def launch_gui(args): + from aider import gui + + target = gui.__file__ + + # from streamlit.web import bootstrap + # bootstrap.load_config_options(flag_options={}) + # cli.main_run(target, args) + # sys.argv = ['streamlit', 'run', '--'] + args + + runner = CliRunner() + st_args = [ + "run", + target, + "--global.developmentMode=false", + "--", + ] + runner.invoke(cli.main, st_args + args) + + def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): if argv is None: argv = sys.argv[1:] @@ -163,6 +185,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F parser = get_parser(default_config_files, git_root) args = parser.parse_args(argv) + if args.gui and not return_coder: + launch_gui(argv) + return + if args.dark_mode: args.user_input_color = "#32FF32" args.tool_error_color = "#FF3333" From 4f996d43ae841f0eb2d3aa798d2991c68481ea3f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 14:17:55 -0700 Subject: [PATCH 57/82] added streamlit dep --- requirements.in | 1 + requirements.txt | 69 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/requirements.in b/requirements.in index df796119f..bbf213d8a 100644 --- a/requirements.in +++ b/requirements.in @@ -26,3 +26,4 @@ playwright pypandoc litellm google-generativeai +streamlit diff --git a/requirements.txt b/requirements.txt index 1cfc8cff2..010fdd2fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,8 @@ aiohttp==3.9.5 # via litellm aiosignal==1.3.1 # via aiohttp +altair==5.3.0 + # via streamlit annotated-types==0.6.0 # via pydantic anyio==4.3.0 @@ -23,8 +25,12 @@ backoff==2.2.1 # via -r requirements.in beautifulsoup4==4.12.3 # via -r requirements.in +blinker==1.8.1 + # via streamlit cachetools==5.3.3 - # via google-auth + # via + # google-auth + # streamlit certifi==2024.2.2 # via # httpcore @@ -37,7 +43,9 @@ cffi==1.16.0 charset-normalizer==3.3.2 # via requests click==8.1.7 - # via litellm + # via + # litellm + # streamlit configargparse==1.7 # via -r requirements.in diff-match-patch==20230430 @@ -57,7 +65,9 @@ fsspec==2024.3.1 gitdb==4.0.11 # via gitpython gitpython==3.1.43 - # via -r requirements.in + # via + # -r requirements.in + # streamlit google-ai-generativelanguage==0.6.2 # via google-generativeai google-api-core[grpc]==2.18.0 @@ -113,9 +123,14 @@ idna==3.7 importlib-metadata==7.1.0 # via litellm jinja2==3.1.3 - # via litellm + # via + # altair + # litellm + # pydeck jsonschema==4.21.1 - # via -r requirements.in + # via + # -r requirements.in + # altair jsonschema-specifications==2023.12.1 # via jsonschema litellm==1.35.23 @@ -135,7 +150,12 @@ networkx==3.2.1 numpy==1.26.4 # via # -r requirements.in + # altair + # pandas + # pyarrow + # pydeck # scipy + # streamlit openai==1.23.3 # via # -r requirements.in @@ -143,13 +163,21 @@ openai==1.23.3 packaging==24.0 # via # -r requirements.in + # altair # huggingface-hub + # streamlit +pandas==2.2.2 + # via + # altair + # streamlit pathspec==0.12.1 # via # -r requirements.in # grep-ast pillow==10.3.0 - # via -r requirements.in + # via + # -r requirements.in + # streamlit playwright==1.43.0 # via -r requirements.in prompt-toolkit==3.0.43 @@ -166,6 +194,9 @@ protobuf==4.25.3 # googleapis-common-protos # grpcio-status # proto-plus + # streamlit +pyarrow==16.0.0 + # via streamlit pyasn1==0.6.0 # via # pyasn1-modules @@ -180,6 +211,8 @@ pydantic==2.7.1 # openai pydantic-core==2.18.2 # via pydantic +pydeck==0.9.0b1 + # via streamlit pyee==11.1.0 # via playwright pygments==2.17.2 @@ -188,8 +221,12 @@ pypandoc==1.13 # via -r requirements.in pyparsing==3.1.2 # via httplib2 +python-dateutil==2.9.0.post0 + # via pandas python-dotenv==1.0.1 # via litellm +pytz==2024.1 + # via pandas pyyaml==6.0.1 # via # -r requirements.in @@ -205,9 +242,12 @@ requests==2.31.0 # google-api-core # huggingface-hub # litellm + # streamlit # tiktoken rich==13.7.1 - # via -r requirements.in + # via + # -r requirements.in + # streamlit rpds-py==0.18.0 # via # jsonschema @@ -216,6 +256,8 @@ rsa==4.9 # via google-auth scipy==1.13.0 # via -r requirements.in +six==1.16.0 + # via python-dateutil smmap==5.0.1 # via gitdb sniffio==1.3.1 @@ -229,12 +271,22 @@ soundfile==0.12.1 # via -r requirements.in soupsieve==2.5 # via beautifulsoup4 +streamlit==1.33.0 + # via -r requirements.in +tenacity==8.2.3 + # via streamlit tiktoken==0.6.0 # via # -r requirements.in # litellm tokenizers==0.19.1 # via litellm +toml==0.10.2 + # via streamlit +toolz==0.12.1 + # via altair +tornado==6.4 + # via streamlit tqdm==4.66.2 # via # google-generativeai @@ -252,6 +304,9 @@ typing-extensions==4.11.0 # pydantic # pydantic-core # pyee + # streamlit +tzdata==2024.1 + # via pandas uritemplate==4.1.1 # via google-api-python-client urllib3==2.2.1 From 0bf45c2cf6100903a00940817b058a829dc835d3 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 14:28:50 -0700 Subject: [PATCH 58/82] added favicon --- aider/gui.py | 1 + assets/favicon-32x32.png | Bin 0 -> 1891 bytes 2 files changed, 1 insertion(+) create mode 100644 assets/favicon-32x32.png diff --git a/aider/gui.py b/aider/gui.py index f2149d8b1..a9df136b1 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -505,6 +505,7 @@ def gui_main(): st.set_page_config( layout="wide", page_title="Aider", + page_icon="https://aider.chat/assets/favicon-32x32.png", menu_items={ "Get Help": "https://aider.chat/docs/faq.html", "Report a bug": "https://github.com/paul-gauthier/aider/issues", diff --git a/assets/favicon-32x32.png b/assets/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..0a921307c2fbd8589bd06da0b33545e084162d8d GIT binary patch literal 1891 zcmZ{ldpOe#8^?b?zu7d0VU9fx%ekIt$swoOY+@~P7)GgSPL<=Va!7s?QarDfLr9uX zM6nv?d?=Ji2c8m9p~w{Vu0P*D-s`^Z`+HsA@BO*%KkxJIZqD-Pjc5P> zPl@7ww+vD;V-pgVB!UPfx)K54cDC$Vp!DyM>Eh`M00~9_0H*@LcZmhh0Kf@60L(G~ zz_tJY)T4_Zc-Tt<3)8jw2k3M zzI{Xdm z{wJhNv`_BW?rv*H?VGyFf0E|YYFd?#`MGrrvQ7X7z(Dlj@|&KaU85xIs$Pnj`EmtJPb7c2RvD(&Us+Tt996m?9!a5 z1NDoinys1)x6BfXUl_UZrel(KK{vB`N+O$u2(m`r%$#o7@dL=({;CiHji!43k|QL% zCKb&(#@YsFR%U=ZIo#{`Yi;_W8Pjg%3$_mq<}D(8>RP^uzTHy2P8ZLGh0;v4KU%F$ z#_28?tA{?M&-U9W*gytMN$RW*Zz z9W;?Pk~+kQ!_v>mq>$RjLiF6T14A(EpZ`u{~T{Z;1DB^o=6il?p}WmkUOca~b?QsFM6;-$crcQGiA;l|sMjQbDa5N}Do z#z}f}lbRYYrMHe3rTJ;(BUNgYEw;#UOz-D`ba7I6owpo8<%pi=C;9pfWd^1TQaRcT zXx5cY6E>RbS=lOjA#8O%8V7n111eN#$j>>Qg(NpKVJokO`=J`WE?3jQSV|s!2wJZ& znBi_BKGKPbnvHUD%)8@yC1XqTw@e$=$W-fdOpi`CiPw9i*;WUb|01btKgt;Z1y>j1nX= zb~&p}KQZ_3st)r|*K>8dl%c#f3I%%DYL?4h3wcHqcEUkAp z&v%8psx_dUMue$vl;5njB9)E%o|WV@IW)g^%rMw5;895L2j}z~ZBp97?@PLaZ#)06 zs&Lvc}k&G3&ztexWp)BY<}DS{hohT0uw1OA4QYGp;~t z%GGwHYh>>Br5{#YtR1hw78&oh!n`056Z|y8K_vc6t zsY^AC;7Uq}6Y+0FCe-ghie|FsDEB5-u9>qS%|($ z{(QWb5?N7}zz@I&3}z0oMcX)E!A7Yy(!?5T(C&Jgzq6f7X;b+W?9|Zs{ApIfnQ-Qt z(rQ=LgFvxY3Cd;nB z; Date: Sun, 28 Apr 2024 14:30:05 -0700 Subject: [PATCH 59/82] favicon --- assets/favicon-32x32.png | Bin 0 -> 1891 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/favicon-32x32.png diff --git a/assets/favicon-32x32.png b/assets/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..0a921307c2fbd8589bd06da0b33545e084162d8d GIT binary patch literal 1891 zcmZ{ldpOe#8^?b?zu7d0VU9fx%ekIt$swoOY+@~P7)GgSPL<=Va!7s?QarDfLr9uX zM6nv?d?=Ji2c8m9p~w{Vu0P*D-s`^Z`+HsA@BO*%KkxJIZqD-Pjc5P> zPl@7ww+vD;V-pgVB!UPfx)K54cDC$Vp!DyM>Eh`M00~9_0H*@LcZmhh0Kf@60L(G~ zz_tJY)T4_Zc-Tt<3)8jw2k3M zzI{Xdm z{wJhNv`_BW?rv*H?VGyFf0E|YYFd?#`MGrrvQ7X7z(Dlj@|&KaU85xIs$Pnj`EmtJPb7c2RvD(&Us+Tt996m?9!a5 z1NDoinys1)x6BfXUl_UZrel(KK{vB`N+O$u2(m`r%$#o7@dL=({;CiHji!43k|QL% zCKb&(#@YsFR%U=ZIo#{`Yi;_W8Pjg%3$_mq<}D(8>RP^uzTHy2P8ZLGh0;v4KU%F$ z#_28?tA{?M&-U9W*gytMN$RW*Zz z9W;?Pk~+kQ!_v>mq>$RjLiF6T14A(EpZ`u{~T{Z;1DB^o=6il?p}WmkUOca~b?QsFM6;-$crcQGiA;l|sMjQbDa5N}Do z#z}f}lbRYYrMHe3rTJ;(BUNgYEw;#UOz-D`ba7I6owpo8<%pi=C;9pfWd^1TQaRcT zXx5cY6E>RbS=lOjA#8O%8V7n111eN#$j>>Qg(NpKVJokO`=J`WE?3jQSV|s!2wJZ& znBi_BKGKPbnvHUD%)8@yC1XqTw@e$=$W-fdOpi`CiPw9i*;WUb|01btKgt;Z1y>j1nX= zb~&p}KQZ_3st)r|*K>8dl%c#f3I%%DYL?4h3wcHqcEUkAp z&v%8psx_dUMue$vl;5njB9)E%o|WV@IW)g^%rMw5;895L2j}z~ZBp97?@PLaZ#)06 zs&Lvc}k&G3&ztexWp)BY<}DS{hohT0uw1OA4QYGp;~t z%GGwHYh>>Br5{#YtR1hw78&oh!n`056Z|y8K_vc6t zsY^AC;7Uq}6Y+0FCe-ghie|FsDEB5-u9>qS%|($ z{(QWb5?N7}zz@I&3}z0oMcX)E!A7Yy(!?5T(C&Jgzq6f7X;b+W?9|Zs{ApIfnQ-Qt z(rQ=LgFvxY3Cd;nB z; Date: Sun, 28 Apr 2024 14:36:06 -0700 Subject: [PATCH 60/82] Added favicon to html --- _layouts/default.html | 1 + 1 file changed, 1 insertion(+) diff --git a/_layouts/default.html b/_layouts/default.html index 8c3201301..c5f256faf 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -11,6 +11,7 @@ + {% include head-custom.html %} From 73bad5250b6cfebf260bfb6443bb2ded3f27faef Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 14:38:30 -0700 Subject: [PATCH 61/82] hit control-c --- aider/main.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/aider/main.py b/aider/main.py index 99805c0e3..3617e31d0 100644 --- a/aider/main.py +++ b/aider/main.py @@ -146,12 +146,9 @@ def scrub_sensitive_info(args, text): def launch_gui(args): from aider import gui - target = gui.__file__ + print("Hit CONTROL-C to exit...") - # from streamlit.web import bootstrap - # bootstrap.load_config_options(flag_options={}) - # cli.main_run(target, args) - # sys.argv = ['streamlit', 'run', '--'] + args + target = gui.__file__ runner = CliRunner() st_args = [ @@ -162,6 +159,11 @@ def launch_gui(args): ] runner.invoke(cli.main, st_args + args) + # from streamlit.web import bootstrap + # bootstrap.load_config_options(flag_options={}) + # cli.main_run(target, args) + # sys.argv = ['streamlit', 'run', '--'] + args + def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): if argv is None: From ec9fac2500277e047578e413a5d7d542ab6420aa Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 14:48:40 -0700 Subject: [PATCH 62/82] simpler launch of streamlit --- aider/coders/base_coder.py | 1 + aider/main.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index ae68d0a50..617ac1642 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -253,6 +253,7 @@ class Coder: self.io.tool_output("JSON Schema:") self.io.tool_output(json.dumps(self.functions, indent=4)) + def show_announcements(self): for line in self.get_announcements(): self.io.tool_output(line) diff --git a/aider/main.py b/aider/main.py index 3617e31d0..942429d24 100644 --- a/aider/main.py +++ b/aider/main.py @@ -5,7 +5,6 @@ from pathlib import Path import git import litellm -from click.testing import CliRunner from streamlit.web import cli from aider import models @@ -146,19 +145,21 @@ def scrub_sensitive_info(args, text): def launch_gui(args): from aider import gui - print("Hit CONTROL-C to exit...") + print() + print("CONTROL-C to exit...") target = gui.__file__ - runner = CliRunner() st_args = [ "run", target, "--global.developmentMode=false", "--", ] - runner.invoke(cli.main, st_args + args) + cli.main(st_args + args) + # from click.testing import CliRunner + # runner = CliRunner() # from streamlit.web import bootstrap # bootstrap.load_config_options(flag_options={}) # cli.main_run(target, args) @@ -330,6 +331,8 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if return_coder: return coder + coder.show_announcements() + if args.commit: coder.commands.cmd_commit("") return From b3225a4070ae240ba893513509e89548340feb27 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 15:18:53 -0700 Subject: [PATCH 63/82] added warning --- aider/gui.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index a9df136b1..fe5e3af0d 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -143,15 +143,15 @@ class GUI: st.title("Aider") # self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"]) - def do_settings_tab(self): - pass - - def do_cmd_tab(self): - with st.sidebar: # self.do_recommended_actions() self.do_add_to_chat() self.do_recent_msgs() self.do_clear_chat_history() + # st.container(height=150, border=False) + # st.write("### Experimental") + + def do_settings_tab(self): + pass def do_recommended_actions(self): with st.expander("Recommended actions", expanded=True): @@ -290,7 +290,12 @@ class GUI: # stuff a bunch of vertical whitespace at the top # to get all the chat text to the bottom self.messages.container(height=300, border=False) + with self.messages: + st.warning( + "This browser version of aider is experimental. Please share feedback in [GitHub" + " issues](https://github.com/paul-gauthier/aider/issues)." + ) for msg in self.state.messages: role = msg["role"] @@ -353,7 +358,6 @@ class GUI: self.do_messages_container() self.do_sidebar() - self.do_cmd_tab() user_inp = st.chat_input("Say something") if user_inp: From 9008789e84e6f592bbabca1dafb4f22cfe64dc5f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 15:32:46 -0700 Subject: [PATCH 64/82] added new template --- .github/ISSUE_TEMPLATE/issue.yml | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/issue.yml diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml new file mode 100644 index 000000000..650b4467d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -0,0 +1,34 @@ +name: Question or bug repot +description: Submit a question or bug report to help us improve aider +labels: +body: + - type: markdown + attributes: + - type: textarea + attributes: + label: Issue + description: Please describe your problem or question. + validations: + required: true + value: | + When asking questions or reporting issues, it is very helpful if you can include: + + - Aider version + - Model being used (`gpt-4-xxx`, etc) + - Other switches or config settings that are active + + The easiest way to do this is just just copy & paste the announcement lines that aider prints when you launch it, like these: + + ``` + Aider v0.21.2-dev + Model: gpt-4-0613 using diff edit format + Git repo: .git with 134 files + Repo-map: using 1024 tokens + Use /help to see in-chat commands, run with --help to see cmd line args + ``` + - type: textarea + attributes: + label: Version and model info + description: Please include the aider version, model name and other config/switches in effect. + validations: + required: false \ No newline at end of file From 58b5a379430bf19d5309e1fdb89b589feb1c6d6a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 15:34:59 -0700 Subject: [PATCH 65/82] fix --- .github/ISSUE_TEMPLATE/issue.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index 650b4467d..fad178f72 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -1,15 +1,15 @@ name: Question or bug repot description: Submit a question or bug report to help us improve aider -labels: +labels: [] body: - - type: markdown - attributes: - type: textarea attributes: label: Issue description: Please describe your problem or question. validations: required: true + - type: markdown + attributes: value: | When asking questions or reporting issues, it is very helpful if you can include: From 2a116eac6d3327da4ab1e9328223b3c4317daaae Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 15:36:43 -0700 Subject: [PATCH 66/82] copy --- .github/ISSUE_TEMPLATE/issue.yml | 2 +- .github/ISSUE_TEMPLATE/new_issue.md | 24 ------------------------ 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/new_issue.md diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index fad178f72..a965cc14f 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -1,4 +1,4 @@ -name: Question or bug repot +name: Question or bug report description: Submit a question or bug report to help us improve aider labels: [] body: diff --git a/.github/ISSUE_TEMPLATE/new_issue.md b/.github/ISSUE_TEMPLATE/new_issue.md deleted file mode 100644 index 7f525dc47..000000000 --- a/.github/ISSUE_TEMPLATE/new_issue.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: New issue -about: Ask a question or report a bug -title: '' -labels: '' -assignees: '' - ---- - -When asking questions or reporting issues, it is very helpful if you can include: - -- Aider version -- Model being used (`gpt-4-xxx`, etc) -- Other switches or config settings that are active - -The easiest way to do this is just just copy & paste the announcement lines that aider prints when you launch it, like these: - -``` -Aider v0.21.2-dev -Model: gpt-4-0613 using diff edit format -Git repo: .git with 134 files -Repo-map: using 1024 tokens -Use /help to see in-chat commands, run with --help to see cmd line args -``` From 7114eb55b0ded2e10d8646921dcdef8208c6c8f5 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 15:39:03 -0700 Subject: [PATCH 67/82] copy --- .github/ISSUE_TEMPLATE/issue.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index a965cc14f..2d1f17305 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -8,9 +8,11 @@ body: description: Please describe your problem or question. validations: required: true - - type: markdown + - type: textarea attributes: - value: | + label: Version and model info + description: Please include the aider version, model name and other config/switches in effect. + placeholder: | When asking questions or reporting issues, it is very helpful if you can include: - Aider version @@ -26,9 +28,5 @@ body: Repo-map: using 1024 tokens Use /help to see in-chat commands, run with --help to see cmd line args ``` - - type: textarea - attributes: - label: Version and model info - description: Please include the aider version, model name and other config/switches in effect. validations: required: false \ No newline at end of file From 0689d89e87813703d0e5ec83eeaff7f333b6d55c Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 15:41:10 -0700 Subject: [PATCH 68/82] copy --- .github/ISSUE_TEMPLATE/issue.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index 2d1f17305..680f65ff1 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -13,20 +13,11 @@ body: label: Version and model info description: Please include the aider version, model name and other config/switches in effect. placeholder: | - When asking questions or reporting issues, it is very helpful if you can include: - - - Aider version - - Model being used (`gpt-4-xxx`, etc) - - Other switches or config settings that are active - - The easiest way to do this is just just copy & paste the announcement lines that aider prints when you launch it, like these: - - ``` + Please include aider version, model being used (`gpt-4-xxx`, etc) and any other switches or config settings that are active. Just copy & paste these lines from aider's output: + Aider v0.21.2-dev Model: gpt-4-0613 using diff edit format Git repo: .git with 134 files Repo-map: using 1024 tokens - Use /help to see in-chat commands, run with --help to see cmd line args - ``` validations: required: false \ No newline at end of file From b2acaae882698c09aba0020720a060f86f33eea0 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 15:42:41 -0700 Subject: [PATCH 69/82] copy --- .github/ISSUE_TEMPLATE/issue.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index 680f65ff1..097fbc102 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -11,10 +11,8 @@ body: - type: textarea attributes: label: Version and model info - description: Please include the aider version, model name and other config/switches in effect. - placeholder: | - Please include aider version, model being used (`gpt-4-xxx`, etc) and any other switches or config settings that are active. Just copy & paste these lines from aider's output: - + description: Please include aider version, model being used (`gpt-4-xxx`, etc) and any other switches or config settings that are active. + placeholder: | Aider v0.21.2-dev Model: gpt-4-0613 using diff edit format Git repo: .git with 134 files From 821dd1e18af0b72c30482207c7c4d18b81ee237c Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 17:08:17 -0700 Subject: [PATCH 70/82] copy --- docs/llms.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/llms.md b/docs/llms.md index b85dc3ede..861e39bf9 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -224,16 +224,26 @@ when working with models that aider is not familiar with. Aider can connect to local Ollama models. ``` +# Pull the model +ollama pull + # Start your ollama server ollama serve # In another terminal window: export OLLAMA_API_BASE=http://127.0.0.1:11434 aider --model ollama/ -``` -The Llama3 70B model works well with aider. -Give aider the `--edit-format diff` switch if you're working with it. +### +# +# llama3:70b works very well with aider +# +ollama pull llama3:70b +ollama serve +# ...in another terminal window: +export OLLAMA_API_BASE=http://127.0.0.1:11434 +aider --model ollama/llama3:70b +``` Also see the [model warnings](#model-warnings) section for information on warnings which will occur From f8b51ea2df6f8b8908dec7f4eeffffd6294e0738 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 17:12:23 -0700 Subject: [PATCH 71/82] copy --- docs/llms.md | 76 +++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/docs/llms.md b/docs/llms.md index 861e39bf9..e531c7a1d 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -42,8 +42,8 @@ So you should expect that models which are less capable than GPT-3.5 may struggl - [Cohere](#cohere) - [Azure](#azure) - [OpenRouter](#openrouter) -- [OpenAI compatible APIs](#openai-compatible-apis) - [Ollama](#ollama) +- [OpenAI compatible APIs](#openai-compatible-apis) - [Other LLMs](#other-llms) - [Model warnings](#model-warnings) - [Editing format](#editing-format) @@ -190,9 +190,6 @@ You'll need an [OpenRouter API key](https://openrouter.ai/keys). pip install aider-chat export OPENROUTER_API_KEY= -# Llama3 70B instruct -aider --model openrouter/meta-llama/llama-3-70b-instruct - # Or any other open router model aider --model openrouter// @@ -200,6 +197,47 @@ aider --model openrouter// aider --models openrouter/ ``` +In particular, Llama3 70B works well with aider: + +``` +# Llama3 70B instruct +aider --model openrouter/meta-llama/llama-3-70b-instruct +``` + + +## Ollama + +Aider can connect to local Ollama models. + +``` +# Pull the model +ollama pull + +# Start your ollama server +ollama serve + +# In another terminal window +export OLLAMA_API_BASE=http://127.0.0.1:11434 +aider --model ollama/ +``` + +In particular, `llama3:70b` works very well with aider + + +``` +ollama pull llama3:70b +ollama serve + +# ...in another terminal window... +export OLLAMA_API_BASE=http://127.0.0.1:11434 +aider --model ollama/llama3:70b +``` + +Also see the [model warnings](#model-warnings) +section for information on warnings which will occur +when working with models that aider is not familiar with. + + ## OpenAI compatible APIs Aider can connect to any LLM which is accessible via an OpenAI compatible API endpoint. @@ -219,36 +257,6 @@ See the [model warnings](#model-warnings) section for information on warnings which will occur when working with models that aider is not familiar with. -## Ollama - -Aider can connect to local Ollama models. - -``` -# Pull the model -ollama pull - -# Start your ollama server -ollama serve - -# In another terminal window: -export OLLAMA_API_BASE=http://127.0.0.1:11434 -aider --model ollama/ - -### -# -# llama3:70b works very well with aider -# -ollama pull llama3:70b -ollama serve -# ...in another terminal window: -export OLLAMA_API_BASE=http://127.0.0.1:11434 -aider --model ollama/llama3:70b -``` - -Also see the [model warnings](#model-warnings) -section for information on warnings which will occur -when working with models that aider is not familiar with. - ## Other LLMs Aider uses the [litellm](https://docs.litellm.ai/docs/providers) package From dd9da6ef6a62a836638306540dce2ec37b03292e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 17:31:30 -0700 Subject: [PATCH 72/82] copy --- docs/llms.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/llms.md b/docs/llms.md index e531c7a1d..b95cc471e 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -197,7 +197,7 @@ aider --model openrouter// aider --models openrouter/ ``` -In particular, Llama3 70B works well with aider: +In particular, Llama3 70B works well with aider, at low cost: ``` # Llama3 70B instruct @@ -221,7 +221,7 @@ export OLLAMA_API_BASE=http://127.0.0.1:11434 aider --model ollama/ ``` -In particular, `llama3:70b` works very well with aider +In particular, `llama3:70b` works very well with aider: ``` From 8a9005eed19417c59aa9432436ea8cb5e04bbb11 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 28 Apr 2024 17:36:58 -0700 Subject: [PATCH 73/82] copy --- docs/llms.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/llms.md b/docs/llms.md index b95cc471e..d1adf0149 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -200,7 +200,8 @@ aider --models openrouter/ In particular, Llama3 70B works well with aider, at low cost: ``` -# Llama3 70B instruct +pip install aider-chat +export OPENROUTER_API_KEY= aider --model openrouter/meta-llama/llama-3-70b-instruct ``` @@ -217,6 +218,7 @@ ollama pull ollama serve # In another terminal window +pip install aider-chat export OLLAMA_API_BASE=http://127.0.0.1:11434 aider --model ollama/ ``` From 3199a8f704b50b7846da8258e99e5489756a25e8 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 06:24:14 -0700 Subject: [PATCH 74/82] fix multiselect for 2nd,3rd,.. files --- aider/gui.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index fe5e3af0d..a4bc73480 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -150,6 +150,11 @@ class GUI: # st.container(height=150, border=False) # st.write("### Experimental") + st.warning( + "This browser version of aider is experimental. Please share feedback in [GitHub" + " issues](https://github.com/paul-gauthier/aider/issues)." + ) + def do_settings_tab(self): pass @@ -174,8 +179,8 @@ class GUI: def do_add_files(self): fnames = st.multiselect( "Add files to the chat", - sorted(self.coder.get_all_relative_files()), - default=sorted(self.coder.get_inchat_relative_files()), + self.coder.get_all_relative_files(), + default=self.state.initial_inchat_files, placeholder="Files to edit", disabled=self.prompt_pending(), help=( @@ -188,6 +193,7 @@ class GUI: if fname not in self.coder.get_inchat_relative_files(): self.coder.add_rel_fname(fname) self.info(f"Added {fname} to the chat") + for fname in self.coder.get_inchat_relative_files(): if fname not in fnames: self.coder.drop_rel_fname(fname) @@ -292,10 +298,6 @@ class GUI: self.messages.container(height=300, border=False) with self.messages: - st.warning( - "This browser version of aider is experimental. Please share feedback in [GitHub" - " issues](https://github.com/paul-gauthier/aider/issues)." - ) for msg in self.state.messages: role = msg["role"] @@ -329,6 +331,8 @@ class GUI: self.state.init("prompt") self.state.init("scraper") + self.state.init("initial_inchat_files", self.coder.get_inchat_relative_files()) + if "input_history" not in self.state.keys: input_history = list(self.coder.io.get_input_history()) seen = set() From 03908c5ab64561aed7ce9eac331603215d1dd2ef Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 06:35:45 -0700 Subject: [PATCH 75/82] copy --- docs/llms.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/llms.md b/docs/llms.md index d1adf0149..a34ebb05d 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -28,10 +28,12 @@ Aider can work also with local models, for example using [Ollama](#ollama). ## Other models Aider supports connecting to almost any LLM, -but it may not work well with some models depending on their capabilities. -For example, GPT-3.5 is just barely capable of reliably *editing code* to provide aider's -interactive "pair programming" style workflow. -So you should expect that models which are less capable than GPT-3.5 may struggle to perform well with aider. +but it may not work well with less capable models. +If you see the model returning code, but aider isn't able to edit your files +and commit the changes... +this is usually because the model isn't capable of properly +returning "code edits". +Models weaker than GPT 3.5 may have problems working well with aider. ## Configuring models @@ -288,8 +290,16 @@ for more details. ## Model warnings -On startup, aider tries to sanity check that it is configured correctly -to work with the specified models: +Aider supports connecting to almost any LLM, +but it may not work well with less capable models. +If you see the model returning code, but aider isn't able to edit your files +and commit the changes... +this is usually because the model isn't capable of properly +returning "code edits". +Models weaker than GPT 3.5 may have problems working well with aider. + +Aider tries to sanity check that it is configured correctly +to work with the specified model: - It checks to see that all required environment variables are set for the model. These variables are required to configure things like API keys, API base URLs, etc. - It checks a metadata database to look up the context window size and token costs for the model. From 29f4d788a36838fa14e8e6fe2289ce220196fb95 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 10:19:22 -0700 Subject: [PATCH 76/82] set streamlit config options --- aider/gui.py | 4 ++++ aider/main.py | 27 ++++++++++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/aider/gui.py b/aider/gui.py index a4bc73480..fa4e2d645 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -521,6 +521,10 @@ def gui_main(): }, ) + # config_options = st.config._config_options + # for key, value in config_options.items(): + # print(f"{key}: {value.value}") + GUI() diff --git a/aider/main.py b/aider/main.py index 942429d24..193f29f89 100644 --- a/aider/main.py +++ b/aider/main.py @@ -7,7 +7,7 @@ import git import litellm from streamlit.web import cli -from aider import models +from aider import __version__, models from aider.args import get_parser from aider.coders import Coder from aider.io import InputOutput @@ -150,13 +150,26 @@ def launch_gui(args): target = gui.__file__ - st_args = [ - "run", - target, - "--global.developmentMode=false", - "--", + st_args = ["run", target] + + st_args += [ + "--browser.gatherUsageStats=false", + "--runner.magicEnabled=false", + "--server.runOnSave=false", ] - cli.main(st_args + args) + + if "-dev" in __version__: + print("Watching for file changes.") + else: + st_args += [ + "--global.developmentMode=false", + "--server.fileWatcherType=none", + "--client.toolbarMode=viewer", # minimal? + ] + + st_args += ["--"] + args + + cli.main(st_args) # from click.testing import CliRunner # runner = CliRunner() From ae7d6a16af295ea2d72ae751174e9527e1c6b147 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 10:58:02 -0700 Subject: [PATCH 77/82] aider: Added additional favicon tags to ensure the favicon works well across all browsers. # Aider chat conversation: USER: add whatever other tags are needed so the favicon works well on all browsers ASSISTANT: To ensure the favicon works well across all browsers, we should add a few more link tags with different icon sizes and formats. Here are the changes to make in the `_layouts/default.html` file: _layouts/default.html ```html <<<<<<< SEARCH ======= >>>>>>> REPLACE ``` These additional tags cover: 1. A 16x16 PNG favicon for older browsers 2. An Apple Touch icon for iOS devices 3. A Web App Manifest file for progressive web apps 4. A mask icon for Safari pinned tabs 5. A tile color for Windows 8/10 start screen 6. A theme color for mobile browsers Make sure to also add the corresponding favicon files in the `/assets` directory of your project: - `favicon-16x16.png` - `apple-touch-icon.png` - `site.webmanifest` - `safari-pinned-tab.svg` With these changes, your favicon should display correctly across all modern browsers and devices. --- _layouts/default.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/_layouts/default.html b/_layouts/default.html index c5f256faf..1ae5a2a2b 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -12,6 +12,12 @@ + + + + + + {% include head-custom.html %} From 4b87f2b40f1721d3468ef0c335441a162be858cc Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 11:00:23 -0700 Subject: [PATCH 78/82] updated icons --- _layouts/default.html | 10 ++++---- assets/icons/android-chrome-192x192.png | Bin 0 -> 29475 bytes assets/icons/android-chrome-384x384.png | Bin 0 -> 93930 bytes assets/icons/apple-touch-icon.png | Bin 0 -> 26544 bytes assets/icons/browserconfig.xml | 9 +++++++ assets/icons/favicon-16x16.png | Bin 0 -> 1212 bytes assets/{ => icons}/favicon-32x32.png | Bin assets/icons/favicon.ico | Bin 0 -> 15086 bytes assets/icons/mstile-150x150.png | Bin 0 -> 18392 bytes assets/icons/safari-pinned-tab.svg | 32 ++++++++++++++++++++++++ assets/icons/site.webmanifest | 19 ++++++++++++++ 11 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 assets/icons/android-chrome-192x192.png create mode 100644 assets/icons/android-chrome-384x384.png create mode 100644 assets/icons/apple-touch-icon.png create mode 100644 assets/icons/browserconfig.xml create mode 100644 assets/icons/favicon-16x16.png rename assets/{ => icons}/favicon-32x32.png (100%) create mode 100644 assets/icons/favicon.ico create mode 100644 assets/icons/mstile-150x150.png create mode 100644 assets/icons/safari-pinned-tab.svg create mode 100644 assets/icons/site.webmanifest diff --git a/_layouts/default.html b/_layouts/default.html index 1ae5a2a2b..51c6cc4d9 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -11,11 +11,11 @@ - - - - - + + + + + diff --git a/assets/icons/android-chrome-192x192.png b/assets/icons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..58500c1c8aacf75905e87db51e4d7933dd7906db GIT binary patch literal 29475 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 EUzvN^r~m)} literal 0 HcmV?d00001 diff --git a/assets/icons/android-chrome-384x384.png b/assets/icons/android-chrome-384x384.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2f5b8baa2673c678a2d3758eff0e22bb71b3d7 GIT binary patch literal 93930 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 r6c}*dhOX8Msknm_P)U{hRFz26Adsy#dhNiAn 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 SO8V#mN=`~yvReH6kN*S5g5j0` literal 0 HcmV?d00001 diff --git a/assets/icons/apple-touch-icon.png b/assets/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..66f4a7256ad8642de34ad03ca75e81dc4296aaa5 GIT binary patch literal 26544 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 literal 0 HcmV?d00001 diff --git a/assets/icons/browserconfig.xml b/assets/icons/browserconfig.xml new file mode 100644 index 000000000..b3930d0f0 --- /dev/null +++ b/assets/icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/assets/icons/favicon-16x16.png b/assets/icons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..8245b45f5324313411c9f2a488ed9106a2861fe3 GIT binary patch literal 1212 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 kI&wdIA<;TV>je*&UI5Ev`}U9)EOn(hx% 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 literal 0 HcmV?d00001 diff --git a/assets/icons/safari-pinned-tab.svg b/assets/icons/safari-pinned-tab.svg new file mode 100644 index 000000000..93e5e19c3 --- /dev/null +++ b/assets/icons/safari-pinned-tab.svg @@ -0,0 +1,32 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + diff --git a/assets/icons/site.webmanifest b/assets/icons/site.webmanifest new file mode 100644 index 000000000..a1553eb86 --- /dev/null +++ b/assets/icons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} From e668803f82b1ae78fe9a8fad1a224a5d4047b981 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 12:35:57 -0700 Subject: [PATCH 79/82] added deepseek docs --- docs/llms.md | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/llms.md b/docs/llms.md index a34ebb05d..2a0ac3f4c 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -7,28 +7,27 @@ **Aider works best with [GPT-4 Turbo](#openai) and [Claude 3 Opus](#anthropic),** as they are the very best models for editing code. -Aider also works quite well with [GPT-3.5](#openai). ## Free models -**Aider works with a number of free API providers.** -Google's [Gemini 1.5 Pro](#gemini) is -the most capable free model to use with aider, with +Aider works with a number of **free** API providers: + +- Google's [Gemini 1.5 Pro](#gemini) is the most capable free model to use with aider, with code editing capabilities similar to GPT-3.5. -You can use [Llama 3 70B on Groq](#llama3) -which is comparable to GPT-3.5 in code editing performance. -Cohere also offers free API access to their [Command-R+ model](#cohere), -which works with aider -as a *very basic* coding assistant. +- You can use [Llama 3 70B on Groq](#llama3) which is comparable to GPT-3.5 in code editing performance. +- The [Deepseek Coder](#deepseek) model works well with aider, comparable to GPT-3.5. Deepseek.com currently offers 5M free tokens of API usage. +- Cohere also offers free API access to their [Command-R+ model](#cohere), which works with aider as a *very basic* coding assistant. ## Local models Aider can work also with local models, for example using [Ollama](#ollama). +It can also access +local models that provide an +[Open AI compatible API](#openai-compatible-apis). -## Other models +## Use a capable model -Aider supports connecting to almost any LLM, -but it may not work well with less capable models. +Be aware that aider may not work well with less capable models. If you see the model returning code, but aider isn't able to edit your files and commit the changes... this is usually because the model isn't capable of properly @@ -45,6 +44,7 @@ Models weaker than GPT 3.5 may have problems working well with aider. - [Azure](#azure) - [OpenRouter](#openrouter) - [Ollama](#ollama) +- [Deepseek](#deepseek) - [OpenAI compatible APIs](#openai-compatible-apis) - [Other LLMs](#other-llms) - [Model warnings](#model-warnings) @@ -242,6 +242,23 @@ section for information on warnings which will occur when working with models that aider is not familiar with. +## Deepseek + +Aider can connect to the Deepseek API, which is OpenAI compatible. +They appear to grant 5M tokens of free API usage to new accounts. + +``` +pip install aider-chat +export OPENAI_API_KEY= +export OPENAI_API_BASE=https://api.deepseek.com/v1 +aider --model openai/deepseek-coder +``` + +See the [model warnings](#model-warnings) +section for information on warnings which will occur +when working with models that aider is not familiar with. + + ## OpenAI compatible APIs Aider can connect to any LLM which is accessible via an OpenAI compatible API endpoint. From e88ea7793bc931cc22b910697fddb27f93a82793 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 12:56:41 -0700 Subject: [PATCH 80/82] copy --- .github/ISSUE_TEMPLATE/issue.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue.yml b/.github/ISSUE_TEMPLATE/issue.yml index 097fbc102..4795b2831 100644 --- a/.github/ISSUE_TEMPLATE/issue.yml +++ b/.github/ISSUE_TEMPLATE/issue.yml @@ -13,9 +13,9 @@ body: label: Version and model info description: Please include aider version, model being used (`gpt-4-xxx`, etc) and any other switches or config settings that are active. placeholder: | - Aider v0.21.2-dev - Model: gpt-4-0613 using diff edit format - Git repo: .git with 134 files - Repo-map: using 1024 tokens + Aider v0.XX.Y + Model: gpt-N-... using ???? edit format + Git repo: .git with ### files + Repo-map: using #### tokens validations: required: false \ No newline at end of file From 178af4a327f457946345f0a38be68d410b03f129 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 13:17:40 -0700 Subject: [PATCH 81/82] copy --- docs/llms.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/llms.md b/docs/llms.md index 2a0ac3f4c..f965ce251 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -49,6 +49,7 @@ Models weaker than GPT 3.5 may have problems working well with aider. - [Other LLMs](#other-llms) - [Model warnings](#model-warnings) - [Editing format](#editing-format) +- [Using a .env file](#using-a-env-file) Aider uses the LiteLLM package to connect to LLM providers. The [LiteLLM provider docs](https://docs.litellm.ai/docs/providers) @@ -384,3 +385,25 @@ Aider is configured to use the best edit format for the popular OpenAI and Anthr For lesser known models aider will default to using the "whole" editing format. If you would like to experiment with the more advanced formats, you can use these switches: `--edit-format diff` or `--edit-format udiff`. + +# Using a .env file + +Aider will read environment variables from a `.env` file in +the current directory. +You can use it to store various keys and other settings for the +models you use with aider. + +Here is an example `.env` file: + +``` +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +GROQ_API_KEY= +OPENROUTER_API_KEY= + +AZURE_API_KEY= +AZURE_API_VERSION=2023-05-15 +AZURE_API_BASE=https://example-endpoint.openai.azure.com + +OLLAMA_API_BASE=http://127.0.0.1:11434 +``` \ No newline at end of file From d6783636c5a7694b09ddee8caa859c20b4382272 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Mon, 29 Apr 2024 20:42:44 -0700 Subject: [PATCH 82/82] copy --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 2b3147563..470b7d7a6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -139,7 +139,7 @@ See the [Coder.create() and Coder.__init__() methods](https://github.com/paul-gauthier/aider/blob/main/aider/coders/base_coder.py) for all the supported arguments. -It can also helpful to set the equivalend of `--yes` by doing this: +It can also be helpful to set the equivalend of `--yes` by doing this: ``` from aider.io import InputOutput