From b8bec3d439f28ebecba857d8d35b0a9cc31a083b Mon Sep 17 00:00:00 2001 From: knilink Date: Sat, 7 Sep 2024 17:23:35 +1000 Subject: [PATCH 1/3] feat: customizable lsp event handler and lsp server log --- README.md | 23 ++++++++++++- copilot.el | 94 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index febe6cdd..dc16d9c8 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,27 @@ For example: (setq copilot-network-proxy '(:host "127.0.0.1" :port 7890)) ``` +### copilot-on-request +Register a handler to be called when a request of type method is received. Return JSON serializable as result or calling `jsonrpc-error` for errors. [readmore](https://www.gnu.org/software/emacs/manual/html_node/elisp/JSONRPC-Overview.html) + +For example: +```elisp +; Display desktop notification if emacs is built with d-bus +(copilot-on-request + 'window/showMessageRequest + (lambda (msg) (notifications-notify :title "Emacs Copilot" :body (plist-get msg :message)))) +``` + +### copilot-on-notification +Register a listener for copilot notifications. + +For example: +```elisp +(copilot-on-notification + 'window/logMessage + (lambda (msg) (message (plist-get msg :message)) +``` + ## Known Issues ### Wrong Position of Other Completion Popups @@ -319,7 +340,7 @@ But I decided to allow them to coexist, allowing you to choose a better one at a ## Reporting Bugs + Make sure you have restarted your Emacs (and rebuild the plugin if necessary) after updating the plugin. -+ Please enable event logging by customize `copilot-log-max` (to e.g. 1000), then paste related logs in the `*copilot events*` and `*copilot stderr*` buffer. ++ Please enable event logging by customize `copilot-log-max` (to e.g. 1000) and enable debug log `(setq copilot-server-args '("--stdio" "--debug"))`, then paste related logs in the `*copilot events*`, `*copilot stderr*` and `*copilot agent log*` buffer. + If an exception is thrown, please also paste the stack trace (use `M-x toggle-debug-on-error` to enable stack trace). ## Roadmap diff --git a/copilot.el b/copilot.el index b6981c7d..9a274f57 100644 --- a/copilot.el +++ b/copilot.el @@ -290,6 +290,7 @@ SUCCESS-FN is the CALLBACK." #'make-instance 'jsonrpc-process-connection :name "copilot" + :request-dispatcher #'copilot--handle-request :notification-dispatcher #'copilot--handle-notification :process (make-process :name "copilot agent" :command (append @@ -624,31 +625,76 @@ automatically, browse to %s." user-code verification-uri)) (defvar copilot--panel-lang nil "Language of current panel solutions.") +(defvar copilot--request-handlers (make-hash-table :test 'equal) + "Hash table storing request handlers.") + +(defun copilot-on-request (method handler) + "Register HANDLER to be called when a request of type METHOD is received. +Each METHOD can have only one HANDLER." + (puthash method handler copilot--request-handlers)) + +(defun copilot--handle-request (_ method msg) + "Handle MSG of type METHOD by calling the appropriate registered handler." + (let ((handler (gethash method copilot--request-handlers))) + (when handler + (funcall handler msg)))) + +(defvar copilot--notification-handlers (make-hash-table :test 'equal) + "Hash table storing lists of notification handlers.") + +(defun copilot-on-notification (method handler) + "Register HANDLER to be called when a notification of type METHOD is received." + (let ((handlers (gethash method copilot--notification-handlers '()))) + (puthash method (cons handler handlers) copilot--notification-handlers))) + (defun copilot--handle-notification (_ method msg) - "Handle MSG of type METHOD." - (when (eql method 'PanelSolution) - (copilot--dbind (:completionText completion-text :score completion-score) msg - (with-current-buffer "*copilot-panel*" - (unless (member (secure-hash 'sha256 completion-text) - (org-map-entries (lambda () (org-entry-get nil "SHA")))) - (save-excursion - (goto-char (point-max)) - (insert "* Solution\n" - " :PROPERTIES:\n" - " :SCORE: " (number-to-string completion-score) "\n" - " :SHA: " (secure-hash 'sha256 completion-text) "\n" - " :END:\n" - "#+BEGIN_SRC " copilot--panel-lang "\n" - completion-text "\n#+END_SRC\n\n") - (call-interactively #'mark-whole-buffer) - (org-sort-entries nil ?R nil nil "SCORE")))))) - (when (eql method 'PanelSolutionsDone) - (message "Copilot: Finish synthesizing solutions.") - (display-buffer "*copilot-panel*") - (with-current-buffer "*copilot-panel*" - (save-excursion - (goto-char (point-max)) - (insert "End of solutions.\n"))))) + "Handle MSG of type METHOD by calling all appropriate registered handlers." + (let ((handlers (gethash method copilot--notification-handlers '()))) + (dolist (handler handlers) + (funcall handler msg)))) + +(copilot-on-notification + 'window/logMessage + (lambda (msg) + (copilot--dbind (:type log-level :message log-msg) msg + (with-current-buffer (get-buffer-create "*copilot agent log*") + (save-excursion + (goto-char (point-max)) + (insert (propertize (concat log-msg "\n") + 'face (pcase log-level + (4 '(:foreground "gray")) + (3 '(:foreground "green")) + (2 '(:foreground "yellow")) + (1 '(:foreground "red")))))))))) + +(copilot-on-notification + 'PanelSolution + (lambda (msg) + (copilot--dbind (:completionText completion-text :score completion-score) msg + (with-current-buffer "*copilot-panel*" + (unless (member (secure-hash 'sha256 completion-text) + (org-map-entries (lambda () (org-entry-get nil "SHA")))) + (save-excursion + (goto-char (point-max)) + (insert "* Solution\n" + " :PROPERTIES:\n" + " :SCORE: " (number-to-string completion-score) "\n" + " :SHA: " (secure-hash 'sha256 completion-text) "\n" + " :END:\n" + "#+BEGIN_SRC " copilot--panel-lang "\n" + completion-text "\n#+END_SRC\n\n") + (call-interactively #'mark-whole-buffer) + (org-sort-entries nil ?R nil nil "SCORE"))))))) + +(copilot-on-notification + 'PanelSolutionsDone + (lambda (_msg) + (message "Copilot: Finish synthesizing solutions.") + (display-buffer "*copilot-panel*") + (with-current-buffer "*copilot-panel*" + (save-excursion + (goto-char (point-max)) + (insert "End of solutions.\n"))))) (defun copilot--get-panel-completions (callback) "Get panel completions with CALLBACK." From 3dc18aef7d218e8de2e0ffc8c1d8aa2caa12dcc7 Mon Sep 17 00:00:00 2001 From: knilink Date: Sat, 7 Sep 2024 17:18:36 +1000 Subject: [PATCH 2/3] feat(version): Upgrade to Copilot 1.40.0 --- copilot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/copilot.el b/copilot.el index 9a274f57..4c53a6d5 100644 --- a/copilot.el +++ b/copilot.el @@ -142,7 +142,7 @@ find indentation offset." (defvar copilot--server-executable nil "The dist directory containing agent.js file.") -(defcustom copilot-version "1.27.0" +(defcustom copilot-version "1.40.0" "Copilot version. The default value is the preferred version and ensures functionality. @@ -329,6 +329,7 @@ Please upgrade the server via `M-x copilot-reinstall-server`")) (setq copilot--connection (copilot--make-connection)) (message "Copilot agent started.") (copilot--request 'initialize '(:capabilities (:workspace (:workspaceFolders t)))) + (copilot--notify 'initialized '()) (copilot--async-request 'setEditorInfo `(:editorInfo (:name "Emacs" :version ,emacs-version) :editorPluginInfo (:name "copilot.el" :version ,copilot-version) From 26a7055e217d8b061506f897761b09f534fb0d9c Mon Sep 17 00:00:00 2001 From: knilink Date: Wed, 25 Sep 2024 18:56:30 +1000 Subject: [PATCH 3/3] feat(chat): copilot chat proof of concept --- copilot-chat.el | 145 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 copilot-chat.el diff --git a/copilot-chat.el b/copilot-chat.el new file mode 100644 index 00000000..11f95aff --- /dev/null +++ b/copilot-chat.el @@ -0,0 +1,145 @@ +(require 'json) +(require 'copilot) + +(defun buffer-file-uri () + (interactive) + (let ((file-name (buffer-file-name))) + (and file-name (concat "file://" file-name)))) + + +(defvar buffer-context nil) +(defvar conversation-id nil) +(defvar work-done-token -1) + +(defconst chat-buffer-name "*Copilot Chat*") + +(defun conversation-turn (conv-id chat-msg) + (setq work-done-token (1+ work-done-token)) + (copilot--async-request + 'conversation/turn + (list :workDoneToken work-done-token + :conversationId conv-id + :message chat-msg))) + + + +(defun my-chat-send-message (source) + "Prompt the user to enter a message and add it to the chat buffer." + (let ((chat-msg (read-string "Enter your message: ")) + (chat-buffer (get-buffer chat-buffer-name))) + (setq buffer-context (current-buffer)) + (message "[buffer-context] %s" buffer-context) + (if (and chat-buffer conversation-id) + (with-current-buffer chat-buffer + (display-buffer chat-buffer) + (end-of-buffer) + (message "conversation/turn %s" chat-buffer) + (insert (concat "User: " chat-msg "\n\n")) + (conversation-turn conversation-id chat-msg)) + (let + ((new-chat-buffer (get-buffer-create chat-buffer-name)) + (document-uri (buffer-file-uri))) + (display-buffer new-chat-buffer) + (with-current-buffer new-chat-buffer + (insert (concat "User: " chat-msg "\n\n")) + (message "conversation/create") + (message document-uri) + (setq work-done-token (1+ work-done-token)) + (copilot--async-request + 'conversation/create + (list :workDoneToken work-done-token + :turns `[(:request ,chat-msg)] + :capabilities '(:skills [ + "current-editor" ;to inform language server the editor support conversation/context current-editor skill + ]) + :doc (list :uri document-uri) + :source source + ) + :success-fn (jsonrpc-lambda (&key conversationId turnId agentSlug)(setq conversation-id conversationId)))))))) + + +; +(defun current-editor-skill (conversation-id turn-id) + (message "[conversation/context][buffer] %s" buffer-context) + (with-current-buffer buffer-context + (let ((res `[(:uri ,(buffer-file-uri) + :position (:line ,(line-number-at-pos) :character ,(current-column)) + :visibleRange (:start (:line ,(line-number-at-pos (window-start)) :character 1) + :end (:line ,(1+ (line-number-at-pos (window-end))) :character 1)) + ,@(if (region-active-p) + `(:selection (:start (:line ,(line-number-at-pos (region-beginning)) :character 1) + :end (:line ,(1+ (line-number-at-pos (region-end))) :character 1)))) + + ;:openedAt + ;:activeAt + ) + nil])) + (message "[conversation/context] %s" res) + res))) + +(copilot-on-request + 'conversation/context + (lambda (msg) + (message (format "[conversation/context][buffer] %s" buffer-context)) + (with-current-buffer buffer-context + (let ((res (copilot--dbind (:conversationId conversation-id :turnId turn-id :skillId skill-id) msg + (cond + ((string= skill-id "current-editor") (current-editor-skill conversation-id turn-id)) + (t '[nil (:code -1 :message "handle skill")]))))) + (message (json-encode res)) + res)))) + +(defun on-progress(msg) + (message (format "[$/progress] %s" msg)) + (copilot--dbind + (:token work-done-token + :value (:kind kind + :title title + :conversationId conversation-id + :turnId turn-id + :reply reply ; streaming response by lines when kind="report" + :suggestedTitle suggested-title ; receive at the end of turn of "panel" source coversation + :followUp follow-up ; receive at the end of turn of "panel" source coversation + :updatedDocuments updated-documents ; receive at the end of turn of "inline" source coversation + )) + + msg + (message "[progress][buffer] %s" (get-buffer chat-buffer-name)) + (with-current-buffer (get-buffer chat-buffer-name) + (end-of-buffer) + (message "[progress][kind] %s" kind) + (message "[progress][updateDocuments] %s" updated-documents) + (cond + ((string= kind "begin") (insert "Assistant: ")) + ((string= kind "end") + (when follow-up + (message "[Suggested followup]") + (insert "Suggested followup: ") + (insert follow-up-msg) + ) + (when updated-documents + (message "[updated-documents]") + (message (format "[updated-documents] %s" updated-documents)) + (update-documents updated-documents)) + (insert "\n\n")) + (reply (message "[reply] %s" reply) (insert reply)))))) + +(copilot-on-notification '$/progress (lambda (message) (on-progress message))) + + +(defun update-documents (updated-documents) + (seq-do (lambda (doc) + (let ((uri (plist-get doc :uri)) + (text (plist-get doc :text))) + (message "[update-documents] uri: %s" uri) + (message "[update-documents] text: %s" text) + (with-current-buffer (find-buffer-visiting uri) + (delete-region (point-min) (point-max)) + (insert text)) + )) + updated-documents)) + +(defun chat-inline () (interactive) (my-chat-send-message "inline")) +(defun chat-panel () (interactive) (my-chat-send-message "panel")) + +(provide 'copilot-chat)