diff options
22 files changed, 262 insertions, 66 deletions
@@ -19,26 +19,26 @@  ## Task, not tab, auto-complete -### Edit in natural language -  ### Get possible explainations  Ask Continue about a part of your code to get another perspective -- `what might cause this error?` -- `what is the load_dotenv library name?` -- `how do I find running process on port 8000?` +- “how can I set up a Prisma schema that cascades deletes?” +- “where in the page should I be making this request to the backend?” +- “how can I communicate between these iframes?” + +### Edit in natural language  Highlight a section of code and instruct Continue to refactor it -- `/edit Make this use more descriptive variable names` -- `/edit Rewrite this API call to grab all pages` -- `/edit Use 'Union' instead of a vertical bar here` +- “/edit migrate this digital ocean terraform file into one that works for GCP” +- “/edit change this plot into a bar chart in this dashboard component” +- “/edit rewrite this function to be async”  ### Generate files from scratch  Let Continue build the scaffolding of Python scripts, React components, and more -- `Create a shell script to back up my home dir to /tmp/` -- `Write a Python script to get Posthog events` -- `Add a React component for syntax highlighted code` +- “/edit here is a connector for postgres, now write one for kafka” +- “/edit make an IAM policy that creates a user with read-only access to S3” +- “/edit use this schema to write me a SQL query that gets recently churned users”  ## Getting Started diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 5c3baafd..615e7657 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -20,6 +20,7 @@ import asyncio  from ..libs.util.step_name_to_steps import get_step_from_name  from ..libs.util.traceback_parsers import get_python_traceback, get_javascript_traceback  from openai import error as openai_errors +from ..libs.util.create_async_task import create_async_task  def get_error_title(e: Exception) -> str: @@ -341,7 +342,8 @@ class Autopilot(ContinueBaseModel):              # Update subscribers with new description              await self.update_subscribers() -        asyncio.create_task(update_description()) +        create_async_task(update_description(), +                          self.continue_sdk.ide.unique_id)          return observation diff --git a/continuedev/src/continuedev/libs/util/create_async_task.py b/continuedev/src/continuedev/libs/util/create_async_task.py new file mode 100644 index 00000000..608d4977 --- /dev/null +++ b/continuedev/src/continuedev/libs/util/create_async_task.py @@ -0,0 +1,23 @@ +from typing import Coroutine, Union +import traceback +from .telemetry import capture_event +import asyncio +import nest_asyncio +nest_asyncio.apply() + + +def create_async_task(coro: Coroutine, unique_id: Union[str, None] = None): +    """asyncio.create_task and log errors by adding a callback""" +    task = asyncio.create_task(coro) + +    def callback(future: asyncio.Future): +        try: +            future.result() +        except Exception as e: +            print("Exception caught from async task: ", e) +            capture_event(unique_id or "None", "async_task_error", { +                "error_title": e.__str__() or e.__repr__(), "error_message": traceback.format_tb(e.__traceback__) +            }) + +    task.add_done_callback(callback) +    return task diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py index 8e9b1fb9..ae53be00 100644 --- a/continuedev/src/continuedev/server/gui.py +++ b/continuedev/src/continuedev/server/gui.py @@ -2,14 +2,14 @@ import json  from fastapi import Depends, Header, WebSocket, APIRouter  from typing import Any, List, Type, TypeVar, Union  from pydantic import BaseModel +import traceback  from uvicorn.main import Server  from .session_manager import SessionManager, session_manager, Session  from .gui_protocol import AbstractGUIProtocolServer  from ..libs.util.queue import AsyncSubscriptionQueue -import asyncio -import nest_asyncio -nest_asyncio.apply() +from ..libs.util.telemetry import capture_event +from ..libs.util.create_async_task import create_async_task  router = APIRouter(prefix="/gui", tags=["gui"]) @@ -102,51 +102,60 @@ class GUIProtocolServer(AbstractGUIProtocolServer):      def on_main_input(self, input: str):          # Do something with user input -        asyncio.create_task(self.session.autopilot.accept_user_input(input)) +        create_async_task(self.session.autopilot.accept_user_input( +            input), self.session.autopilot.continue_sdk.ide.unique_id)      def on_reverse_to_index(self, index: int):          # Reverse the history to the given index -        asyncio.create_task(self.session.autopilot.reverse_to_index(index)) +        create_async_task(self.session.autopilot.reverse_to_index( +            index), self.session.autopilot.continue_sdk.ide.unique_id)      def on_step_user_input(self, input: str, index: int): -        asyncio.create_task( -            self.session.autopilot.give_user_input(input, index)) +        create_async_task( +            self.session.autopilot.give_user_input(input, index), self.session.autopilot.continue_sdk.ide.unique_id)      def on_refinement_input(self, input: str, index: int): -        asyncio.create_task( -            self.session.autopilot.accept_refinement_input(input, index)) +        create_async_task( +            self.session.autopilot.accept_refinement_input(input, index), self.session.autopilot.continue_sdk.ide.unique_id)      def on_retry_at_index(self, index: int): -        asyncio.create_task( -            self.session.autopilot.retry_at_index(index)) +        create_async_task( +            self.session.autopilot.retry_at_index(index), self.session.autopilot.continue_sdk.ide.unique_id)      def on_change_default_model(self, model: str): -        asyncio.create_task(self.session.autopilot.change_default_model(model)) +        create_async_task(self.session.autopilot.change_default_model( +            model), self.session.autopilot.continue_sdk.ide.unique_id)      def on_clear_history(self): -        asyncio.create_task(self.session.autopilot.clear_history()) +        create_async_task(self.session.autopilot.clear_history( +        ), self.session.autopilot.continue_sdk.ide.unique_id)      def on_delete_at_index(self, index: int): -        asyncio.create_task(self.session.autopilot.delete_at_index(index)) +        create_async_task(self.session.autopilot.delete_at_index( +            index), self.session.autopilot.continue_sdk.ide.unique_id)      def on_delete_context_at_indices(self, indices: List[int]): -        asyncio.create_task( -            self.session.autopilot.delete_context_at_indices(indices) +        create_async_task( +            self.session.autopilot.delete_context_at_indices( +                indices), self.session.autopilot.continue_sdk.ide.unique_id          )      def on_toggle_adding_highlighted_code(self): -        asyncio.create_task( -            self.session.autopilot.toggle_adding_highlighted_code() +        create_async_task( +            self.session.autopilot.toggle_adding_highlighted_code( +            ), self.session.autopilot.continue_sdk.ide.unique_id          )      def on_set_editing_at_indices(self, indices: List[int]): -        asyncio.create_task( -            self.session.autopilot.set_editing_at_indices(indices) +        create_async_task( +            self.session.autopilot.set_editing_at_indices( +                indices), self.session.autopilot.continue_sdk.ide.unique_id          )      def on_set_pinned_at_indices(self, indices: List[int]): -        asyncio.create_task( -            self.session.autopilot.set_pinned_at_indices(indices) +        create_async_task( +            self.session.autopilot.set_pinned_at_indices( +                indices), self.session.autopilot.continue_sdk.ide.unique_id          ) @@ -179,6 +188,8 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we      except Exception as e:          print("ERROR in gui websocket: ", e) +        capture_event(session.autopilot.continue_sdk.ide.unique_id, "gui_error", { +                      "error_title": e.__str__() or e.__repr__(), "error_message": traceback.format_tb(e.__traceback__)})          raise e      finally:          print("Closing gui websocket") diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index e4a6266a..93996edd 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Type, TypeVar, Union  import uuid  from fastapi import WebSocket, Body, APIRouter  from uvicorn.main import Server +import traceback  from ..libs.util.telemetry import capture_event  from ..libs.util.queue import AsyncSubscriptionQueue @@ -15,8 +16,7 @@ from pydantic import BaseModel  from .gui import SessionManager, session_manager  from .ide_protocol import AbstractIdeProtocolServer  import asyncio -import nest_asyncio -nest_asyncio.apply() +from ..libs.util.create_async_task import create_async_task  router = APIRouter(prefix="/ide", tags=["ide"]) @@ -250,24 +250,25 @@ class IdeProtocolServer(AbstractIdeProtocolServer):      def onDeleteAtIndex(self, index: int):          for _, session in self.session_manager.sessions.items(): -            asyncio.create_task(session.autopilot.delete_at_index(index)) +            create_async_task( +                session.autopilot.delete_at_index(index), self.unique_id)      def onCommandOutput(self, output: str):          # Send the output to ALL autopilots.          # Maybe not ideal behavior          for _, session in self.session_manager.sessions.items(): -            asyncio.create_task( -                session.autopilot.handle_command_output(output)) +            create_async_task( +                session.autopilot.handle_command_output(output), self.unique_id)      def onHighlightedCodeUpdate(self, range_in_files: List[RangeInFileWithContents]):          for _, session in self.session_manager.sessions.items(): -            asyncio.create_task( -                session.autopilot.handle_highlighted_code(range_in_files)) +            create_async_task( +                session.autopilot.handle_highlighted_code(range_in_files), self.unique_id)      def onMainUserInput(self, input: str):          for _, session in self.session_manager.sessions.items(): -            asyncio.create_task( -                session.autopilot.accept_user_input(input)) +            create_async_task( +                session.autopilot.accept_user_input(input), self.unique_id)      # Request information. Session doesn't matter.      async def getOpenFiles(self) -> List[str]: @@ -412,5 +413,7 @@ async def websocket_endpoint(websocket: WebSocket):          await websocket.close()      except Exception as e:          print("Error in ide websocket: ", e) +        capture_event(ideProtocolServer.unique_id, "gui_error", { +                      "error_title": e.__str__() or e.__repr__(), "error_message": traceback.format_tb(e.__traceback__)})          await websocket.close()          raise e diff --git a/continuedev/src/continuedev/server/session_manager.py b/continuedev/src/continuedev/server/session_manager.py index 99a38146..873a379e 100644 --- a/continuedev/src/continuedev/server/session_manager.py +++ b/continuedev/src/continuedev/server/session_manager.py @@ -1,3 +1,4 @@ +from asyncio import BaseEventLoop  from fastapi import WebSocket  from typing import Any, Dict, List, Union  from uuid import uuid4 @@ -7,9 +8,7 @@ from ..core.policy import DemoPolicy  from ..core.main import FullState  from ..core.autopilot import Autopilot  from .ide_protocol import AbstractIdeProtocolServer -import asyncio -import nest_asyncio -nest_asyncio.apply() +from ..libs.util.create_async_task import create_async_task  class Session: @@ -38,7 +37,7 @@ class DemoAutopilot(Autopilot):  class SessionManager:      sessions: Dict[str, Session] = {} -    _event_loop: Union[asyncio.BaseEventLoop, None] = None +    _event_loop: Union[BaseEventLoop, None] = None      def get_session(self, session_id: str) -> Session:          if session_id not in self.sessions: @@ -57,7 +56,7 @@ class SessionManager:              })          autopilot.on_update(on_update) -        asyncio.create_task(autopilot.run_policy()) +        create_async_task(autopilot.run_policy())          return session_id      def remove_session(self, session_id: str): diff --git a/continuedev/src/continuedev/steps/search_directory.py b/continuedev/src/continuedev/steps/search_directory.py index 2eecc99c..bfb97630 100644 --- a/continuedev/src/continuedev/steps/search_directory.py +++ b/continuedev/src/continuedev/steps/search_directory.py @@ -6,6 +6,7 @@ from ..models.filesystem import RangeInFile  from ..models.main import Range  from ..core.main import Step  from ..core.sdk import ContinueSDK +from ..libs.util.create_async_task import create_async_task  import os  import re @@ -60,9 +61,9 @@ class EditAllMatchesStep(Step):          # Search all files for a given string          range_in_files = find_all_matches_in_dir(self.pattern, self.directory or await sdk.ide.getWorkspaceDirectory()) -        tasks = [asyncio.create_task(sdk.edit_file( +        tasks = [create_async_task(sdk.edit_file(              range=range_in_file.range,              filename=range_in_file.filepath,              prompt=self.user_request -        )) for range_in_file in range_in_files] +        ), sdk.ide.unique_id) for range_in_file in range_in_files]          await asyncio.gather(*tasks) diff --git a/docs/docs/how-to-use-continue.md b/docs/docs/how-to-use-continue.md index 96c44228..66de7ee9 100644 --- a/docs/docs/how-to-use-continue.md +++ b/docs/docs/how-to-use-continue.md @@ -29,42 +29,82 @@ Here are tasks that Continue excels at helping you complete:  Continue works well in situations where find and replace does not work (i.e. “/edit change all of these to be like that”) +Examples +- "/edit Use 'Union' instead of a vertical bar here" +- “/edit Make this use more descriptive variable names” +  ### Writing files from scratch  Continue can help you get started building React components, Python scripts, Shell scripts, Makefiles, Unit tests, etc. +Examples +- “/edit write a python script to get Posthog events" +- “/edit add a React component for syntax highlighted code" +  ### Creating projects from scratch  Continue can go even further. For example, it can help build the scaffolding for a Python package, which includes a typer cli app to sort the arguments and print them back out. +Examples +- “/edit use this schema to write me a SQL query that gets recently churned users” +- “/edit create a shell script to back up my home dir to /tmp/" +  ### Fix highlighted code  After selecting a code section, try to refactor it with Continue (e.g “/edit change the function to work like this”, “/edit do this everywhere”) +Examples +- “/edit migrate this digital ocean terraform file into one that works for GCP” +- “/edit rewrite this function to be async” +  ### Ask about highlighted code or an entire file  If you don't understand how some code works, highlight it and ask "how does this code work?" +Examples +- “where in the page should I be making this request to the backend?” +- “how can I communicate between these iframes?” +  ### Ask about errors  Continue can also help explain errors and offer possible solutions. You will need to copy and paste the error text into the text input though. +Examples +- “explain this error to me in human understandable way” +- "what are some ideas for how I might solve this problem?" +  ### Figure out what shell command to run  Instead of switching windows and getting distracted, you can ask things like "How do I find running process on port 8000?" +Examples +- "what is the load_dotenv library name?" +- "how do I find running process on port 8000?" +  ### Ask single-turn open-ended questions  Instead of leaving your IDE, you can ask open-ended questions that you don't expect to turn into multi-turn conversations. +Examples +- “how can I set up a Prisma schema that cascades deletes?” +- "what is the difference between dense and sparse embeddings?" +  ### Editing small existing files  You can highlight an entire file and ask Continue to improve it as long as the file is not too large. +Examples +- “/edit here is a connector for postgres, now write one for kafka” +- "/edit Rewrite this API call to grab all pages" +  ### Tasks with a few steps  There are many more tasks that Continue can help you complete. Typically, these will be tasks that don't involve too many steps to complete. +Examples +- “/edit make an IAM policy that creates a user with read-only access to S3” +- “/edit change this plot into a bar chart in this dashboard component” +  ## When to not use Continue  Here are tasks that Continue is **not** helpful with today: Binary files differdiff --git a/extension/README.md b/extension/README.md index b57aedb7..2d449b92 100644 --- a/extension/README.md +++ b/extension/README.md @@ -7,23 +7,23 @@  ### Get possible explainations  Ask Continue about a part of your code to get another perspective -- `what might cause this error?` -- `what is the load_dotenv library name?` -- `how do I find running process on port 8000?` +- “how can I set up a Prisma schema that cascades deletes?” +- “where in the page should I be making this request to the backend?” +- “how can I communicate between these iframes?”  ### Edit in natural language  Highlight a section of code and instruct Continue to refactor it -- `/edit Make this use more descriptive variable names` -- `/edit Rewrite this API call to grab all pages` -- `/edit Use 'Union' instead of a vertical bar here` +- “/edit migrate this digital ocean terraform file into one that works for GCP” +- “/edit change this plot into a bar chart in this dashboard component” +- “/edit rewrite this function to be async”  ### Generate files from scratch  Let Continue build the scaffolding of Python scripts, React components, and more -- `Create a shell script to back up my home dir to /tmp/` -- `Write Python in a new file to get Posthog events` -- `Add a React component for syntax highlighted code` +- “/edit here is a connector for postgres, now write one for kafka” +- “/edit make an IAM policy that creates a user with read-only access to S3” +- “/edit use this schema to write me a SQL query that gets recently churned users”  ## OpenAI API Key diff --git a/extension/media/edit.png b/extension/media/edit.png Binary files differindex 5e77c0ea..f4ca623c 100644 --- a/extension/media/edit.png +++ b/extension/media/edit.png diff --git a/extension/media/explain.png b/extension/media/explain.png Binary files differindex 196ab914..79e8ccc9 100644 --- a/extension/media/explain.png +++ b/extension/media/explain.png diff --git a/extension/media/generate.png b/extension/media/generate.png Binary files differindex 9d84e4ae..c16d9f9f 100644 --- a/extension/media/generate.png +++ b/extension/media/generate.png diff --git a/extension/package-lock.json b/extension/package-lock.json index a2ac0a04..71f4d974 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@  {    "name": "continue", -  "version": "0.0.145", +  "version": "0.0.147",    "lockfileVersion": 2,    "requires": true,    "packages": {      "": {        "name": "continue", -      "version": "0.0.145", +      "version": "0.0.147",        "license": "Apache-2.0",        "dependencies": {          "@electron/rebuild": "^3.2.10", diff --git a/extension/package.json b/extension/package.json index 0464ba55..d9f155df 100644 --- a/extension/package.json +++ b/extension/package.json @@ -14,7 +14,7 @@    "displayName": "Continue",    "pricing": "Free",    "description": "The open-source coding autopilot", -  "version": "0.0.145", +  "version": "0.0.147",    "publisher": "Continue",    "engines": {      "vscode": "^1.67.0" @@ -181,7 +181,7 @@            {              "id": "edit",              "title": "Edit in natural language", -            "description": "Highlight a section of code and instruct Continue to refactor it (e.g. `/edit Make this use more descriptive variable names`)", +            "description": "Highlight a section of code and instruct Continue to refactor it (e.g. `/edit rewrite this function to be async`)",              "media": {                "image": "media/edit.png",                "altText": "Empty image" @@ -191,7 +191,7 @@            {              "id": "explain",              "title": "Get possible explanations", -            "description": "Ask Continue about a part of your code to get another perspective (e.g. `how do I find running process on port 8000?`)", +            "description": "Ask Continue about a part of your code to get another perspective (e.g. `where in the page should I be making this request to the backend?`)",              "media": {                "image": "media/explain.png",                "altText": "Empty image" @@ -201,7 +201,7 @@            {              "id": "generate",              "title": "Generate files from scratch", -            "description": "Let Continue build the scaffolding of Python scripts, React components, and more (e.g. `Create a shell script to back up my home dir to /tmp/`)", +            "description": "Let Continue build the scaffolding of Python scripts, React components, and more (e.g. `/edit here is a connector for postgres, now write one for kafka`)",              "media": {                "image": "media/generate.png",                "altText": "Empty image" diff --git a/extension/react-app/src/components/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx new file mode 100644 index 00000000..061faba5 --- /dev/null +++ b/extension/react-app/src/components/Onboarding.tsx @@ -0,0 +1,115 @@ +import { useSelector } from "react-redux"; +import { RootStore } from "../redux/store"; +import React, { useState, useEffect } from "react"; +import styled from "styled-components"; +import { ArrowLeft, ArrowRight } from "@styled-icons/heroicons-outline"; +import { defaultBorderRadius } from "."; + +const StyledDiv = styled.div` +  position: absolute; +  top: 0; +  left: 0; +  width: 100%; +  height: 100%; +  background-color: #1e1e1e; +  z-index: 200; +`; + +const StyledSpan = styled.span` +  padding: 8px; +  border-radius: ${defaultBorderRadius}; +  &:hover { +    background-color: #ffffff33; +  } +`; + +const Onboarding = () => { +  const [counter, setCounter] = useState(4); +  const gifs = ["intro", "explain", "edit", "generate"]; +  const topMessages = [ +    "Welcome to Continue!", +    "Answer coding questions", +    "Edit in natural language", +    "Generate files from scratch", +  ]; +  const bottomMessages = [ +    "", +    "Ask Continue about a part of your code to get another perspective", +    "Highlight a section of code and instruct Continue to refactor it", +    "Let Continue build the scaffolding of Python scripts, React components, and more", +  ]; + +  const vscMediaUrl = useSelector( +    (state: RootStore) => state.config.vscMediaUrl +  ); + +  useEffect(() => { +    const hasVisited = localStorage.getItem("hasVisited"); +    if (hasVisited) { +      setCounter(4); +    } else { +      setCounter(0); +      localStorage.setItem("hasVisited", "true"); +    } +  }, []); + +  return ( +    <StyledDiv hidden={counter >= 4}> +      <div +        style={{ +          display: "grid", +          justifyContent: "center", +          alignItems: "center", +          height: "100%", +          textAlign: "center", +          background: `linear-gradient( +            101.79deg, +            #12887a66 0%, +            #87245c66 32%, +            #e1263766 63%, +            #ffb21566 100% +          )`, +          paddingLeft: "16px", +          paddingRight: "16px", +        }} +      > +        <h1>{topMessages[counter]}</h1> +        <div style={{ display: "flex", justifyContent: "center" }}> +          <img +            src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`} +            alt={topMessages[counter]} +          /> +        </div> +        <p>{bottomMessages[counter]}</p> +        <p +          style={{ +            paddingLeft: "50px", +            paddingRight: "50px", +            paddingBottom: "50px", +            textAlign: "center", +          }} +        > +          <div +            style={{ +              cursor: "pointer", +            }} +          > +            <StyledSpan +              hidden={counter === 0} +              onClick={() => setCounter((prev) => Math.max(prev - 1, 0))} +            > +              <ArrowLeft width="18px" strokeWidth="2px" /> Previous +            </StyledSpan> +            <span hidden={counter === 0}>{" | "}</span> +            <StyledSpan onClick={() => setCounter((prev) => prev + 1)}> +              Click to {counter === 3 || "learn how to"} use Continue{" "} +              <ArrowRight width="18px" strokeWidth="2px" /> +            </StyledSpan> +          </div> +        </p> +      </div> +    </StyledDiv> +  ); +}; + +export default Onboarding; diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx index 646aef50..0e60e05c 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/tabs/gui.tsx @@ -23,6 +23,7 @@ import { RootStore } from "../redux/store";  import LoadingCover from "../components/LoadingCover";  import { postVscMessage } from "../vscode";  import UserInputContainer from "../components/UserInputContainer"; +import Onboarding from "../components/Onboarding";  const TopGUIDiv = styled.div`    overflow: hidden; @@ -267,6 +268,7 @@ function GUI(props: GUIProps) {    // const iterations = useSelector(selectIterations);    return (      <> +      <Onboarding></Onboarding>        <LoadingCover hidden={true} message="Downloading local model..." />        <TextDialog          showDialog={showFeedbackDialog} diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index 679d94ba..304c592b 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -300,7 +300,7 @@ class IdeProtocolClient {      });      const resp = await this.messenger?.sendAndReceive("openGUI", {});      const sessionId = resp.sessionId; -    console.log("New Continue session with ID: ", sessionId); +    // console.log("New Continue session with ID: ", sessionId);      return sessionId;    } diff --git a/media/edit.gif b/media/edit.gif Binary files differnew file mode 100644 index 00000000..6780cdf7 --- /dev/null +++ b/media/edit.gif diff --git a/media/explain.gif b/media/explain.gif Binary files differnew file mode 100644 index 00000000..e74803dc --- /dev/null +++ b/media/explain.gif diff --git a/media/generate.gif b/media/generate.gif Binary files differnew file mode 100644 index 00000000..5c1d112b --- /dev/null +++ b/media/generate.gif diff --git a/media/intro.gif b/media/intro.gif Binary files differnew file mode 100644 index 00000000..f872dc91 --- /dev/null +++ b/media/intro.gif  | 
