From 6f209db9a084bb04102e847db40e4ddb9c293dd3 Mon Sep 17 00:00:00 2001 From: Terminal <32599364+TerminalFi@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:49:01 -0500 Subject: [PATCH] feat: allow user customizable prompts for reusable logic (#194) Co-authored-by: Jack Cherng --- LSP-copilot.sublime-settings | 17 +++++ plugin/client.py | 6 +- plugin/commands.py | 131 +++++++++++++++++++++-------------- plugin/helpers.py | 129 +++++++++++++++++++++++++--------- plugin/types.py | 92 ++++++++++++++++++++---- requirements-dev.txt | 2 +- sublime-package.json | 61 +++++++++++----- 7 files changed, 319 insertions(+), 119 deletions(-) diff --git a/LSP-copilot.sublime-settings b/LSP-copilot.sublime-settings index 7d1e0f5..de5bdf8 100644 --- a/LSP-copilot.sublime-settings +++ b/LSP-copilot.sublime-settings @@ -21,6 +21,23 @@ // The (Jinja2) template of the status bar text which is inside the parentheses `(...)`. // See https://jinja.palletsprojects.com/templates/ "status_text": "{% if is_copilot_ignored %}{{ is_copilot_ignored }}{% elif is_waiting %}{{ is_waiting }}{% elif server_version %}v{{ server_version }}{% endif %}", + "prompts": [ + { + "id": "review", + "description": "Review code and provide feedback.", + "prompt": [ + "Review the referenced code and provide feedback.", + "Feedback should first reply back with the line or lines of code, followed by the feedback about the code.", + "Do not invent new problems.", + "The feedback should be constructive and aim to improve the code quality.", + "If there are no issues detected, reply that the code looks good and no changes are necessary.", + "Group related feedback into a single comment if possible.", + "Present each comment with a brief description of the issue and a suggestion for improvement.", + "Use the format `Comment #: [description] [suggestion]` for each comment, # representing the number of comments.", + "At last provide a summary of the overall code quality and any general suggestions for improvement.", + ] + } + ] }, // ST4 configuration "selector": "source | text | embedding" diff --git a/plugin/client.py b/plugin/client.py index 315f8ad..d411937 100644 --- a/plugin/client.py +++ b/plugin/client.py @@ -33,7 +33,7 @@ ActivityIndicator, CopilotIgnore, GithubInfo, - prepare_completion_request, + prepare_completion_request_doc, preprocess_completions, preprocess_panel_completions, ) @@ -399,7 +399,7 @@ def _request_completions(self, view: sublime.View, request: str, *, no_callback: ): return - if not (params := prepare_completion_request(view)): + if not (doc := prepare_completion_request_doc(view)): return if no_callback: @@ -410,7 +410,7 @@ def _request_completions(self, view: sublime.View, request: str, *, no_callback: self._activity_indicator.start() callback = functools.partial(self._on_get_completions, view, region=sel[0].to_tuple()) - session.send_request_async(Request(request, params), callback) + session.send_request_async(Request(request, {"doc": doc}), callback) def _on_get_completions( self, diff --git a/plugin/commands.py b/plugin/commands.py index 9386601..9c96c1a 100644 --- a/plugin/commands.py +++ b/plugin/commands.py @@ -40,11 +40,14 @@ from .decorators import _must_be_active_view from .helpers import ( GithubInfo, - prepare_completion_request, + prepare_completion_request_doc, + prepare_conversation_turn_request, preprocess_chat_message, preprocess_message_for_html, ) from .types import ( + CopilotPayloadConversationCreate, + CopilotPayloadConversationPreconditions, CopilotPayloadConversationTemplate, CopilotPayloadFileStatus, CopilotPayloadGetVersion, @@ -54,7 +57,8 @@ CopilotPayloadSignInConfirm, CopilotPayloadSignInInitiate, CopilotPayloadSignOut, - CopilotRequestCoversationAgent, + CopilotRequestConversationAgent, + CopilotUserDefinedPromptTemplates, T_Callable, ) from .ui import ViewCompletionManager, ViewPanelCompletionManager, WindowConversationManager @@ -196,12 +200,14 @@ def run(self, view_id: int | None = None) -> None: view = self.window.active_view() else: view = find_view_by_id(view_id) + if not view: return + ViewPanelCompletionManager(view).close() -class CopilotConversationChatShimCommand(LspWindowCommand): +class CopilotConversationChatShimCommand(CopilotWindowCommand): def run(self, window_id: int, message: str = "") -> None: if not (window := find_window_by_id(window_id)): return @@ -213,7 +219,7 @@ def run(self, window_id: int, message: str = "") -> None: view.run_command("copilot_conversation_chat", {"message": message}) -class CopilotConversationChatCommand(LspTextCommand): +class CopilotConversationChatCommand(CopilotTextCommand): @_provide_plugin_session() def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, message: str = "") -> None: if not (window := self.view.window()): @@ -234,7 +240,11 @@ def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, message: ) def _on_result_conversation_preconditions( - self, plugin: CopilotPlugin, session: Session, payload, initial_message: str + self, + plugin: CopilotPlugin, + session: Session, + payload: CopilotPayloadConversationPreconditions, + initial_message: str, ) -> None: if not (window := self.view.window()): return @@ -243,7 +253,8 @@ def _on_result_conversation_preconditions( if not (view := find_view_by_id(wcm.last_active_view_id)): return - is_template, msg = preprocess_chat_message(view, initial_message) + user_prompts: list[CopilotUserDefinedPromptTemplates] = session.config.settings.get("prompts") or [] + is_template, msg = preprocess_chat_message(view, initial_message, user_prompts) if msg: wcm.append_conversation_entry({ "kind": plugin.get_account_status().user or "user", @@ -272,7 +283,12 @@ def _on_result_conversation_preconditions( wcm.is_waiting = True wcm.update() - def _on_result_conversation_create(self, plugin: CopilotPlugin, session: Session, payload) -> None: + def _on_result_conversation_create( + self, + plugin: CopilotPlugin, + session: Session, + payload: CopilotPayloadConversationCreate, + ) -> None: if not (window := self.view.window()): return @@ -292,8 +308,8 @@ def _on_prompt(self, plugin: CopilotPlugin, session: Session, msg: str): if not (view := find_view_by_id(wcm.last_active_view_id)): return - - is_template, msg = preprocess_chat_message(view, msg) + user_prompts: list[CopilotUserDefinedPromptTemplates] = session.config.settings.get("prompts") or [] + is_template, msg = preprocess_chat_message(view, msg, user_prompts) wcm.append_conversation_entry({ "kind": plugin.get_account_status().user or "user", "conversationId": wcm.conversation_id, @@ -302,23 +318,11 @@ def _on_prompt(self, plugin: CopilotPlugin, session: Session, msg: str): "annotations": [], "hideText": False, }) - - if not (request := prepare_completion_request(view)): + if not (request := prepare_conversation_turn_request(wcm.conversation_id, wcm.window.id(), msg, view)): return session.send_request( - Request( - REQ_CONVERSATION_TURN, - { - "conversationId": wcm.conversation_id, - "message": msg, - "workDoneToken": f"copilot_chat://{wcm.window.id()}", - "doc": request["doc"], - "computeSuggestions": True, - "references": [], - "source": "panel", - }, - ), + Request(REQ_CONVERSATION_TURN, request), lambda _: wcm.prompt(callback=lambda x: self._on_prompt(plugin, session, x)), ) wcm.is_waiting = True @@ -335,7 +339,7 @@ def run(self, window_id: int | None = None) -> None: WindowConversationManager(window).close() -class CopilotConversationRatingShimCommand(LspWindowCommand): +class CopilotConversationRatingShimCommand(CopilotWindowCommand): def run(self, turn_id: str, rating: int) -> None: wcm = WindowConversationManager(self.window) if not (view := find_view_by_id(wcm.last_active_view_id)): @@ -343,7 +347,7 @@ def run(self, turn_id: str, rating: int) -> None: view.run_command("copilot_conversation_rating", {"turn_id": turn_id, "rating": rating}) -class CopilotConversationRatingCommand(LspTextCommand): +class CopilotConversationRatingCommand(CopilotTextCommand): @_provide_plugin_session() def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, turn_id: str, rating: int) -> None: session.send_request( @@ -354,15 +358,15 @@ def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, turn_id: "rating": rating, }, ), - self._on_result_coversation_rating, + self._on_result_conversation_rating, ) - def _on_result_coversation_rating(self, payload: Literal["OK"]) -> None: + def _on_result_conversation_rating(self, payload: Literal["OK"]) -> None: # Returns OK pass -class CopilotConversationDestroyShimCommand(LspWindowCommand): +class CopilotConversationDestroyShimCommand(CopilotWindowCommand): def run(self, conversation_id: str) -> None: wcm = WindowConversationManager(self.window) if not (view := find_view_by_id(wcm.last_active_view_id)): @@ -370,7 +374,7 @@ def run(self, conversation_id: str) -> None: view.run_command("copilot_conversation_destroy", {"conversation_id": conversation_id}) -class CopilotConversationDestroyCommand(LspTextCommand): +class CopilotConversationDestroyCommand(CopilotTextCommand): @_provide_plugin_session() def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, conversation_id: str) -> None: if not ( @@ -388,10 +392,10 @@ def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit, conversa "options": {}, }, ), - self._on_result_coversation_destroy, + self._on_result_conversation_destroy, ) - def _on_result_coversation_destroy(self, payload) -> None: + def _on_result_conversation_destroy(self, payload: str) -> None: if not (window := self.view.window()): return if payload != "OK": @@ -403,13 +407,16 @@ def _on_result_coversation_destroy(self, payload) -> None: wcm.close() wcm.reset() - def is_enabled(self, event: dict[Any, Any] | None = None, point: int | None = None) -> bool: + def is_enabled(self, event: dict[Any, Any] | None = None, point: int | None = None) -> bool: # type: ignore if not (window := self.view.window()): return False - return super().is_enabled() and bool(WindowConversationManager(window).conversation_id) + return bool( + super().is_enabled() # type: ignore + and WindowConversationManager(window).conversation_id + ) -class CopilotConversationTurnDeleteShimCommand(LspWindowCommand): +class CopilotConversationTurnDeleteShimCommand(CopilotWindowCommand): def run(self, window_id: int, conversation_id: str, turn_id: str) -> None: wcm = WindowConversationManager(self.window) if not (view := find_view_by_id(wcm.last_active_view_id)): @@ -420,7 +427,7 @@ def run(self, window_id: int, conversation_id: str, turn_id: str) -> None: ) -class CopilotConversationTurnDeleteCommand(LspTextCommand): +class CopilotConversationTurnDeleteCommand(CopilotTextCommand): @_provide_plugin_session() def run( self, @@ -453,10 +460,16 @@ def run( "options": {}, }, ), - lambda x: self._on_result_coversation_turn_delete(window_id, conversation_id, turn_id, x), + lambda x: self._on_result_conversation_turn_delete(window_id, conversation_id, turn_id, x), ) - def _on_result_coversation_turn_delete(self, window_id: int, conversation_id: str, turn_id: str, payload) -> None: + def _on_result_conversation_turn_delete( + self, + window_id: int, + conversation_id: str, + turn_id: str, + payload: str, + ) -> None: if payload != "OK": status_message("Failed to delete turn.") return @@ -476,7 +489,7 @@ def _on_result_coversation_turn_delete(self, window_id: int, conversation_id: st wcm.update() -class CopilotConversationCopyCodeCommand(LspWindowCommand): +class CopilotConversationCopyCodeCommand(CopilotWindowCommand): def run(self, window_id: int, code_block_index: int) -> None: if not (window := find_window_by_id(window_id)): return @@ -488,7 +501,7 @@ def run(self, window_id: int, code_block_index: int) -> None: sublime.set_clipboard(code) -class CopilotConversationInsertCodeShimCommand(LspWindowCommand): +class CopilotConversationInsertCodeShimCommand(CopilotWindowCommand): def run(self, window_id: int, code_block_index: int) -> None: if not (window := find_window_by_id(window_id)): return @@ -503,7 +516,7 @@ def run(self, window_id: int, code_block_index: int) -> None: view.run_command("copilot_conversation_insert_code", {"characters": code}) -class CopilotConversationInsertCodeCommand(LspTextCommand): +class CopilotConversationInsertCodeCommand(CopilotTextCommand): def run(self, edit: sublime.Edit, characters: str) -> None: if len(self.view.sel()) > 1: return @@ -513,34 +526,50 @@ def run(self, edit: sublime.Edit, characters: str) -> None: self.view.insert(edit, begin, characters) -class CopilotConversationAgentsCommand(LspTextCommand): +class CopilotConversationAgentsCommand(CopilotTextCommand): @_provide_plugin_session() def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit) -> None: - session.send_request(Request(REQ_CONVERSATION_AGENTS, {"options": {}}), self._on_result_coversation_agents) + session.send_request(Request(REQ_CONVERSATION_AGENTS, {"options": {}}), self._on_result_conversation_agents) - def _on_result_coversation_agents(self, payload: list[CopilotRequestCoversationAgent]) -> None: + def _on_result_conversation_agents(self, payload: list[CopilotRequestConversationAgent]) -> None: window = self.view.window() if not window: return window.show_quick_panel([[item["slug"], item["description"]] for item in payload], lambda _: None) -class CopilotConversationTemplatesCommand(LspTextCommand): +class CopilotConversationTemplatesCommand(CopilotTextCommand): @_provide_plugin_session() def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit) -> None: + user_prompts: list[CopilotUserDefinedPromptTemplates] = session.config.settings.get("prompts") or [] session.send_request( - Request(REQ_CONVERSATION_TEMPLATES, {"options": {}}), self._on_result_conversation_templates + Request(REQ_CONVERSATION_TEMPLATES, {"options": {}}), + lambda payload: self._on_result_conversation_templates(user_prompts, payload), ) - def _on_result_conversation_templates(self, payload: list[CopilotPayloadConversationTemplate]) -> None: + def _on_result_conversation_templates( + self, + user_prompts: list[CopilotUserDefinedPromptTemplates], + payload: list[CopilotPayloadConversationTemplate], + ) -> None: if not (window := self.view.window()): return + + templates = payload + user_prompts + prompts = [ + [item["id"], item["description"], ", ".join(item["scopes"]) if item.get("scopes", None) else "chat-panel"] + for item in templates + ] window.show_quick_panel( - [[item["id"], item["description"], ", ".join(item["scopes"])] for item in payload], - lambda index: self._on_selected(index, payload), + prompts, + lambda index: self._on_selected(index, templates), ) - def _on_selected(self, index: int, items: list[CopilotPayloadConversationTemplate]) -> None: + def _on_selected( + self, + index: int, + items: list[CopilotPayloadConversationTemplate | CopilotUserDefinedPromptTemplates], + ) -> None: if index == -1: return self.view.run_command("copilot_conversation_chat", {"message": f'/{items[index]["id"]}'}) @@ -588,7 +617,7 @@ def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit) -> None: class CopilotGetPanelCompletionsCommand(CopilotTextCommand): @_provide_plugin_session() def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit) -> None: - if not (params := prepare_completion_request(self.view)): + if not (doc := prepare_completion_request_doc(self.view)): return vcm = ViewPanelCompletionManager(self.view) @@ -596,7 +625,7 @@ def run(self, plugin: CopilotPlugin, session: Session, _: sublime.Edit) -> None: vcm.is_visible = True vcm.completions = [] - params["panelId"] = vcm.panel_id + params = {"doc": doc, "panelId": vcm.panel_id} session.send_request(Request(REQ_GET_PANEL_COMPLETIONS, params), self._on_result_get_panel_completions) def _on_result_get_panel_completions(self, payload: CopilotPayloadPanelCompletionSolutionCount) -> None: diff --git a/plugin/helpers.py b/plugin/helpers.py index 8e749ee..b63049a 100644 --- a/plugin/helpers.py +++ b/plugin/helpers.py @@ -5,20 +5,25 @@ import re import threading import time -from collections.abc import Callable from operator import itemgetter from pathlib import Path -from typing import Any +from typing import Any, Callable, Literal, Sequence, cast import sublime from LSP.plugin.core.url import filename_to_uri -from more_itertools import duplicates_everseen +from more_itertools import duplicates_everseen, first_true from wcmatch import glob from .constants import COPILOT_WINDOW_SETTINGS_PREFIX, PACKAGE_NAME from .log import log_error from .settings import get_plugin_setting_dotted -from .types import CopilotConversationTemplates, CopilotPayloadCompletion, CopilotPayloadPanelSolution +from .types import ( + CopilotDocType, + CopilotPayloadCompletion, + CopilotPayloadPanelSolution, + CopilotRequestConversationTurn, + CopilotUserDefinedPromptTemplates, +) from .utils import ( all_views, all_windows, @@ -161,33 +166,82 @@ def trigger(self, view: sublime.View) -> bool: return False -def prepare_completion_request(view: sublime.View) -> dict[str, Any] | None: +def prepare_completion_request_doc(view: sublime.View, max_selections: int = 1) -> CopilotDocType | None: if not view: return None - if len(sel := view.sel()) != 1: + if len(sel := view.sel()) > max_selections or len(sel) == 0: return None file_path = view.file_name() or f"buffer:{view.buffer().id()}" row, col = view.rowcol(sel[0].begin()) return { - "doc": { - "source": view.substr(sublime.Region(0, view.size())), - "tabSize": view.settings().get("tab_size"), - "indentSize": 1, # there is no such concept in ST - "insertSpaces": view.settings().get("translate_tabs_to_spaces"), - "path": file_path, - "uri": file_path if file_path.startswith("buffer:") else file_path and filename_to_uri(file_path), - "relativePath": get_project_relative_path(file_path), - "languageId": get_view_language_id(view), - "position": {"line": row, "character": col}, - # Buffer Version. Generally this is handled by LSP, but we need to handle it here - # Will need to test getting the version from LSP - "version": view.change_count(), - } + "source": view.substr(sublime.Region(0, view.size())), + "tabSize": cast(int, view.settings().get("tab_size")), + "indentSize": 1, # there is no such concept in ST + "insertSpaces": cast(bool, view.settings().get("translate_tabs_to_spaces")), + "path": file_path, + "uri": file_path if file_path.startswith("buffer:") else filename_to_uri(file_path), + "relativePath": get_project_relative_path(file_path), + "languageId": get_view_language_id(view), + "position": {"line": row, "character": col}, + # Buffer Version. Generally this is handled by LSP, but we need to handle it here + # Will need to test getting the version from LSP + "version": view.change_count(), } +def prepare_conversation_turn_request( + conversation_id: str, + window_id: int, + message: str, + view: sublime.View, + source: Literal["panel", "inline"] = "panel", +) -> CopilotRequestConversationTurn | None: + if not (doc := prepare_completion_request_doc(view, max_selections=5)): + return None + turn: CopilotRequestConversationTurn = { + "conversationId": conversation_id, + "message": message, + "workDoneToken": f"copilot_chat://{window_id}", + "doc": doc, + "computeSuggestions": True, + "references": [], + "source": source, + } + + visible_region = view.visible_region() + visible_start = view.rowcol(visible_region.begin()) + visible_end = view.rowcol(visible_region.end()) + + # References can technicaly be across multiple files + # TODO: Support references across multiple files + for selection in view.sel(): + if selection.empty() or view.substr(selection).strip() == "": + continue + file_path = view.file_name() or f"buffer:{view.buffer().id()}" + selection_start = view.rowcol(selection.begin()) + selection_end = view.rowcol(selection.end()) + turn["references"].append({ + "type": "file", + "status": "included", + "uri": file_path if file_path.startswith("buffer:") else filename_to_uri(file_path), + "range": doc["position"], + "visibleRange": { + "start": {"line": visible_start[0], "character": visible_start[1]}, + "end": {"line": visible_end[0], "character": visible_end[1]}, + }, + "selection": { + "start": {"line": selection_start[0], "character": selection_start[1]}, + "end": {"line": selection_end[0], "character": selection_end[1]}, + }, + }) + return turn + + def preprocess_message_for_html(message: str) -> str: + def _escape_html(text: str) -> str: + return re.sub(r"<(.*?)>", r"<\1>", text) + new_lines: list[str] = [] inside_code_block = False inline_code_pattern = re.compile(r"`([^`]*)`") @@ -200,28 +254,41 @@ def preprocess_message_for_html(message: str) -> str: escaped_line = "" start = 0 for match in inline_code_pattern.finditer(line): - escaped_line += re.sub(r"<(.*?)>", r"<\1>", line[start : match.start()]) - escaped_line += match.group(0) + escaped_line += _escape_html(line[start : match.start()]) + match.group(0) start = match.end() - escaped_line += re.sub(r"<(.*?)>", r"<\1>", line[start:]) + escaped_line += _escape_html(line[start:]) new_lines.append(escaped_line) else: new_lines.append(line) return "\n".join(new_lines) -def preprocess_chat_message(view: sublime.View, message: str) -> tuple[bool, str]: +def preprocess_chat_message( + view: sublime.View, + message: str, + templates: Sequence[CopilotUserDefinedPromptTemplates] | None = None, +) -> tuple[bool, str]: from .template import load_string_template - is_template = message in CopilotConversationTemplates - if is_template: - message += " {{ sel[0] }}" + templates = templates or [] + user_template = first_true(templates, pred=lambda t: f"/{t['id']}" == message) + is_template = False + + if user_template: + is_template = True + message += "\n\n{{ user_prompt }}\n\n{{ code }}" + else: + return False, message + + region = view.sel()[0] + lang = get_view_language_id(view, region.begin()) template = load_string_template(message) - lang = get_view_language_id(view, view.sel()[0].begin()) - sel = [f"\n```{lang}\n{view.substr(region)}\n```\n" for region in view.sel()] + message = template.render( + code=f"\n```{lang}\n{view.substr(region)}\n```\n", + user_prompt="\n".join(user_template["prompt"]) if user_template else "", + ) - message = template.render({"sel": sel}) return is_template, message @@ -247,7 +314,7 @@ def preprocess_completions(view: sublime.View, completions: list[CopilotPayloadC _generate_completion_region(view, completion) -def preprocess_panel_completions(view: sublime.View, completions: list[CopilotPayloadPanelSolution]) -> None: +def preprocess_panel_completions(view: sublime.View, completions: Sequence[CopilotPayloadPanelSolution]) -> None: """Preprocess the `completions` from "getCompletionsCycling" request.""" for completion in completions: _generate_completion_region(view, completion) diff --git a/plugin/types.py b/plugin/types.py index f569662..da88f79 100644 --- a/plugin/types.py +++ b/plugin/types.py @@ -3,6 +3,8 @@ from dataclasses import dataclass from typing import Any, Callable, Literal, Tuple, TypedDict, TypeVar +from LSP.plugin.core.typing import StrEnum + T_Callable = TypeVar("T_Callable", bound=Callable[..., Any]) @@ -35,6 +37,34 @@ class NetworkProxy(TypedDict, total=True): rejectUnauthorized: bool +# ------------------- # +# basic Copilot types # +# ------------------- # + + +class CopilotPositionType(TypedDict, total=True): + character: int + line: int + + +class CopilotRangeType(TypedDict, total=True): + start: CopilotPositionType + end: CopilotPositionType + + +class CopilotDocType(TypedDict, total=True): + source: str + tabSize: int + indentSize: int + insertSpaces: bool + path: str + uri: str + relativePath: str + languageId: str + position: CopilotPositionType + version: int + + # --------------- # # Copilot payload # # --------------- # @@ -44,21 +74,11 @@ class CopilotPayloadFileStatus(TypedDict, total=True): status: Literal["not included", "included"] -class CopilotPayloadCompletionPosition(TypedDict, total=True): - character: int - line: int - - -class CopilotPayloadCompletionRange(TypedDict, total=True): - start: CopilotPayloadCompletionPosition - end: CopilotPayloadCompletionPosition - - class CopilotPayloadCompletion(TypedDict, total=True): text: str - position: CopilotPayloadCompletionPosition + position: CopilotPositionType uuid: str - range: CopilotPayloadCompletionRange + range: CopilotRangeType displayText: str point: StPoint region: StRegion @@ -131,7 +151,7 @@ class CopilotPayloadPanelSolution(TypedDict, total=True): score: int panelId: str completionText: str - range: CopilotPayloadCompletionRange + range: CopilotRangeType region: StRegion @@ -144,6 +164,14 @@ class CopilotPayloadPanelCompletionSolutionCount(TypedDict, total=True): # --------------------- # +class CopilotConversationTemplates(StrEnum): + FIX = "/fix" + TESTS = "/tests" + DOC = "/doc" + EXPLAIN = "/explain" + SIMPLIFY = "/simplify" + + class CopilotPayloadConversationEntry(TypedDict, total=True): kind: str conversationId: str @@ -170,12 +198,42 @@ class CopilotPayloadConversationTemplate(TypedDict, total=True): scopes: list[str] -class CopilotRequestCoversationAgent(TypedDict, total=True): +class CopilotRequestConversationTurn(TypedDict, total=True): + conversationId: str + message: str + workDoneToken: str + doc: CopilotDocType + computeSuggestions: bool + references: list[CopilotRequestConversationTurnReference] + source: Literal["panel", "inline"] + + +class CopilotRequestConversationTurnReference(TypedDict, total=True): + type: str + status: str + uri: str + range: CopilotPositionType + visibleRange: CopilotRangeType + selection: CopilotRangeType + + +class CopilotRequestConversationAgent(TypedDict, total=True): slug: str name: str description: str +class CopilotPayloadConversationPreconditions(TypedDict, total=True): + pass + + +class CopilotPayloadConversationCreate(TypedDict, total=True): + conversationId: str + """E.g., `"15d1791c-42f4-490c-9f79-0b79c4142d17"`.""" + turnId: str + """E.g., `"a4a3785f-808f-41cc-8037-cd6707ffe584"`.""" + + class CopilotPayloadConversationContext(TypedDict, total=True): conversationId: str """E.g., `"e3b0d5e3-0c3b-4292-a5ea-15d6003e7c45"`.""" @@ -184,4 +242,8 @@ class CopilotPayloadConversationContext(TypedDict, total=True): skillId: Literal["current-editor", "project-labels", "recent-files"] # not the complet list yet -CopilotConversationTemplates = {"/fix", "/tests", "/doc", "/explain", "/simplify"} +class CopilotUserDefinedPromptTemplates(TypedDict, total=True): + id: str + description: str + prompt: list[str] + scopes: list[str] diff --git a/requirements-dev.txt b/requirements-dev.txt index 55e139b..86f4756 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ markupsafe==2.1.5 # via jinja2 more-itertools==10.3.0 # via -r requirements.in -mypy==1.11.0 +mypy==1.11.1 # via -r requirements-dev.in mypy-extensions==1.0.0 # via mypy diff --git a/sublime-package.json b/sublime-package.json index c96e327..2645817 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -19,6 +19,19 @@ "additionalProperties": false, "type": "object", "properties": { + "authProvider": { + "default": "github", + "markdownDescription": "The GitHub identity to use for Copilot", + "type": "string", + "enumDescriptions": [ + "GitHub.com", + "GitHub Enterprise" + ], + "enum": [ + "github", + "github-enterprise" + ], + }, "auto_ask_completions": { "default": true, "description": "Auto ask the server for completions. Otherwise, you have to trigger it manually.", @@ -43,19 +56,6 @@ "markdownDescription": "Enables `debug` mode fo the LSP-copilot. Enabling all commands regardless of status requirements.", "type": "boolean" }, - "authProvider": { - "default": "github", - "markdownDescription": "The GitHub identity to use for Copilot", - "type": "string", - "enumDescriptions": [ - "GitHub.com", - "GitHub Enterprise" - ], - "enum": [ - "github", - "github-enterprise" - ], - }, "github-enterprise": { "type": "object", "markdownDescription": "The configuration for Github Enterprise.", @@ -77,20 +77,45 @@ "description": "Enables local checks. This feature is not fully understood yet.", "type": "boolean" }, + "prompts": { + "default": true, + "markdownDescription": "Enables custom user prompts for Copilot completions.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "markdownDescription": "The ID of the prompt that acts as the trigger. `/` is automatically assumed and shouldn't be included.", + "type": "string" + }, + "description": { + "markdownDescription": "The description of the prompt.", + "type": "string" + }, + "prompt": { + "markdownDescription": "The prompt message.", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, "proxy": { "default": "", "markdownDescription": "The HTTP proxy to use for Copilot requests. It's in the form of `username:password@host:port` or just `host:port`.", "type": "string" }, - "telemetry": { - "default": false, - "markdownDescription": "Enables Copilot telemetry requests for `Accept` and `Reject` completions.", - "type": "boolean" - }, "status_text": { "default": "{% if server_version %}v{{ server_version }}{% endif %}", "markdownDescription": "The (Jinja2) template of the status bar text which is inside the parentheses `(...)`. See https://jinja.palletsprojects.com/templates/", "type": "string" + }, + "telemetry": { + "default": false, + "markdownDescription": "Enables Copilot telemetry requests for `Accept` and `Reject` completions.", + "type": "boolean" } } }