From 4d7e72970f770eb49627589fb142c93dfb6fd73b Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Sat, 22 Jul 2023 22:37:13 -0700 Subject: @ feature (very large commit) --- schema/json/ContextItem.json | 76 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 schema/json/ContextItem.json (limited to 'schema/json/ContextItem.json') diff --git a/schema/json/ContextItem.json b/schema/json/ContextItem.json new file mode 100644 index 00000000..35766f11 --- /dev/null +++ b/schema/json/ContextItem.json @@ -0,0 +1,76 @@ +{ + "title": "ContextItem", + "$ref": "#/definitions/src__continuedev__core__context__ContextItem", + "definitions": { + "ContextItemId": { + "title": "ContextItemId", + "description": "A ContextItemId is a unique identifier for a ContextItem.", + "type": "object", + "properties": { + "provider_title": { + "title": "Provider Title", + "type": "string" + }, + "item_id": { + "title": "Item Id", + "type": "string" + } + }, + "required": [ + "provider_title", + "item_id" + ] + }, + "ContextItemDescription": { + "title": "ContextItemDescription", + "description": "A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.\n\nThe id can be used to retrieve the ContextItem from the ContextManager.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "id": { + "$ref": "#/definitions/ContextItemId" + } + }, + "required": [ + "name", + "description", + "id" + ] + }, + "src__continuedev__core__context__ContextItem": { + "title": "ContextItem", + "description": "A ContextItem is a single item that is stored in the ContextManager.", + "type": "object", + "properties": { + "description": { + "$ref": "#/definitions/ContextItemDescription" + }, + "content": { + "title": "Content", + "type": "string" + }, + "editing": { + "title": "Editing", + "default": false, + "type": "boolean" + }, + "editable": { + "title": "Editable", + "default": false, + "type": "boolean" + } + }, + "required": [ + "description", + "content" + ] + } + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 8c7ad065810a867c70d5a948bb54d94c44b6090a Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Mon, 24 Jul 2023 14:59:43 -0700 Subject: fixes from merge, images --- continuedev/src/continuedev/core/autopilot.py | 6 +- .../context_providers/file_context_provider.py | 54 ------ .../highlighted_code_context_provider.py | 191 --------------------- .../context_providers/file_context_provider.py | 55 ++++++ .../highlighted_code_context_provider.py | 191 +++++++++++++++++++++ .../continuedev/plugins/steps/custom_command.py | 2 +- extension/media/continue-dev-square.png | Bin 0 -> 36355 bytes extension/media/terminal-continue.png | Bin 0 -> 36669 bytes extension/package.json | 4 +- schema/json/ContextItem.json | 4 +- 10 files changed, 254 insertions(+), 253 deletions(-) delete mode 100644 continuedev/src/continuedev/libs/context_providers/file_context_provider.py delete mode 100644 continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py create mode 100644 continuedev/src/continuedev/plugins/context_providers/file_context_provider.py create mode 100644 continuedev/src/continuedev/plugins/context_providers/highlighted_code_context_provider.py create mode 100644 extension/media/continue-dev-square.png create mode 100644 extension/media/terminal-continue.png (limited to 'schema/json/ContextItem.json') diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 003962c6..2ce7c1f9 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -10,12 +10,12 @@ from ..models.filesystem import RangeInFileWithContents from ..models.filesystem_edit import FileEditWithFullContents from .observation import Observation, InternalErrorObservation from .context import ContextManager -from ..libs.context_providers.file_context_provider import FileContextProvider -from ..libs.context_providers.highlighted_code_context_provider import HighlightedCodeContextProvider +from ..plugins.context_providers.file_context_provider import FileContextProvider +from ..plugins.context_providers.highlighted_code_context_provider import HighlightedCodeContextProvider from ..server.ide_protocol import AbstractIdeProtocolServer from ..libs.util.queue import AsyncSubscriptionQueue from ..models.main import ContinueBaseModel -from .main import Context, ContinueCustomException, HighlightedRangeContext, Policy, History, FullState, Step, HistoryNode +from .main import Context, ContinueCustomException, Policy, History, FullState, Step, HistoryNode from ..plugins.steps.core.core import ReversibleStep, ManualEditStep, UserInputStep from ..libs.util.telemetry import capture_event from .sdk import ContinueSDK diff --git a/continuedev/src/continuedev/libs/context_providers/file_context_provider.py b/continuedev/src/continuedev/libs/context_providers/file_context_provider.py deleted file mode 100644 index c3ec351f..00000000 --- a/continuedev/src/continuedev/libs/context_providers/file_context_provider.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -from typing import List -from ...core.main import ContextItem, ContextItemDescription, ContextItemId -from ...core.context import ContextProvider -from fnmatch import fnmatch - - -def get_file_contents(filepath: str) -> str: - with open(filepath, "r") as f: - return f.read() - - -class FileContextProvider(ContextProvider): - """ - The FileContextProvider is a ContextProvider that allows you to search files in the open workspace. - """ - - title = "file" - workspace_dir: str - ignore_patterns: List[str] = list(map(lambda folder: f"**/{folder}", [ - ".git", - ".vscode", - ".idea", - ".vs", - ".venv", - "env", - ".env", - "node_modules", - "dist", - "build", - "target", - "out", - "bin", - ])) - - async def provide_context_items(self) -> List[ContextItem]: - filepaths = [] - for root, dir_names, file_names in os.walk(self.workspace_dir): - dir_names[:] = [d for d in dir_names if not any( - fnmatch(d, pattern) for pattern in self.ignore_patterns)] - for file_name in file_names: - filepaths.append(os.path.join(root, file_name)) - - return [ContextItem( - content=get_file_contents(file), - description=ContextItemDescription( - name=f"File {os.path.basename(file)}", - description=file, - id=ContextItemId( - provider_title=self.title, - item_id=file - ) - ) - ) for file in filepaths] diff --git a/continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py b/continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py deleted file mode 100644 index 23d4fc86..00000000 --- a/continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py +++ /dev/null @@ -1,191 +0,0 @@ -import os -from typing import Any, Dict, List - -import meilisearch -from ...core.main import ChatMessage -from ...models.filesystem import RangeInFile, RangeInFileWithContents -from ...core.context import ContextItem, ContextItemDescription, ContextItemId -from pydantic import BaseModel - - -class HighlightedRangeContextItem(BaseModel): - rif: RangeInFileWithContents - item: ContextItem - - -class HighlightedCodeContextProvider(BaseModel): - """ - The ContextProvider class is a plugin that lets you provide new information to the LLM by typing '@'. - When you type '@', the context provider will be asked to populate a list of options. - These options will be updated on each keystroke. - When you hit enter on an option, the context provider will add that item to the autopilot's list of context (which is all stored in the ContextManager object). - """ - - title = "code" - - ide: Any # IdeProtocolServer - - highlighted_ranges: List[HighlightedRangeContextItem] = [] - adding_highlighted_code: bool = False - - should_get_fallback_context_item: bool = True - last_added_fallback: bool = False - - async def _get_fallback_context_item(self) -> HighlightedRangeContextItem: - if not self.should_get_fallback_context_item: - return None - - visible_files = await self.ide.getVisibleFiles() - if len(visible_files) > 0: - content = await self.ide.readFile(visible_files[0]) - rif = RangeInFileWithContents.from_entire_file( - visible_files[0], content) - - item = self._rif_to_context_item(rif, 0, True) - item.description.name = self._rif_to_name( - rif, show_line_nums=False) - - self.last_added_fallback = True - return HighlightedRangeContextItem(rif=rif, item=item) - - return None - - async def get_selected_items(self) -> List[ContextItem]: - items = [hr.item for hr in self.highlighted_ranges] - - if len(items) == 0 and (fallback_item := await self._get_fallback_context_item()): - items = [fallback_item.item] - - return items - - async def get_chat_messages(self) -> List[ContextItem]: - ranges = self.highlighted_ranges - if len(ranges) == 0 and (fallback_item := await self._get_fallback_context_item()): - ranges = [fallback_item] - - return [ChatMessage( - role="user", - content=f"Code in this file is highlighted ({r.rif.filepath}):\n```\n{r.rif.contents}\n```", - summary=f"Code in this file is highlighted: {r.rif.filepath}" - ) for r in ranges] - - def _make_sure_is_editing_range(self): - """If none of the highlighted ranges are currently being edited, the first should be selected""" - if len(self.highlighted_ranges) == 0: - return - if not any(map(lambda x: x.item.editing, self.highlighted_ranges)): - self.highlighted_ranges[0].item.editing = True - - def _disambiguate_highlighted_ranges(self): - """If any files have the same name, also display their folder name""" - name_status: Dict[str, set] = { - } # basename -> set of full paths with that basename - for hr in self.highlighted_ranges: - basename = os.path.basename(hr.rif.filepath) - if basename in name_status: - name_status[basename].add(hr.rif.filepath) - else: - name_status[basename] = {hr.rif.filepath} - - for hr in self.highlighted_ranges: - if len(name_status[basename]) > 1: - hr.item.description.name = self._rif_to_name(hr.rif, display_filename=os.path.join( - os.path.basename(os.path.dirname(hr.rif.filepath)), basename)) - else: - hr.item.description.name = self._rif_to_name( - hr.rif, display_filename=basename) - - async def provide_context_items(self) -> List[ContextItem]: - return [] - - async def delete_context_with_ids(self, ids: List[ContextItemId]) -> List[ContextItem]: - indices_to_delete = [ - int(id.item_id) for id in ids - ] - - kept_ranges = [] - for i, hr in enumerate(self.highlighted_ranges): - if i not in indices_to_delete: - kept_ranges.append(hr) - self.highlighted_ranges = kept_ranges - - self._make_sure_is_editing_range() - - if len(self.highlighted_ranges) == 0 and self.last_added_fallback: - self.should_get_fallback_context_item = False - - return [hr.item for hr in self.highlighted_ranges] - - def _rif_to_name(self, rif: RangeInFileWithContents, display_filename: str = None, show_line_nums: bool = True) -> str: - line_nums = f" ({rif.range.start.line + 1}-{rif.range.end.line + 1})" if show_line_nums else "" - return f"{display_filename or os.path.basename(rif.filepath)}{line_nums}" - - def _rif_to_context_item(self, rif: RangeInFileWithContents, idx: int, editing: bool) -> ContextItem: - return ContextItem( - description=ContextItemDescription( - name=self._rif_to_name(rif), - description=rif.filepath, - id=ContextItemId( - provider_title=self.title, - item_id=str(idx) - ) - ), - content=rif.contents, - editing=editing, - editable=True - ) - - async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]): - self.should_get_fallback_context_item = True - self.last_added_fallback = False - - # Filter out rifs from ~/.continue/diffs folder - range_in_files = [ - rif for rif in range_in_files if not os.path.dirname(rif.filepath) == os.path.expanduser("~/.continue/diffs")] - - # If not adding highlighted code - if not self.adding_highlighted_code: - if len(self.highlighted_ranges) == 1 and len(range_in_files) <= 1 and (len(range_in_files) == 0 or range_in_files[0].range.start == range_in_files[0].range.end): - # If un-highlighting the range to edit, then remove the range - self.highlighted_ranges = [] - elif len(range_in_files) > 0: - # Otherwise, replace the current range with the new one - # This is the first range to be highlighted - self.highlighted_ranges = [ - HighlightedRangeContextItem( - rif=range_in_files[0], - item=self._rif_to_context_item(range_in_files[0], 0, True))] - - return - - # If current range overlaps with any others, delete them and only keep the new range - new_ranges = [] - for i, hr in enumerate(self.highlighted_ranges): - found_overlap = False - for new_rif in range_in_files: - if hr.rif.filepath == new_rif.filepath and hr.rif.range.overlaps_with(new_rif.range): - found_overlap = True - break - - # Also don't allow multiple ranges in same file with same content. This is useless to the model, and avoids - # the bug where cmd+f causes repeated highlights - if hr.rif.filepath == new_rif.filepath and hr.rif.contents == new_rif.contents: - found_overlap = True - break - - if not found_overlap: - new_ranges.append(HighlightedRangeContextItem(rif=hr.rif, item=self._rif_to_context_item( - hr.rif, len(new_ranges), False))) - - self.highlighted_ranges = new_ranges + [HighlightedRangeContextItem(rif=rif, item=self._rif_to_context_item( - rif, len(new_ranges) + idx, False)) for idx, rif in enumerate(range_in_files)] - - self._make_sure_is_editing_range() - self._disambiguate_highlighted_ranges() - - async def set_editing_at_ids(self, ids: List[str]): - for hr in self.highlighted_ranges: - hr.item.editing = hr.item.description.id.to_string() in ids - - async def add_context_item(self, id: ContextItemId, query: str, search_client: meilisearch.Client, prev: List[ContextItem] = None) -> List[ContextItem]: - raise NotImplementedError() diff --git a/continuedev/src/continuedev/plugins/context_providers/file_context_provider.py b/continuedev/src/continuedev/plugins/context_providers/file_context_provider.py new file mode 100644 index 00000000..854310b1 --- /dev/null +++ b/continuedev/src/continuedev/plugins/context_providers/file_context_provider.py @@ -0,0 +1,55 @@ +import os +from typing import List +from ...core.main import ContextItem, ContextItemDescription, ContextItemId +from ...core.context import ContextProvider +from fnmatch import fnmatch + + +def get_file_contents(filepath: str) -> str: + with open(filepath, "r") as f: + return f.read() + + +class FileContextProvider(ContextProvider): + """ + The FileContextProvider is a ContextProvider that allows you to search files in the open workspace. + """ + + title = "file" + workspace_dir: str + ignore_patterns: List[str] = list(map(lambda folder: f"**/{folder}", [ + ".git", + ".vscode", + ".idea", + ".vs", + ".venv", + "env", + ".env", + "node_modules", + "dist", + "build", + "target", + "out", + "bin", + ])) + + async def provide_context_items(self) -> List[ContextItem]: + filepaths = [] + for root, dir_names, file_names in os.walk(self.workspace_dir): + dir_names[:] = [d for d in dir_names if not any( + fnmatch(d, pattern) for pattern in self.ignore_patterns)] + for file_name in file_names: + filepaths.append(os.path.join(root, file_name)) + + return [ContextItem( + content=get_file_contents(file)[:min( + 2000, len(get_file_contents(file)))], + description=ContextItemDescription( + name=f"File {os.path.basename(file)}", + description=file, + id=ContextItemId( + provider_title=self.title, + item_id=file + ) + ) + ) for file in filepaths] diff --git a/continuedev/src/continuedev/plugins/context_providers/highlighted_code_context_provider.py b/continuedev/src/continuedev/plugins/context_providers/highlighted_code_context_provider.py new file mode 100644 index 00000000..23d4fc86 --- /dev/null +++ b/continuedev/src/continuedev/plugins/context_providers/highlighted_code_context_provider.py @@ -0,0 +1,191 @@ +import os +from typing import Any, Dict, List + +import meilisearch +from ...core.main import ChatMessage +from ...models.filesystem import RangeInFile, RangeInFileWithContents +from ...core.context import ContextItem, ContextItemDescription, ContextItemId +from pydantic import BaseModel + + +class HighlightedRangeContextItem(BaseModel): + rif: RangeInFileWithContents + item: ContextItem + + +class HighlightedCodeContextProvider(BaseModel): + """ + The ContextProvider class is a plugin that lets you provide new information to the LLM by typing '@'. + When you type '@', the context provider will be asked to populate a list of options. + These options will be updated on each keystroke. + When you hit enter on an option, the context provider will add that item to the autopilot's list of context (which is all stored in the ContextManager object). + """ + + title = "code" + + ide: Any # IdeProtocolServer + + highlighted_ranges: List[HighlightedRangeContextItem] = [] + adding_highlighted_code: bool = False + + should_get_fallback_context_item: bool = True + last_added_fallback: bool = False + + async def _get_fallback_context_item(self) -> HighlightedRangeContextItem: + if not self.should_get_fallback_context_item: + return None + + visible_files = await self.ide.getVisibleFiles() + if len(visible_files) > 0: + content = await self.ide.readFile(visible_files[0]) + rif = RangeInFileWithContents.from_entire_file( + visible_files[0], content) + + item = self._rif_to_context_item(rif, 0, True) + item.description.name = self._rif_to_name( + rif, show_line_nums=False) + + self.last_added_fallback = True + return HighlightedRangeContextItem(rif=rif, item=item) + + return None + + async def get_selected_items(self) -> List[ContextItem]: + items = [hr.item for hr in self.highlighted_ranges] + + if len(items) == 0 and (fallback_item := await self._get_fallback_context_item()): + items = [fallback_item.item] + + return items + + async def get_chat_messages(self) -> List[ContextItem]: + ranges = self.highlighted_ranges + if len(ranges) == 0 and (fallback_item := await self._get_fallback_context_item()): + ranges = [fallback_item] + + return [ChatMessage( + role="user", + content=f"Code in this file is highlighted ({r.rif.filepath}):\n```\n{r.rif.contents}\n```", + summary=f"Code in this file is highlighted: {r.rif.filepath}" + ) for r in ranges] + + def _make_sure_is_editing_range(self): + """If none of the highlighted ranges are currently being edited, the first should be selected""" + if len(self.highlighted_ranges) == 0: + return + if not any(map(lambda x: x.item.editing, self.highlighted_ranges)): + self.highlighted_ranges[0].item.editing = True + + def _disambiguate_highlighted_ranges(self): + """If any files have the same name, also display their folder name""" + name_status: Dict[str, set] = { + } # basename -> set of full paths with that basename + for hr in self.highlighted_ranges: + basename = os.path.basename(hr.rif.filepath) + if basename in name_status: + name_status[basename].add(hr.rif.filepath) + else: + name_status[basename] = {hr.rif.filepath} + + for hr in self.highlighted_ranges: + if len(name_status[basename]) > 1: + hr.item.description.name = self._rif_to_name(hr.rif, display_filename=os.path.join( + os.path.basename(os.path.dirname(hr.rif.filepath)), basename)) + else: + hr.item.description.name = self._rif_to_name( + hr.rif, display_filename=basename) + + async def provide_context_items(self) -> List[ContextItem]: + return [] + + async def delete_context_with_ids(self, ids: List[ContextItemId]) -> List[ContextItem]: + indices_to_delete = [ + int(id.item_id) for id in ids + ] + + kept_ranges = [] + for i, hr in enumerate(self.highlighted_ranges): + if i not in indices_to_delete: + kept_ranges.append(hr) + self.highlighted_ranges = kept_ranges + + self._make_sure_is_editing_range() + + if len(self.highlighted_ranges) == 0 and self.last_added_fallback: + self.should_get_fallback_context_item = False + + return [hr.item for hr in self.highlighted_ranges] + + def _rif_to_name(self, rif: RangeInFileWithContents, display_filename: str = None, show_line_nums: bool = True) -> str: + line_nums = f" ({rif.range.start.line + 1}-{rif.range.end.line + 1})" if show_line_nums else "" + return f"{display_filename or os.path.basename(rif.filepath)}{line_nums}" + + def _rif_to_context_item(self, rif: RangeInFileWithContents, idx: int, editing: bool) -> ContextItem: + return ContextItem( + description=ContextItemDescription( + name=self._rif_to_name(rif), + description=rif.filepath, + id=ContextItemId( + provider_title=self.title, + item_id=str(idx) + ) + ), + content=rif.contents, + editing=editing, + editable=True + ) + + async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]): + self.should_get_fallback_context_item = True + self.last_added_fallback = False + + # Filter out rifs from ~/.continue/diffs folder + range_in_files = [ + rif for rif in range_in_files if not os.path.dirname(rif.filepath) == os.path.expanduser("~/.continue/diffs")] + + # If not adding highlighted code + if not self.adding_highlighted_code: + if len(self.highlighted_ranges) == 1 and len(range_in_files) <= 1 and (len(range_in_files) == 0 or range_in_files[0].range.start == range_in_files[0].range.end): + # If un-highlighting the range to edit, then remove the range + self.highlighted_ranges = [] + elif len(range_in_files) > 0: + # Otherwise, replace the current range with the new one + # This is the first range to be highlighted + self.highlighted_ranges = [ + HighlightedRangeContextItem( + rif=range_in_files[0], + item=self._rif_to_context_item(range_in_files[0], 0, True))] + + return + + # If current range overlaps with any others, delete them and only keep the new range + new_ranges = [] + for i, hr in enumerate(self.highlighted_ranges): + found_overlap = False + for new_rif in range_in_files: + if hr.rif.filepath == new_rif.filepath and hr.rif.range.overlaps_with(new_rif.range): + found_overlap = True + break + + # Also don't allow multiple ranges in same file with same content. This is useless to the model, and avoids + # the bug where cmd+f causes repeated highlights + if hr.rif.filepath == new_rif.filepath and hr.rif.contents == new_rif.contents: + found_overlap = True + break + + if not found_overlap: + new_ranges.append(HighlightedRangeContextItem(rif=hr.rif, item=self._rif_to_context_item( + hr.rif, len(new_ranges), False))) + + self.highlighted_ranges = new_ranges + [HighlightedRangeContextItem(rif=rif, item=self._rif_to_context_item( + rif, len(new_ranges) + idx, False)) for idx, rif in enumerate(range_in_files)] + + self._make_sure_is_editing_range() + self._disambiguate_highlighted_ranges() + + async def set_editing_at_ids(self, ids: List[str]): + for hr in self.highlighted_ranges: + hr.item.editing = hr.item.description.id.to_string() in ids + + async def add_context_item(self, id: ContextItemId, query: str, search_client: meilisearch.Client, prev: List[ContextItem] = None) -> List[ContextItem]: + raise NotImplementedError() diff --git a/continuedev/src/continuedev/plugins/steps/custom_command.py b/continuedev/src/continuedev/plugins/steps/custom_command.py index 1491a975..419b3c3d 100644 --- a/continuedev/src/continuedev/plugins/steps/custom_command.py +++ b/continuedev/src/continuedev/plugins/steps/custom_command.py @@ -1,6 +1,6 @@ from ...libs.util.templating import render_templated_string from ...core.main import Step -from ...core.sdk import ContinueSDK +from ...core.sdk import ContinueSDK, Models from ..steps.chat import SimpleChatStep diff --git a/extension/media/continue-dev-square.png b/extension/media/continue-dev-square.png new file mode 100644 index 00000000..e4b62556 Binary files /dev/null and b/extension/media/continue-dev-square.png differ diff --git a/extension/media/terminal-continue.png b/extension/media/terminal-continue.png new file mode 100644 index 00000000..ef310fa3 Binary files /dev/null and b/extension/media/terminal-continue.png differ diff --git a/extension/package.json b/extension/package.json index 6618ff45..cfce77df 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,6 +1,6 @@ { "name": "continue", - "icon": "media/continue-gradient.png", + "icon": "media/terminal-continue.png", "repository": { "type": "git", "url": "https://github.com/continuedev/continue" @@ -157,7 +157,7 @@ { "id": "continue", "title": "Continue ", - "icon": "react-app/dist/play_button.png" + "icon": "media/continue-dev-square.png" } ] }, diff --git a/schema/json/ContextItem.json b/schema/json/ContextItem.json index 35766f11..32a214d3 100644 --- a/schema/json/ContextItem.json +++ b/schema/json/ContextItem.json @@ -1,6 +1,6 @@ { "title": "ContextItem", - "$ref": "#/definitions/src__continuedev__core__context__ContextItem", + "$ref": "#/definitions/src__continuedev__core__main__ContextItem", "definitions": { "ContextItemId": { "title": "ContextItemId", @@ -44,7 +44,7 @@ "id" ] }, - "src__continuedev__core__context__ContextItem": { + "src__continuedev__core__main__ContextItem": { "title": "ContextItem", "description": "A ContextItem is a single item that is stored in the ContextManager.", "type": "object", -- cgit v1.2.3-70-g09d2